diff --git a/.github/FUNDING.yaml b/.github/FUNDING.yaml deleted file mode 100644 index 78a5c42d..00000000 --- a/.github/FUNDING.yaml +++ /dev/null @@ -1 +0,0 @@ -github: DerGoogler diff --git a/.github/ISSUE_TEMPLATE/bug.yml b/.github/ISSUE_TEMPLATE/bug.yml new file mode 100644 index 00000000..4b8aaf9d --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug.yml @@ -0,0 +1,73 @@ +name: Bug Report +description: Report a bug +labels: [ "bug" ] +title: "[BUG] " +body: + - type: checkboxes + id: checklist + attributes: + label: Checklist + description: Ensure that our bug report form is appropriate for you + options: + - label: No one has submitted a similar or identical bug report before + required: true + - label: I'm using the latest version of MMRL + required: true + - type: textarea + id: bug + attributes: + label: Bug description + description: Please describe the bug + placeholder: | + e.g. Crashed when installing module + validations: + required: true + - type: textarea + id: expected + attributes: + label: Expected behavior + description: What did you expect to happen + placeholder: | + e.g. Install a module + validations: + required: true + - type: textarea + id: actual + attributes: + label: Actual behavior + description: What happened instead + placeholder: | + e.g. Crashed + validations: + required: true + - type: textarea + id: steps + attributes: + label: Steps to reproduce + description: How to reproduce the bug + placeholder: | + 1. Open the app + 2. Crashed + + What an app + - type: input + id: ui + attributes: + label: UI / OS + description: Your system UI or OS + placeholder: MIUI / OneUI / etc. + validations: + required: true + - type: input + id: android + attributes: + label: Android Version + description: Your Android Version + placeholder: "14" + validations: + required: true + - type: textarea + id: additional + attributes: + label: Additional info + description: Everything else you consider worthy that we didn't ask for \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml deleted file mode 100644 index 38bd12af..00000000 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ /dev/null @@ -1,101 +0,0 @@ -name: Bug report -description: Template for bug reports -title: '[Bug] ' -labels: bug -body: - - type: markdown - attributes: - value: | - # General - - > [!IMPORTANT] - > The following things are not MMRL related - > - **Install scripts** such as typos in there or other things. - > - Configured **ModFS**. Try delete `/data/adb/mmrl/modfs.v*.json` - > - Some parts of **ModConf**. Things like `Cannot find module 'dont-anoy'` are sometimes MMRL related, this libaray may has benn removed, renamed or does not exist. - > - General module functionality after the install. MMRL is *module manager* **not** a *module executer* - > - > If these rules are ignored then your issue will be closed. - - type: textarea - id: reproduce_steps - attributes: - label: To Reproduce Steps to reproduce the behavior - description: | - 1. Go to '...' - 2. Click on '....' - 3. Scroll down to '....' - 4. See error - validations: - required: true - - type: input - id: expected_behavior - attributes: - label: Expected behavior - description: A clear and concise description of what you expected to happen. - validations: - required: true - - type: textarea - id: screenshots - attributes: - label: Screenshots - description: If applicable, add screenshots to help explain your problem - placeholder: | - ![](https://...) - ![](https://...) - ![](https://...) - - type: textarea - id: logs - attributes: - label: Logs - description: | - To gather log use: - ```shell - logcat --pid=`pidof -s PACKAGENAME` -v color - ``` - render: logs - - type: markdown - attributes: - value: | - # Device info - - type: input - id: device - attributes: - label: Device - placeholder: IPhone5 - validations: - required: true - - type: input - id: os - attributes: - label: OS - placeholder: ROM and version, ie Havoc-OS 4.12 - validations: - required: true - - type: input - id: app_version - attributes: - label: App Version - placeholder: 2.19.18 - validations: - required: true - - type: input - id: user_agent - attributes: - label: User Agent - placeholder: Mozilla/5.0 (Linux; Android 11; Pixel 5) AppleWebKit/537.36 - - type: dropdown - id: root_solution - attributes: - label: Root solution - description: Which root you use - options: - - Magisk - - KernelSU - - APatch - validations: - required: true - - type: input - id: additional_context - attributes: - label: Additional context - description: Add any other context about the problem here diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml index 62fe077a..0086358d 100644 --- a/.github/ISSUE_TEMPLATE/config.yml +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -1,8 +1 @@ blank_issues_enabled: true -contact_links: - - name: MMRL Telegram group - url: https://t.me/RepoLoader - about: Join the oficial MMRL group and discuss with the developers and other community member. - - name: Fox2Code Telegram group - url: https://t.me/Fox2Code_Chat - about: Join the Telegram group if you want to discuss with the community. diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md deleted file mode 100644 index 6532412f..00000000 --- a/.github/ISSUE_TEMPLATE/feature_request.md +++ /dev/null @@ -1,20 +0,0 @@ ---- -name: Feature request -about: Suggest an idea for this project -title: '' -labels: enhancement -assignees: '' - ---- - -**Is your feature request related to a problem? Please describe.** -A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] - -**Describe the solution you'd like** -A clear and concise description of what you want to happen. - -**Describe alternatives you've considered** -A clear and concise description of any alternative solutions or features you've considered. - -**Additional context** -Add any other context or screenshots about the feature request here. \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/fr.yml b/.github/ISSUE_TEMPLATE/fr.yml new file mode 100644 index 00000000..54b5b1db --- /dev/null +++ b/.github/ISSUE_TEMPLATE/fr.yml @@ -0,0 +1,36 @@ +name: Feature Request +description: Suggest an idea for this project +labels: [ "enhancement" ] +title: "[FR] " +body: + - type: checkboxes + id: checklist + attributes: + label: Checklist + description: Ensure that our bug report form is appropriate for you + options: + - label: No one has submitted a similar or identical feature request before + required: true + - label: This suggestion does not depart from the original intention of MMRL + required: true + - type: textarea + id: propose + attributes: + label: Enhancement propose + description: Propose of the enhancement + placeholder: | + Show your idea here + validations: + required: true + - type: textarea + id: solution + attributes: + label: Solution + description: What's your solution for this enhancement + placeholder: | + How to do it on your opinion + - type: textarea + id: addition + attributes: + label: Additional info + description: Everything else you consider worthy that we didn't ask for \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/mbl.yml b/.github/ISSUE_TEMPLATE/mbl.yml deleted file mode 100644 index e6cd1f32..00000000 --- a/.github/ISSUE_TEMPLATE/mbl.yml +++ /dev/null @@ -1,22 +0,0 @@ -name: Module Blacklist Removal -description: Issue template to request a blacklisted module removal -title: '[MBL]: ' -body: - - type: input - id: moduleid - attributes: - label: Module ID - placeholder: mmrl_install_tools - validations: - required: true - - type: textarea - id: reason - attributes: - label: Reason why it should be removed - validations: - required: true - - type: markdown - attributes: - value: >- - This template was generated with [Issue Forms - Creator](https://issue-forms-creator.netlify.app) diff --git a/.github/ISSUE_TEMPLATE/question.md b/.github/ISSUE_TEMPLATE/question.md deleted file mode 100644 index 8ed307f5..00000000 --- a/.github/ISSUE_TEMPLATE/question.md +++ /dev/null @@ -1,8 +0,0 @@ ---- -name: Question -about: Question about this project -title: '' -labels: question -assignees: '' - ---- \ No newline at end of file diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 7e72a704..0a940253 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -1,11 +1,29 @@ -# To get started with Dependabot version updates, you'll need to specify which -# package ecosystems to update and where the package manifests are located. -# Please see the documentation for all configuration options: -# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates - version: 2 updates: - - package-ecosystem: "npm" # See documentation for possible values - directory: "/Website" # Location of package manifests + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "daily" + time: "21:00" + labels: [ "github_actions" ] + + - package-ecosystem: gradle + directory: "/" schedule: - interval: "weekly" + interval: daily + time: "21:00" + labels: [ "dependencies" ] + registries: "*" + groups: + kotlin-ksp: + patterns: + - "org.jetbrains.kotlin:*" + - "org.jetbrains.kotlin.jvm" + - "com.google.devtools.ksp" + - "com.google.devtools.ksp.gradle.plugin" + +registries: + maven-google: + type: "maven-repository" + url: "https://maven.google.com" + replaces-base: true diff --git a/.github/workflows/android.yml b/.github/workflows/android.yml new file mode 100644 index 00000000..073b06ec --- /dev/null +++ b/.github/workflows/android.yml @@ -0,0 +1,63 @@ +name: Android CI + +on: + push: + branches: [ "main" ] + pull_request: + branches: [ "main" ] + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Set up signing key + if: github.ref == 'refs/heads/main' + run: | + if [ ! -z "${{ secrets.KEY_STORE }}" ]; then + echo keyStorePassword='${{ secrets.KEY_STORE_PASSWORD }}' >> signing.properties + echo keyAlias='${{ secrets.KEY_ALIAS }}' >> signing.properties + echo keyPassword='${{ secrets.KEY_PASSWORD }}' >> signing.properties + echo keyStore='${{ github.workspace }}/key.jks' >> signing.properties + echo ${{ secrets.KEY_STORE }} | base64 --decode > ${{ github.workspace }}/key.jks + fi + + - name: Set up JDK + uses: actions/setup-java@v4 + with: + distribution: 'zulu' + java-version: 21 + + - name: Set up Gradle + uses: gradle/actions/setup-gradle@v3 + with: + validate-wrappers: true + gradle-home-cache-cleanup: true + + - name: Build with Gradle + run: ./gradlew assembleRelease + + - name: Get release name + if: success() && github.ref == 'refs/heads/main' + id: release-name + run: | + name=`ls app/build/outputs/apk/release/*.apk | awk -F '(/|.apk)' '{print $6}'` && echo "name=${name}" >> $GITHUB_OUTPUT + + - name: Upload built apk + if: success() && github.ref == 'refs/heads/main' + uses: actions/upload-artifact@v4 + with: + name: ${{ steps.release-name.outputs.name }} + path: app/build/outputs/apk/release/*.apk + + - name: Upload mappings + if: success() && github.ref == 'refs/heads/main' + uses: actions/upload-artifact@v4 + with: + name: mappings + path: app/build/outputs/mapping/release diff --git a/.github/workflows/build-debug.yml b/.github/workflows/build-debug.yml deleted file mode 100644 index 2c377dd0..00000000 --- a/.github/workflows/build-debug.yml +++ /dev/null @@ -1,96 +0,0 @@ -name: Generate APK Debug - -on: - push: - branches: - - '*' - paths-ignore: - - '**.md' - pull_request: - branches: - - '*' - paths-ignore: - - '**.md' - workflow_dispatch: - -jobs: - build: - runs-on: ubuntu-latest - if: ${{ startsWith(github.event.head_commit.message, '[android]') }} - permissions: - contents: read - packages: write - - steps: - - name: Check out repository - uses: actions/checkout@v3 - with: - submodules: true - - - name: Set up Java 17 - uses: actions/setup-java@v3 - with: - java-version: 17 - distribution: "temurin" - - - name: Set up Node.js 16 - uses: actions/setup-node@v3 - with: - node-version: 16 - - - name: Linking Node.js binaries - run: sudo ln `which node` /usr/bin/node && sudo ln `which npm` /usr/bin/npm - - - name: Setup Android SDK - uses: android-actions/setup-android@v2 - - - name: Setup Gradle - uses: gradle/gradle-build-action@v2 - with: - gradle-home-cache-includes: | - caches - notifications - jdks - ${{ github.workspace }}/.gradle/configuration-cache - - - name: Change wrapper permissions - run: chmod +x ./gradlew - - - name: Installing dependencies - run: ./gradlew app:webInstall - - - name: Build apk debug - run: ./gradlew app:assembleDefaultDebug - - - name: Build apk debug - run: ls app/build/outputs/apk/default/debug/ - - - name: Upload MMRL-default-arm64-v8a-debug - uses: actions/upload-artifact@v3 - with: - name: MMRL-default-arm64-v8a-debug - path: ${{ github.workspace }}/app/build/outputs/apk/default/debug/*-default-arm64-v8a-debug-*-*.apk - - - name: Upload MMRL-default-armeabi-v7a-debug - uses: actions/upload-artifact@v3 - with: - name: MMRL-default-armeabi-v7a-debug - path: ${{ github.workspace }}/app/build/outputs/apk/default/debug/*-default-armeabi-v7a-debug-*-*.apk - - - name: Upload MMRL-default-universal-debug - uses: actions/upload-artifact@v3 - with: - name: MMRL-default-universal-debug - path: ${{ github.workspace }}/app/build/outputs/apk/default/debug/*-default-universal-debug-*-*.apk - - - name: Upload MMRL-default-x86-debug - uses: actions/upload-artifact@v3 - with: - name: MMRL-default-x86-debug - path: ${{ github.workspace }}/app/build/outputs/apk/default/debug/*-default-x86-debug-*-*.apk - - - name: Upload MMRL-default-x86_64-debug - uses: actions/upload-artifact@v3 - with: - name: MMRL-default-x86_64-debug - path: ${{ github.workspace }}/app/build/outputs/apk/default/debug/*-default-x86_64-debug-*-*.apk diff --git a/.github/workflows/webpack.yml b/.github/workflows/webpack.yml deleted file mode 100644 index 8416a5ec..00000000 --- a/.github/workflows/webpack.yml +++ /dev/null @@ -1,29 +0,0 @@ -name: Build with Webpack - -on: - push: - branches: [master] - pull_request: - branches: [master] - -jobs: - build: - runs-on: ubuntu-latest - if: ${{ startsWith(github.event.head_commit.message, '[webpack]') }} - - steps: - - uses: actions/checkout@v2 - - - uses: actions/setup-node@v3 - with: - node-version: 18 - - - name: Build Development - run: | - npm install --force - npm run web:dev - - - name: Build Production - run: | - npm install --force - npm run web:prod diff --git a/.gitignore b/.gitignore index 49c464e6..e414d03a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,72 +1,27 @@ -#built application files -*.apk -*.abb -*.aab -*.ap_ - -# files for the dex VM -*.dex - -# Java class files -*.class - -# generated files -bin/ -!.vscode/bin -gen/ -*.cxx -app/.cxx/* -app/src/main/assets/www/* -app/release/**/* -app/default/**/* +# Gradle +.gradle/ +build/ +# Kotlin +.kotlin -# Local configuration file (sdk path, etc) +# Local configuration local.properties - -# Windows thumbnail db -Thumbs.db - -# OSX files -.DS_Store +signing.properties # Android Studio -*.iml -.idea -#.idea/workspace.xml - remove # and delete .idea if it better suit your needs. -.gradle -build/ -.navigation captures/ -output.json - -#NDK -obj/ -.externalNativeBuild +release/ +.externalNativeBuild/ +.cxx/ +# IntelliJ +*.iml +.idea/ -# Website -.cache/ -coverage/ -dist/* -!dist/index.html -!dist/moduleOptions.json -!vscode/settings.json -node_modules/ -cdn_modules/ -*.log - -# -- -node_modules/ -cdn_modules/ -package-lock.json -node_modules.bun +# Keystore +*.jks +*.keystore -# OS generated files -.DS_Store -.DS_Store? -._* -.Spotlight-V100 -.Trashes -ehthumbs.db -Thumbs.db \ No newline at end of file +# MacOS +.DS_Store \ No newline at end of file diff --git a/.prettierrc b/.prettierrc deleted file mode 100644 index 9f3f0fa2..00000000 --- a/.prettierrc +++ /dev/null @@ -1,11 +0,0 @@ -{ - "printWidth": 140, - "tabWidth": 2, - "useTabs": false, - "semi": true, - "singleQuote": false, - "trailingComma": "es5", - "bracketSpacing": true, - "jsxBracketSameLine": false, - "fluid": false - } \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json deleted file mode 100644 index 55428b7a..00000000 --- a/.vscode/settings.json +++ /dev/null @@ -1,28 +0,0 @@ -{ - "liveServer.settings.root": "./app/src/main/assets", - "files.exclude": { - "**/.gradle": true, - "**/.idea": true, - "app/.cxx": true, - "app/build": true, - "app/proguard-rules.pro": true, - "app/release": true, - "build": true, - "gradle": true, - "gradlew": true, - "gradlew.bat": true, - "node_modules": true, - "package-lock.json": true - }, - "editor.defaultFormatter": "esbenp.prettier-vscode", - "[python]": { - "editor.defaultFormatter": "ms-python.python" - }, - // "terminal.integrated.cwd": "Website" - "terminal.integrated.profiles.linux": { - "WSL": { - "path": "wsl.exe" - } - }, - "terminal.integrated.defaultProfile.linux": "WSL" -} diff --git a/LICENSE b/LICENSE index 588d74c6..f288702d 100644 --- a/LICENSE +++ b/LICENSE @@ -1,21 +1,674 @@ -MIT License - -Copyright (c) 2022 Jimmy Böhm (Der_Googler) - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. \ No newline at end of file + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + 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. + + + Copyright (C) + + 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 . + +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: + + Copyright (C) + 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 +. + + 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 +. diff --git a/README.md b/README.md new file mode 100644 index 00000000..d9c23395 --- /dev/null +++ b/README.md @@ -0,0 +1,17 @@ +This repository stores a fork of [MRepoApp/MRepo](https://github.com/MRepoApp/MRepo) and will force pushed into [DerGoogler/MMRL](https://github.com/DerGoogler/MMRL ) when it is finshed. + +Please not that this build doesn't have the right versioning nor the version changes. It's a WIP + +> Translate PR's will be ignored and closed + +Supported repos: + +- ``` + apt.izzysoft.de/magisk + ``` +- ``` + gr.dergoogler.com/gmr + ``` +- ``` + magisk-modules-alt-repo.github.io/json-v2 + ``` \ No newline at end of file diff --git a/app/build.gradle b/app/build.gradle deleted file mode 100644 index a8584cff..00000000 --- a/app/build.gradle +++ /dev/null @@ -1,169 +0,0 @@ -import groovy.json.* - -apply plugin: 'com.android.application' -apply plugin: 'com.github.node-gradle.node' - -def inputFile = new File("./package.json") -def pkg = new JsonSlurper().parseText(inputFile.text) -def app_name = "MMRL" -def cur_time = System.currentTimeMillis() - - -android { - buildFeatures { - buildConfig = true - } - - compileSdk pkg.config.target_sdk - namespace pkg.config.application_id - - defaultConfig { - applicationId pkg.config.application_id - minSdk pkg.config.min_sdk - targetSdk pkg.config.target_sdk - versionName pkg.config.version_name - versionCode pkg.config.version_code - } - - splits { - abi { - enable true - reset() - include "armeabi-v7a", "x86", "arm64-v8a", "x86_64" - universalApk true - } - } - - applicationVariants.configureEach { variant -> - variant.outputs.configureEach { output -> - outputFileName = outputFileName.replace(".apk", "-${variant.versionName}-${variant.versionCode}.apk").replace("-unsigned", "") - } - } - - buildTypes { - release { - resValue "string", "app_name", "${app_name}" - minifyEnabled true - proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' - multiDexEnabled true - buildConfigField "long", "BUILD_DATE", "${cur_time}" - buildConfigField "String", "BUILD_TYPE", "\"production\"" - } - debug { - resValue "string", "app_name", "${app_name} Debug" - jniDebuggable true - renderscriptDebuggable true - minifyEnabled false - multiDexEnabled false - applicationIdSuffix '.debug' - buildConfigField "long", "BUILD_DATE", "${cur_time}" - buildConfigField "String", "BUILD_TYPE", "\"development\"" - } - } - compileOptions { - sourceCompatibility JavaVersion.VERSION_14 - targetCompatibility JavaVersion.VERSION_14 - } - packagingOptions { - resources { - excludes += ['META-INF/DEPENDENCIES.txt', 'META-INF/DEPENDENCIES', 'META-INF/LICENSE', 'META-INF/LICENSE.txt', 'META-INF/MANIFEST.MF', 'META-INF/NOTICE', 'META-INF/NOTICE.txt', 'META-INF/ASL2.0'] - } - } - flavorDimensions 'type' - productFlavors { - 'default' { - dimension 'type' - } - } -} - -repositories { - maven { url 'https://jitpack.io' } - mavenCentral() -} - -dependencies { - implementation 'androidx.browser:browser:1.8.0' - implementation "androidx.annotation:annotation:1.8.2" - implementation "androidx.appcompat:appcompat:1.7.0" - implementation 'androidx.core:core:1.13.1' - implementation 'androidx.webkit:webkit:1.11.0' - implementation 'androidx.profileinstaller:profileinstaller:1.3.1' - implementation 'androidx.room:room-runtime:2.6.1' - - implementation "com.github.topjohnwu.libsu:core:5.2.1" - implementation "com.github.topjohnwu.libsu:io:5.2.1" - - implementation 'org.apache.cordova:framework:12.0.1' - - implementation 'com.squareup.okhttp3:okhttp:4.12.0' -} - -configurations.implementation.setCanBeResolved(true) - -node { - download = true - version = "18.19.0" - npmVersion = "10.2.3" -} - -tasks.register("webInstall", NpmTask) { - args = ["install", "-f"] -} - -tasks.register("webDev", NpmTask) { - args = ["run", "web:dev"] -} - -tasks.register("webProd", NpmTask) { - args = ["run", "web:prod"] -} - -afterEvaluate { - tasks.named('assembleDefaultDebug').configure { -// dependsOn webProd - } - tasks.named('assembleDefaultRelease').configure { -// dependsOn webProd - dependsOn(["printSolvedDepsTreeInJson"]) - } -} - -tasks.register('printDepsTreeInJson') { - doLast { - configurations.implementation.incoming.getResolutionResult().getAllDependencies().each { depResult -> - println "{\"from\":\"" + depResult.getFrom() + "\"," + "\"requested\":\"" + depResult.getRequested() + "\"}" - } - } -} - -tasks.register('printSolvedDepsTreeInJson') { - doLast { - def jsonOutput = "[" - configurations.implementation.resolvedConfiguration.firstLevelModuleDependencies.each { dep -> - def addToJson - addToJson = { resolvedDep -> - jsonOutput += "\n{" - jsonOutput += "\"name\":\"${resolvedDep.module.id.group}:${resolvedDep.module.id.name}\"," - jsonOutput += "\"description\":\"${resolvedDep.module.id}\"," - jsonOutput += "\"version\":\"${resolvedDep.module.id.version}\"," - jsonOutput += "\"license\":\"null\"," - jsonOutput += "\"author\":\"null\"," - jsonOutput += "\"repository\":\"https://mvnrepository.com/artifact/${resolvedDep.module.id.group}/${resolvedDep.module.id.name}/${resolvedDep.module.id.version}\"" - jsonOutput += "}," - } - addToJson(dep) - } - if (jsonOutput[-1] == ',') { - jsonOutput = jsonOutput[0..-2] - } - jsonOutput += "]" - - // This took me more than two hours to make -_- - def jsonFile = new JsonSlurper().parseText(jsonOutput) - def json = JsonOutput.toJson(jsonFile) - def pretty = JsonOutput.prettyPrint(json) - def myFile = new File('./src/util/native-licenses.json') - myFile.write(pretty) - } -} diff --git a/app/build.gradle.kts b/app/build.gradle.kts new file mode 100644 index 00000000..d93b5200 --- /dev/null +++ b/app/build.gradle.kts @@ -0,0 +1,169 @@ +import com.android.build.gradle.internal.api.ApkVariantOutputImpl + +plugins { + alias(libs.plugins.self.application) + alias(libs.plugins.self.compose) + alias(libs.plugins.self.hilt) + alias(libs.plugins.self.room) + alias(libs.plugins.kotlin.parcelize) + alias(libs.plugins.ksp) + alias(libs.plugins.protobuf) +} + +val baseVersionName = "4.24.32" + +android { + namespace = "com.dergoogler.mmrl" + + defaultConfig { + applicationId = namespace + versionName = baseVersionName + versionCode = commitCount + 32432 + + resourceConfigurations += arrayOf( + "en", + "ar", + "de", + "es", + "fr", + "hi", + "in", + "it", + "ja", + "pl", + "pt", + "ro", + "ru", + "tr", + "vi", + "zh-rCN", + "zh-rTW" + ) + } + + val releaseSigning = if (project.hasReleaseKeyStore) { + signingConfigs.create("release") { + storeFile = project.releaseKeyStore + storePassword = project.releaseKeyStorePassword + keyAlias = project.releaseKeyAlias + keyPassword = project.releaseKeyPassword + enableV2Signing = true + enableV3Signing = true + } + } else { + signingConfigs.getByName("debug") + } + + buildTypes { + release { + isMinifyEnabled = true + isShrinkResources = true + proguardFiles( + getDefaultProguardFile("proguard-android-optimize.txt"), + "proguard-rules.pro" + ) + buildConfigField("Boolean", "IS_DEV_VERSION", "false") + isDebuggable = false + isJniDebuggable = false + isRenderscriptDebuggable = false + renderscriptOptimLevel = 3 + multiDexEnabled = false + } + + debug { + buildConfigField("Boolean", "IS_DEV_VERSION", "true") + applicationIdSuffix = ".debug" + isJniDebuggable = true + isDebuggable = true + isRenderscriptDebuggable = true + renderscriptOptimLevel = 0 + isMinifyEnabled = false + multiDexEnabled = false + } + + all { + signingConfig = releaseSigning + } + } + + buildFeatures { + buildConfig = true + } + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_21 + targetCompatibility = JavaVersion.VERSION_21 + } + + packaging.resources.excludes += setOf( + "META-INF/**", + "okhttp3/**", + //"kotlin/**", + "org/**", + "**.properties", + "**.bin", + "**/*.proto" + ) + + dependenciesInfo.includeInApk = false + + applicationVariants.configureEach { + outputs.configureEach { + (this as? ApkVariantOutputImpl)?.outputFileName = + "MMRL-${versionName}-${versionCode}-${name}.apk" + } + } +} + +protobuf { + protoc { + artifact = libs.protobuf.protoc.get().toString() + } + + generateProtoTasks { + all().forEach { task -> + task.builtins { + register("java") { + option("lite") + } + } + } + } +} + +dependencies { + compileOnly(projects.hiddenApi) + implementation(projects.compat) + + implementation(libs.androidx.activity.compose) + implementation(libs.androidx.appcompat) + implementation(libs.androidx.compose.ui.util) + implementation(libs.androidx.core.ktx) + implementation(libs.androidx.core.splashscreen) + implementation(libs.androidx.datastore.core) + implementation(libs.androidx.documentfile) + implementation(libs.androidx.hilt.navigation.compose) + implementation(libs.androidx.lifecycle.livedata.ktx) + implementation(libs.androidx.lifecycle.runtime.compose) + implementation(libs.androidx.lifecycle.service) + implementation(libs.androidx.lifecycle.viewModel.compose) + implementation(libs.androidx.navigation.compose) + implementation(libs.kotlinx.coroutines.android) + implementation(libs.kotlinx.datetime) + implementation(libs.kotlin.reflect) + implementation(libs.protobuf.kotlin.lite) + implementation(libs.markwon.core) + implementation(libs.timber) + + implementation(libs.semver) + implementation(libs.coil.compose) + + + implementation(libs.square.retrofit) + implementation(libs.square.retrofit.moshi) + implementation(libs.square.okhttp) + implementation(libs.square.okhttp.dnsoverhttps) + implementation(libs.square.logging.interceptor) + implementation(libs.square.moshi) + ksp(libs.square.moshi.kotlin) +} \ No newline at end of file diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro index 267df1c9..abe16093 100644 --- a/app/proguard-rules.pro +++ b/app/proguard-rules.pro @@ -1,24 +1,14 @@ -# Add project specific ProGuard rules here. -# By default, the flags in this file are appended to flags specified -# in /Applications/Utilities/sdk/tools/proguard/proguard-android.txt -# You can edit the include path and order by changing the proguardFiles -# directive in build.gradle. -# -# For more details, see -# http://developer.android.com/guide/developing/tools/proguard.html +-verbose +-dontpreverify +-optimizationpasses 5 +-dontskipnonpubliclibraryclasses -# Add any project specific keep options here: +-dontwarn org.conscrypt.** +-dontwarn kotlinx.serialization.** -# If your project uses WebView with JS, uncomment the following -# and specify the fully qualified class name to the JavaScript interface -# class: -#-keepclassmembers class fqcn.of.javascript.interface.for.webview { -# public *; -#} +# Keep DataStore fields +-keepclassmembers class * extends com.google.protobuf.GeneratedMessageLite* { + ; +} -# These clases contain references to cordova webView --keep class org.apache.cordova.** {*; } --keep class org.apache.cordova.* - --keep class org.apache.cordova.** { *; } --keep public class * extends org.apache.cordova.CordovaPlugin \ No newline at end of file +-repackageclasses com.dergoogler.mmrl \ No newline at end of file diff --git a/app/schemas/com.dergoogler.mmrl.database.AppDatabase/1.json b/app/schemas/com.dergoogler.mmrl.database.AppDatabase/1.json new file mode 100644 index 00000000..2353d867 --- /dev/null +++ b/app/schemas/com.dergoogler.mmrl.database.AppDatabase/1.json @@ -0,0 +1,489 @@ +{ + "formatVersion": 1, + "database": { + "version": 1, + "identityHash": "48e744a8188bf09dc610ffe40bb08b4a", + "entities": [ + { + "tableName": "repos", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`url` TEXT NOT NULL, `name` TEXT NOT NULL, `enable` INTEGER NOT NULL, `submission` TEXT, `website` TEXT, `donate` TEXT, `support` TEXT, `version` INTEGER NOT NULL, `timestamp` REAL NOT NULL, `size` INTEGER NOT NULL, PRIMARY KEY(`url`))", + "fields": [ + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "enable", + "columnName": "enable", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "submission", + "columnName": "submission", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "website", + "columnName": "website", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "donate", + "columnName": "donate", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "support", + "columnName": "support", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "metadata.version", + "columnName": "version", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "metadata.timestamp", + "columnName": "timestamp", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "metadata.size", + "columnName": "size", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "url" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "localModules_updatable", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `updatable` INTEGER NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "updatable", + "columnName": "updatable", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "onlineModules", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `repoUrl` TEXT NOT NULL, `name` TEXT NOT NULL, `version` TEXT NOT NULL, `versionCode` INTEGER NOT NULL, `author` TEXT NOT NULL, `description` TEXT NOT NULL, `maxApi` INTEGER, `minApi` INTEGER, `size` INTEGER, `categories` TEXT, `icon` TEXT, `homepage` TEXT, `donate` TEXT, `support` TEXT, `cover` TEXT, `screenshots` TEXT, `license` TEXT, `readme` TEXT, `require` TEXT, `verified` INTEGER, `magisk` TEXT, `kernelsu` TEXT, `apatch` TEXT, `title` TEXT, `message` TEXT, `service` INTEGER, `postFsData` INTEGER, `resetprop` INTEGER, `sepolicy` INTEGER, `zygisk` INTEGER, `apks` INTEGER, `webroot` INTEGER, `postMount` INTEGER, `bootCompleted` INTEGER, `type` TEXT NOT NULL, `added` REAL, `source` TEXT NOT NULL, `antifeatures` TEXT, PRIMARY KEY(`id`, `repoUrl`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "repoUrl", + "columnName": "repoUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "version", + "columnName": "version", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "versionCode", + "columnName": "versionCode", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "author", + "columnName": "author", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "maxApi", + "columnName": "maxApi", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "minApi", + "columnName": "minApi", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "size", + "columnName": "size", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "categories", + "columnName": "categories", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "icon", + "columnName": "icon", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "homepage", + "columnName": "homepage", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "donate", + "columnName": "donate", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "support", + "columnName": "support", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "cover", + "columnName": "cover", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "screenshots", + "columnName": "screenshots", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "license", + "columnName": "license", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "readme", + "columnName": "readme", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "require", + "columnName": "require", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "verified", + "columnName": "verified", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "root.magisk", + "columnName": "magisk", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "root.kernelsu", + "columnName": "kernelsu", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "root.apatch", + "columnName": "apatch", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "note.title", + "columnName": "title", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "note.message", + "columnName": "message", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "features.service", + "columnName": "service", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "features.postFsData", + "columnName": "postFsData", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "features.resetprop", + "columnName": "resetprop", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "features.sepolicy", + "columnName": "sepolicy", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "features.zygisk", + "columnName": "zygisk", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "features.apks", + "columnName": "apks", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "features.webroot", + "columnName": "webroot", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "features.postMount", + "columnName": "postMount", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "features.bootCompleted", + "columnName": "bootCompleted", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "track.type", + "columnName": "type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "track.added", + "columnName": "added", + "affinity": "REAL", + "notNull": false + }, + { + "fieldPath": "track.source", + "columnName": "source", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "track.antifeatures", + "columnName": "antifeatures", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id", + "repoUrl" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "versions", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `repoUrl` TEXT NOT NULL, `timestamp` REAL NOT NULL, `version` TEXT NOT NULL, `versionCode` INTEGER NOT NULL, `zipUrl` TEXT NOT NULL, `changelog` TEXT NOT NULL, PRIMARY KEY(`id`, `repoUrl`, `versionCode`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "repoUrl", + "columnName": "repoUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "timestamp", + "columnName": "timestamp", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "version", + "columnName": "version", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "versionCode", + "columnName": "versionCode", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "zipUrl", + "columnName": "zipUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "changelog", + "columnName": "changelog", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id", + "repoUrl", + "versionCode" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "localModules", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `name` TEXT NOT NULL, `version` TEXT NOT NULL, `versionCode` INTEGER NOT NULL, `author` TEXT NOT NULL, `description` TEXT NOT NULL, `state` TEXT NOT NULL, `updateJson` TEXT NOT NULL, `lastUpdated` INTEGER NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "version", + "columnName": "version", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "versionCode", + "columnName": "versionCode", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "author", + "columnName": "author", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "state", + "columnName": "state", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "updateJson", + "columnName": "updateJson", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastUpdated", + "columnName": "lastUpdated", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + } + ], + "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, '48e744a8188bf09dc610ffe40bb08b4a')" + ] + } +} \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 515f5158..8556bed0 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -1,12 +1,16 @@ + xmlns:tools="http://schemas.android.com/tools"> - - + + + + + - - - - - - - + android:supportsRtl="true" + tools:targetApi="34"> + + android:launchMode="singleTask" + android:windowSoftInputMode="adjustResize"> + + - - - - - - - - - - - - - - + + - + + - - - - - - - - - - - - - - + android:name=".service.DownloadService" + android:foregroundServiceType="dataSync" + android:exported="false" /> + - + \ No newline at end of file diff --git a/app/src/main/ic_launcher-playstore.png b/app/src/main/ic_launcher-playstore.png deleted file mode 100644 index 074aab12..00000000 Binary files a/app/src/main/ic_launcher-playstore.png and /dev/null differ diff --git a/app/src/main/java/com/dergoogler/core/NativeBuildConfig.java b/app/src/main/java/com/dergoogler/core/NativeBuildConfig.java deleted file mode 100644 index 954601d9..00000000 --- a/app/src/main/java/com/dergoogler/core/NativeBuildConfig.java +++ /dev/null @@ -1,38 +0,0 @@ -package com.dergoogler.core; - -import android.os.Build; -import android.webkit.JavascriptInterface; - -import com.dergoogler.mmrl.BuildConfig; - -public class NativeBuildConfig { - @JavascriptInterface - public long BUILD_DATE() { - return BuildConfig.BUILD_DATE; - } - - @JavascriptInterface - public int VERSION_CODE() { - return BuildConfig.VERSION_CODE; - } - - @JavascriptInterface - public String VERSION_NAME() { - return BuildConfig.VERSION_NAME; - } - - @JavascriptInterface - public String APPLICATION_ID() { - return BuildConfig.APPLICATION_ID; - } - - @JavascriptInterface - public boolean DEBUG() { - return BuildConfig.DEBUG; - } - - @JavascriptInterface - public String BUILD_TYPE() { - return BuildConfig.BUILD_TYPE; - } -} \ No newline at end of file diff --git a/app/src/main/java/com/dergoogler/core/NativeEnvironment.java b/app/src/main/java/com/dergoogler/core/NativeEnvironment.java deleted file mode 100644 index 54a0aec4..00000000 --- a/app/src/main/java/com/dergoogler/core/NativeEnvironment.java +++ /dev/null @@ -1,36 +0,0 @@ -package com.dergoogler.core; - -import android.content.Context; -import android.content.ContextWrapper; -import android.os.Environment; -import android.webkit.JavascriptInterface; - -public class NativeEnvironment { - - private final Context ctx; - - public NativeEnvironment(Context ctx) { - this.ctx = ctx; - } - - @JavascriptInterface - public String getExternalStorageDir() { - return Environment.getExternalStorageDirectory().getAbsolutePath(); - } - - @JavascriptInterface - public String getPackageDataDir() { - return this.ctx.getExternalFilesDir(null).getAbsolutePath(); - } - - @JavascriptInterface - public String getPublicDir(String type) { - return Environment.getExternalStoragePublicDirectory(type).getAbsolutePath(); - } - - @JavascriptInterface - public String getDataDir() { - return new ContextWrapper(this.ctx).getFilesDir().getPath(); - } - -} diff --git a/app/src/main/java/com/dergoogler/core/NativeLog.java b/app/src/main/java/com/dergoogler/core/NativeLog.java deleted file mode 100644 index 66528c83..00000000 --- a/app/src/main/java/com/dergoogler/core/NativeLog.java +++ /dev/null @@ -1,12 +0,0 @@ -package com.dergoogler.core; - -import android.util.Log; -import android.webkit.JavascriptInterface; - -public class NativeLog { - @JavascriptInterface - public void native_log(int prio, String tag, String msg) { - Log.println(prio, tag, msg); - } - -} diff --git a/app/src/main/java/com/dergoogler/core/NativeOS.java b/app/src/main/java/com/dergoogler/core/NativeOS.java deleted file mode 100644 index 631ffb28..00000000 --- a/app/src/main/java/com/dergoogler/core/NativeOS.java +++ /dev/null @@ -1,181 +0,0 @@ -package com.dergoogler.core; - -import android.Manifest; -import android.annotation.SuppressLint; -import android.app.Activity; -import android.content.ActivityNotFoundException; -import android.content.Context; -import android.content.Intent; -import android.content.pm.PackageManager; -import android.content.res.Resources; -import android.graphics.Color; -import android.net.Uri; -import android.os.Build; -import android.os.Environment; -import android.provider.Settings; -import android.util.Log; -import android.view.DisplayCutout; -import android.view.View; -import android.view.WindowInsets; -import android.webkit.JavascriptInterface; -import android.widget.Toast; - -import androidx.browser.customtabs.CustomTabColorSchemeParams; -import androidx.browser.customtabs.CustomTabsIntent; -import androidx.core.content.ContextCompat; -import androidx.core.graphics.Insets; - -import com.dergoogler.mmrl.MainActivity; - -public class NativeOS { - private final MainActivity ctx; - - public NativeOS(MainActivity ctx) { - this.ctx = ctx; - } - - - @JavascriptInterface - public void makeToast(String content, int duration) { - Toast.makeText(this.ctx, content, duration).show(); - } - - @JavascriptInterface - public String getSchemeParam(String param) { - Intent intent = ((Activity) this.ctx).getIntent(); - if (Intent.ACTION_VIEW.equals(intent.getAction())) { - Uri uri = intent.getData(); - return uri.getQueryParameter(param); - } else { - return ""; - } - } - - @Deprecated - @JavascriptInterface - public boolean hasStoragePermission() { - return true; - } - - @Deprecated - @JavascriptInterface - public void requestStoargePermission() { - // do nothing - } - - @JavascriptInterface - public void open(String link, String color) { - Uri uriUrl = Uri.parse(link); - CustomTabsIntent.Builder intentBuilder = new CustomTabsIntent.Builder(); - CustomTabColorSchemeParams params = new CustomTabColorSchemeParams.Builder() - .setToolbarColor(Color.parseColor(color)) - .build(); - intentBuilder.setColorSchemeParams(CustomTabsIntent.COLOR_SCHEME_DARK, params); - CustomTabsIntent customTabsIntent = intentBuilder.build(); - - // It's not the best, but it should work - try { - customTabsIntent.launchUrl(this.ctx, uriUrl); - } catch (ActivityNotFoundException e) { - Intent intent = new Intent(Intent.ACTION_VIEW); - intent.setData(Uri.parse(link)); - this.ctx.startActivity(intent); - } - } - - @JavascriptInterface - public void close() { - ((Activity) this.ctx).finishAffinity(); - int pid = android.os.Process.myPid(); - android.os.Process.killProcess(pid); - Intent intent = new Intent(Intent.ACTION_MAIN); - intent.addCategory(Intent.CATEGORY_HOME); - this.ctx.startActivity(intent); - } - - @JavascriptInterface - public boolean isPackageInstalled(String targetPackage) { - PackageManager pm = this.ctx.getPackageManager(); - try { - pm.getPackageInfo(targetPackage, 0); - return true; - } catch (PackageManager.NameNotFoundException e) { - return false; - } - } - - @JavascriptInterface - public void launchAppByPackageName(String targetPackage) { - Intent launchIntent = this.ctx.getPackageManager().getLaunchIntentForPackage(targetPackage); - if (launchIntent != null) { - this.ctx.startActivity(launchIntent);//null pointer check in case package name was not found - } - } - @JavascriptInterface - public int sdk() { - return Build.VERSION.SDK_INT; - } - - private static int manipulateColor(int color, float factor) { - int a = Color.alpha(color); - int r = Math.round(Color.red(color) * (100 + factor) / 100); - int g = Math.round(Color.green(color) * (100 + factor) / 100); - int b = Math.round(Color.blue(color) * (100 + factor) / 100); - return Color.argb(a, - Math.min(r,255), - Math.min(g,255), - Math.min(b,255)); - } - - @JavascriptInterface - public String getMonetColor(String id) { - @SuppressLint("DiscouragedApi") int nameResourceID = this.ctx.getResources().getIdentifier("@android:color/" + id, - "color", this.ctx.getApplicationInfo().packageName); - if (nameResourceID == 0) { - throw new IllegalArgumentException( - "No resource string found with name " + id); - } else { - int color = ContextCompat.getColor(this.ctx, nameResourceID); - int red = Color.red(color); - int blue = Color.blue(color); - int green = Color.green(color); - return String.format("#%02x%02x%02x", red, green, blue); - } - } - - @Deprecated - @JavascriptInterface - public void setStatusBarColor(String color, boolean white) { - if (white) { - try { - ((Activity) this.ctx).getWindow().getDecorView().setSystemUiVisibility(View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR); - } catch (Exception e) { - e.printStackTrace(); - } - } - try { - ((Activity) this.ctx).getWindow().setStatusBarColor(Color.parseColor(color)); - } catch (Exception e) { - e.printStackTrace(); - } - } - - @Deprecated - @JavascriptInterface - public void setNavigationBarColor(String color) { - try { - ((Activity) this.ctx).getWindow().setNavigationBarColor(Color.parseColor(color)); - } catch (Exception e) { - e.printStackTrace(); - } - } - - @JavascriptInterface - public void shareText(String title, String body) { - Intent intent = new Intent(android.content.Intent.ACTION_SEND); - intent.setType("text/plain"); - intent.putExtra(android.content.Intent.EXTRA_SUBJECT, title); - intent.putExtra(android.content.Intent.EXTRA_TEXT, body); - ctx.startActivity(Intent.createChooser(intent, title)); - } -} diff --git a/app/src/main/java/com/dergoogler/core/NativeShell.java b/app/src/main/java/com/dergoogler/core/NativeShell.java deleted file mode 100644 index 367012c6..00000000 --- a/app/src/main/java/com/dergoogler/core/NativeShell.java +++ /dev/null @@ -1,124 +0,0 @@ -package com.dergoogler.core; - -import android.util.Log; -import android.webkit.JavascriptInterface; -import android.webkit.WebView; - -import com.dergoogler.plugin.TerminalPlugin; -import com.dergoogler.util.Json; -import com.topjohnwu.superuser.Shell; -import com.topjohnwu.superuser.ShellUtils; - -import org.json.JSONArray; -import org.json.JSONException; - -import java.io.IOException; -import java.util.ArrayList; -import java.util.List; - - -public class NativeShell { - private static final String TAG = "NativeShell"; - private final WebView wv; - ArrayList output = new ArrayList<>(); - - public NativeShell(WebView wv) { - this.wv = wv; - } - - @JavascriptInterface - public Object v2(String jsonArr) { - String[] cmds; - try { - cmds = Json.getStringArray(new JSONArray(jsonArr)); - } catch (JSONException e) { - Log.e(TAG + ":v2", e.toString()); - return null; - } - Shell.Job shell = Shell.cmd(cmds); - - final Shell.Result[] result = new Shell.Result[1]; - - return new Object() { - - @JavascriptInterface - public void exec() { - result[0] = shell.exec(); - } - - @JavascriptInterface - public String result() { - List out = shell.to(new ArrayList<>(), null).exec().getOut(); - return ShellUtils.isValidOutput(out) ? out.get(out.size() - 1) : ""; - } - - @JavascriptInterface - public boolean isSuccess() { - return result[0].isSuccess(); - } - - @JavascriptInterface - public int getCode(String command) { - return result[0].getCode(); - } - - - }; - } - - @Deprecated - @JavascriptInterface - public void exec(String command) { - Shell.cmd(command).exec(); - } - - @Deprecated - @JavascriptInterface - public String result(String command) { - return ShellUtils.fastCmd(command); - } - - @Deprecated - @JavascriptInterface - public boolean isSuccess(String command) { - return Shell.cmd(command).exec().isSuccess(); - } - - @Deprecated - @JavascriptInterface - public int getCode(String command) { - return Shell.cmd(command).exec().getCode(); - } - - @JavascriptInterface - public boolean isSuAvailable() { - try { - Runtime.getRuntime().exec("su --version"); - return true; - } catch (IOException e) { - // java.io.IOException: Cannot run program "su": error=2, No such file or directory - Log.e(TAG + ":isSuAvailable", e.toString()); - return false; - } - } - - @Deprecated - @JavascriptInterface - public boolean isAppGrantedRoot() { - return Shell.cmd("if grep ' / ' /proc/mounts | grep -q '/dev/root' &> /dev/null; " + - "then echo true; else echo false; fi", "magisk -V", "magisk --path") - .to(output).exec().isSuccess(); - } - - @Deprecated - @JavascriptInterface - public static native int pw_uid(); - - @Deprecated - @JavascriptInterface - public static native int pw_gid(); - - @Deprecated - @JavascriptInterface - public static native String pw_name(); -} diff --git a/app/src/main/java/com/dergoogler/core/NativeStorage.java b/app/src/main/java/com/dergoogler/core/NativeStorage.java deleted file mode 100644 index 1864a4ed..00000000 --- a/app/src/main/java/com/dergoogler/core/NativeStorage.java +++ /dev/null @@ -1,57 +0,0 @@ -package com.dergoogler.core; - -import android.app.Activity; -import android.content.Context; -import android.content.SharedPreferences; -import android.webkit.JavascriptInterface; - -import androidx.annotation.NonNull; - - -public class NativeStorage { - private final SharedPreferences localStorage; - private String localStorageName; - - public NativeStorage(Context ctx) { - this.localStorage = ctx.getSharedPreferences("localstorage_v2", Activity.MODE_PRIVATE); - } - - @JavascriptInterface - public void defineName(String name) { - this.localStorageName = name; - } - - @JavascriptInterface - public String getItem(String key, String def) { - try { - return this.localStorage.getString(key, def); - } catch (Exception e) { - return null; - } - } - - @JavascriptInterface - public String getItem(String key) { - try { - return this.localStorage.getString(key, null); - } catch (Exception e) { - return null; - } - } - - @JavascriptInterface - public void setItem(String key, String value) { - this.localStorage.edit().putString(key, value).apply(); - } - - @JavascriptInterface - public void removeItem(String key) { - this.localStorage.edit().remove(key).apply(); - } - - @JavascriptInterface - public void clear() { - this.localStorage.edit().clear().apply(); - } - -} \ No newline at end of file diff --git a/app/src/main/java/com/dergoogler/core/NativeSuFile.java b/app/src/main/java/com/dergoogler/core/NativeSuFile.java deleted file mode 100644 index df9c8ef4..00000000 --- a/app/src/main/java/com/dergoogler/core/NativeSuFile.java +++ /dev/null @@ -1,310 +0,0 @@ -package com.dergoogler.core; - -import android.content.ContentResolver; -import android.content.Intent; -import android.database.Cursor; -import android.net.Uri; -import android.provider.OpenableColumns; -import android.util.Base64; -import android.util.Base64OutputStream; -import android.util.Log; -import android.webkit.JavascriptInterface; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; - -import com.dergoogler.mmrl.MainActivity; -import com.topjohnwu.superuser.io.SuFile; -import com.topjohnwu.superuser.io.SuFileInputStream; -import com.topjohnwu.superuser.io.SuFileOutputStream; - -import java.io.BufferedReader; -import java.io.ByteArrayOutputStream; -import java.io.Closeable; -import java.io.File; -import java.io.FileNotFoundException; -import java.io.FileOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.InputStreamReader; -import java.io.OutputStream; - -import java.nio.charset.StandardCharsets; -import java.util.Date; - -public class NativeSuFile { - private final MainActivity ctx; - private static final String TAG = "NativeSuFile"; - - public NativeSuFile(MainActivity ctx) { - this.ctx = ctx; - } - - @JavascriptInterface - public Object v2(String path) { - SuFile file = new SuFile(path); - return new Object() { - @JavascriptInterface - public void write(String data) { - try { - OutputStream outputStream = SuFileOutputStream.open(file); - outputStream.write(data.getBytes(StandardCharsets.UTF_8)); - outputStream.flush(); - } catch (IOException e) { - Log.e(TAG + ":write", e.toString()); - } - } - - @JavascriptInterface - public String read(String def) { - try { - try (BufferedReader br = new BufferedReader(new InputStreamReader(SuFileInputStream.open(file)))) { - StringBuilder sb = new StringBuilder(); - String line; - while ((line = br.readLine()) != null) { - sb.append(line); - sb.append('\n'); - } - return sb.toString(); - } - } catch (IOException e) { - Log.e(TAG + ":read", e.toString()); - return def; - } - } - - @JavascriptInterface - public String readAsBase64() { - try { - InputStream is = SuFileInputStream.open(file); - ByteArrayOutputStream baos = new ByteArrayOutputStream(); - Base64OutputStream b64os = new Base64OutputStream(baos, Base64.DEFAULT); - byte[] buffer = new byte[8192]; - int bytesRead; - try { - while ((bytesRead = is.read(buffer)) > -1) { - b64os.write(buffer, 0, bytesRead); - } - return baos.toString(); - } catch (IOException e) { - Log.e(TAG + ":readAsBase64", e.toString()); - return ""; - } finally { - closeQuietly(is); - closeQuietly(b64os); // This also closes baos - } - } catch (FileNotFoundException e) { - Log.e(TAG + ":readAsBase64", e.toString()); - return ""; - } - } - - private void closeQuietly(Closeable closeable) { - try { - closeable.close(); - } catch (IOException e) { - } - } - - @JavascriptInterface - public String list(String delimiter) { - String[] files = file.list(); - if (delimiter == null) { - return String.join(",", files); - } else { - return String.join(delimiter, files); - } - } - - @JavascriptInterface - public long lastModified() { - return file.lastModified(); - } - - @JavascriptInterface - public boolean create(int type) { - return switch (type) { - case 0 -> file.createNewFile(); - case 1 -> file.mkdirs(); - case 2 -> file.mkdir(); - default -> false; - }; - } - - @JavascriptInterface - public boolean delete() { - return file.delete(); - } - - - @JavascriptInterface - public boolean deleteRecursive() { - return file.deleteRecursive(); - } - - @JavascriptInterface - public boolean exists() { - return file.exists(); - } - - @JavascriptInterface - public boolean _can_TypeMethod(int type) { - return switch (type) { - case 0 -> file.canRead(); - case 1 -> file.canWrite(); - case 2 -> file.canExecute(); - default -> false; - }; - } - - @JavascriptInterface - public boolean setExecuteWriteReadable(int type, boolean state, boolean ownerOnly) { - return switch (type) { - case 0 -> file.setReadable(state, ownerOnly); - case 1 -> file.setWritable(state, ownerOnly); - case 2 -> file.setExecutable(state, ownerOnly); - default -> false; - }; - } - - @JavascriptInterface - public boolean _is_TypeMethod(int type) { - return switch (type) { - case 0 -> file.isFile(); - case 1 -> file.isSymlink(); - case 2 -> file.isDirectory(); - case 3 -> file.isBlock(); - case 4 -> file.isCharacter(); - case 5 -> file.isNamedPipe(); - case 6 -> file.isSocket(); - case 7 -> file.isHidden(); - default -> false; - }; - } - - @JavascriptInterface - public boolean createNewSym_link(int type, String existing) { - return switch (type) { - case 0 -> file.createNewLink(existing); - case 1 -> file.createNewSymlink(existing); - default -> false; - }; - } - - @JavascriptInterface - public int hasCode() { - return file.hashCode(); - } - }; - } - - @JavascriptInterface - public String readFile(String path) { - try { - try (BufferedReader br = new BufferedReader(new InputStreamReader(SuFileInputStream.open(path)))) { - StringBuilder sb = new StringBuilder(); - String line; - while ((line = br.readLine()) != null) { - sb.append(line); - sb.append('\n'); - } - return sb.toString(); - } - } catch (IOException e) { - e.printStackTrace(); - return ""; - } - } - - @JavascriptInterface - public String listFiles(String path) { - String[] modules = new SuFile(path).list(); - return String.join(",", modules); - } - - @JavascriptInterface - public boolean createFile(String path) { - return new SuFile(path).createNewFile(); - } - - @JavascriptInterface - public boolean deleteFile(String path) { - return new SuFile(path).delete(); - } - - @JavascriptInterface - public void deleteRecursive(String path) { - new SuFile(path).deleteRecursive(); - } - - @JavascriptInterface - public boolean existFile(String path) { - return new SuFile(path).exists(); - } - - @JavascriptInterface - public @Nullable String getSharedFile() { - Intent intent = ctx.getIntent(); - Uri uri = intent.getData(); - if (uri != null) { - return createCopyAndReturnRealPath(uri); - } else { - return null; - } - } - - public @Nullable String createCopyAndReturnRealPath(Uri uri) { - final ContentResolver contentResolver = ctx.getContentResolver(); - if (contentResolver == null) - return null; - - // Create file path inside app's data dir - String filePath = ctx.getApplicationInfo().dataDir + File.separator + "cache" + File.separator - + getFileName(uri); - - File file = new File(filePath); - try { - InputStream inputStream = contentResolver.openInputStream(uri); - if (inputStream == null) - return null; - - OutputStream outputStream = new FileOutputStream(file); - byte[] buf = new byte[1024]; - int len; - while ((len = inputStream.read(buf)) > 0) - outputStream.write(buf, 0, len); - - outputStream.close(); - inputStream.close(); - } catch (IOException ignore) { - return null; - } - - return file.getAbsolutePath(); - } - - public String getFileName(Uri uri) { - String result = null; - if ("content".equals(uri.getScheme())) { - try (Cursor cursor = ctx.getContentResolver().query(uri, null, null, null, null)) { - if (cursor != null && cursor.moveToFirst()) { - int index = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME); - if (index != -1) { - result = cursor.getString(index); - } - } - } - } - if (result == null) { - result = uri.getPath(); - if (result != null) { - int cut = result.lastIndexOf('/'); - if (cut != -1) { - result = result.substring(cut + 1); - } - } - } - return result; - } - -} diff --git a/app/src/main/java/com/dergoogler/core/NativeSuZip.java b/app/src/main/java/com/dergoogler/core/NativeSuZip.java deleted file mode 100644 index 1304c40f..00000000 --- a/app/src/main/java/com/dergoogler/core/NativeSuZip.java +++ /dev/null @@ -1,103 +0,0 @@ -package com.dergoogler.core; - -import android.util.Log; -import android.webkit.JavascriptInterface; - -import com.topjohnwu.superuser.io.SuFile; -import com.topjohnwu.superuser.io.SuFileInputStream; -import com.topjohnwu.superuser.io.SuFileOutputStream; - -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.nio.charset.StandardCharsets; -import java.nio.file.Files; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.zip.ZipEntry; -import java.util.zip.ZipInputStream; - -public class NativeSuZip { - private final String TAG = "NativeSuZip"; - - public NativeSuZip() { - } - - private String parseSlashes(String path) { - if (!path.endsWith("/")) { - path = path.replaceAll("/$", ""); - } - if (!path.startsWith("/")) { - path = "/" + path; - } - return path; - } - - @JavascriptInterface - public Object newFS(String zipPath) { - Map zipContent = new HashMap<>(); - Set directories = new HashSet<>(); - - try { - ZipInputStream zipInputStream = new ZipInputStream(SuFileInputStream.open(new SuFile(zipPath))); - ZipEntry entry; - while ((entry = zipInputStream.getNextEntry()) != null) { - if (entry.isDirectory()) { - directories.add(parseSlashes(entry.getName())); - } else { - ByteArrayOutputStream baos = new ByteArrayOutputStream(); - byte[] buffer = new byte[1024]; - int len; - while ((len = zipInputStream.read(buffer)) > -1) { - baos.write(buffer, 0, len); - - } - zipContent.put(parseSlashes(entry.getName()), baos.toByteArray()); - } - } - } catch (IOException e) { - e.printStackTrace(); - Log.e(TAG, "newFS: " + e); - } - - List files = new ArrayList<>(zipContent.keySet()); - System.out.println("Files in ZIP:"); - files.forEach(System.out::println); - - - return new Object() { - @JavascriptInterface - public String list() { - return String.join(",", new ArrayList<>(zipContent.keySet())); - } - - @JavascriptInterface - public String read(String path) { - byte[] content = zipContent.get(parseSlashes(path)); - if (content == null) { - Log.e(TAG, "File not found: " + path); - return ""; - } - return new String(content, StandardCharsets.UTF_8); - } - - @JavascriptInterface - public boolean exists(String path) { - return zipContent.containsKey(parseSlashes(path)) || directories.contains(parseSlashes(path)); - } - - @JavascriptInterface - public boolean isDirectory(String path) { - return directories.contains(parseSlashes(path)); - } - - @JavascriptInterface - public boolean isFile(String path) { - return zipContent.containsKey(parseSlashes(path)); - } - }; - } -} diff --git a/app/src/main/java/com/dergoogler/core/NativeView.java b/app/src/main/java/com/dergoogler/core/NativeView.java deleted file mode 100644 index 7743147a..00000000 --- a/app/src/main/java/com/dergoogler/core/NativeView.java +++ /dev/null @@ -1,150 +0,0 @@ -package com.dergoogler.core; - -import android.annotation.SuppressLint; -import android.app.Activity; -import android.content.res.Resources; -import android.graphics.Color; -import android.os.Build; -import android.util.DisplayMetrics; -import android.util.Log; -import android.util.TypedValue; -import android.view.DisplayCutout; -import android.view.View; -import android.view.ViewGroup; -import android.view.WindowInsets; -import android.view.WindowManager; -import android.webkit.JavascriptInterface; -import android.webkit.WebView; - -import androidx.core.graphics.Insets; -import androidx.core.view.ViewCompat; -import androidx.core.view.WindowCompat; -import androidx.core.view.WindowInsetsCompat; -import androidx.core.view.WindowInsetsControllerCompat; - -import com.dergoogler.mmrl.MainActivity; -import com.dergoogler.util.Json; -import com.topjohnwu.superuser.Shell; - -import org.json.JSONArray; -import org.json.JSONException; - -import java.util.ArrayList; - -public class NativeView { - private static final String TAG = "NativeView"; - private final Activity ctx; - private Insets insets; - private final WindowInsetsControllerCompat windowInsetsController; - - public NativeView(Activity ctx, WebView wv) { - this.ctx = ctx; - ViewCompat.setOnApplyWindowInsetsListener(wv, (v, windowInsets) -> { - this.insets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars()); - return WindowInsetsCompat.CONSUMED; - }); - this.windowInsetsController = WindowCompat.getInsetsController(ctx.getWindow(), wv); - WindowCompat.setDecorFitsSystemWindows(ctx.getWindow(), false); - this.windowInsetsController.setSystemBarsBehavior(WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE); - - } - - private int dpToPx(int dp) { - float scale = this.ctx.getResources().getDisplayMetrics().density; - return (int) (dp / scale); - } - - @JavascriptInterface - public int getWindowTopInsets() { - return dpToPx(this.insets.top); - } - - @JavascriptInterface - public int getWindowRightInsets() { - return dpToPx(this.insets.right); - } - - @JavascriptInterface - public int getWindowBottomInsets() { - return dpToPx(this.insets.bottom); - } - - @JavascriptInterface - public int getWindowLeftInsets() { - return dpToPx(this.insets.left); - } - - @JavascriptInterface - public boolean isAppearanceLightNavigationBars() { - return windowInsetsController.isAppearanceLightNavigationBars(); - } - - @JavascriptInterface - public void setAppearanceLightNavigationBars(boolean isLight) { - windowInsetsController.setAppearanceLightNavigationBars(isLight); - } - - @JavascriptInterface - public boolean isAppearanceLightStatusBars() { - return windowInsetsController.isAppearanceLightStatusBars(); - } - - @JavascriptInterface - public void setAppearanceLightStatusBars(boolean isLight) { - windowInsetsController.setAppearanceLightStatusBars(isLight); - } - - private void hideSystemBars(int type) { - windowInsetsController.hide(type); - } - - private void showSystemBars(int type) { - windowInsetsController.show(type); - } - - @JavascriptInterface - public void addFlag(int flag) { - try { - ctx.getWindow().addFlags(flag); - } catch (Exception e) { - Log.e(TAG + ":addFlag", e.toString()); - } - } - - @JavascriptInterface - public void clearFlag(int flag) { - try { - ctx.getWindow().clearFlags(flag); - } catch (Exception e) { - Log.e(TAG + ":clearFlag", e.toString()); - } - } - - - @Deprecated - @JavascriptInterface - public void setStatusBarColor(String color, boolean white) { - if (white) { - try { - ((Activity) this.ctx).getWindow().getDecorView().setSystemUiVisibility(View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR); - } catch (Exception e) { - e.printStackTrace(); - } - } - try { - ((Activity) this.ctx).getWindow().setStatusBarColor(Color.parseColor(color)); - } catch (Exception e) { - e.printStackTrace(); - } - } - - @Deprecated - @JavascriptInterface - public void setNavigationBarColor(String color) { - try { - ((Activity) this.ctx).getWindow().setNavigationBarColor(Color.parseColor(color)); - } catch (Exception e) { - e.printStackTrace(); - } - } -} diff --git a/app/src/main/java/com/dergoogler/mmrl/MainActivity.java b/app/src/main/java/com/dergoogler/mmrl/MainActivity.java deleted file mode 100644 index 853a7825..00000000 --- a/app/src/main/java/com/dergoogler/mmrl/MainActivity.java +++ /dev/null @@ -1,171 +0,0 @@ -package com.dergoogler.mmrl; - -import android.annotation.SuppressLint; -import android.graphics.Rect; -import android.os.Build; -import android.os.Bundle; -import android.os.StrictMode; -import android.util.Log; -import android.view.View; -import android.webkit.ConsoleMessage; -import android.webkit.WebSettings; -import android.webkit.WebView; -import android.widget.FrameLayout; -import android.widget.LinearLayout; - -import androidx.core.view.WindowCompat; - -import com.dergoogler.core.NativeEnvironment; -import com.dergoogler.core.NativeSuFile; -import com.dergoogler.core.NativeLog; -import com.dergoogler.core.NativeOS; -import com.dergoogler.core.NativeStorage; -import com.dergoogler.core.NativeShell; -import com.dergoogler.core.NativeBuildConfig; -import com.dergoogler.core.NativeView; -import com.dergoogler.core.NativeSuZip; -import com.topjohnwu.superuser.io.SuFile; - -import org.apache.cordova.*; -import org.apache.cordova.engine.SystemWebChromeClient; -import org.apache.cordova.engine.SystemWebViewEngine; - - -public class MainActivity extends CordovaActivity { - - private WebView wv; - private View rootView; - private int previousHeight = 0; - private boolean isKeyboardShowing = false; - - @Override - @SuppressLint("SetJavaScriptEnabled") - public void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - WindowCompat.setDecorFitsSystemWindows(getWindow(), false); - appView = findViewById(R.id.mmrl_view); - super.init(); - - if (isEmulator) { - StrictMode.ThreadPolicy policy = new StrictMode.ThreadPolicy.Builder().permitAll().build(); - StrictMode.setThreadPolicy(policy); - } - - wv = (WebView) appView.getEngine().getView(); - rootView = findViewById(android.R.id.content); - CordovaWebViewEngine wve = appView.getEngine(); - - - rootView.getViewTreeObserver().addOnGlobalLayoutListener(() -> { - Rect r = new Rect(); - rootView.getWindowVisibleDisplayFrame(r); - int screenHeight = rootView.getRootView().getHeight(); - int keypadHeight = screenHeight - r.bottom; - - if (keypadHeight > screenHeight * 0.15) { // 0.15 ratio is perhaps enough to determine keypad height. - if (!isKeyboardShowing) { - isKeyboardShowing = true; - adjustWebViewHeight(keypadHeight); - } - } else { - if (isKeyboardShowing) { - isKeyboardShowing = false; - resetWebViewHeight(); - } - } - }); - - // enable Cordova apps to be started in the background - Bundle extras = getIntent().getExtras(); - if (extras != null && extras.getBoolean("cdvStartInBackground", false)) { - moveTaskToBack(true); - } - - wv.setBackgroundColor(0x101010); - - loadUrl(launchUrl); - - WebSettings webViewSettings = wv.getSettings(); - // Options - webViewSettings.setJavaScriptEnabled(true); - webViewSettings.setAllowFileAccess(true); - webViewSettings.setAllowContentAccess(true); - webViewSettings.setAllowFileAccessFromFileURLs(true); - webViewSettings.setAllowUniversalAccessFromFileURLs(true); - webViewSettings.setDatabaseEnabled(false); - webViewSettings.setUserAgentString(this.mmrlUserAgent()); - webViewSettings.setAllowFileAccessFromFileURLs(false); - webViewSettings.setAllowFileAccess(false); - webViewSettings.setAllowContentAccess(false); - webViewSettings.setSupportZoom(false); - webViewSettings.setGeolocationEnabled(false); - webViewSettings.setDomStorageEnabled(true); - webViewSettings.setCacheMode(WebSettings.LOAD_CACHE_ELSE_NETWORK); - - SuFile disableHardwareAccelerated = new SuFile("/data/adb/mmrl/settings/disableHardwareAccelerated"); - if (disableHardwareAccelerated.exists()) { - wv.setLayerType(View.LAYER_TYPE_SOFTWARE, null); - } else { - wv.setLayerType(View.LAYER_TYPE_HARDWARE, null); - } - - // Core - wv.addJavascriptInterface(new NativeSuFile(this), "__sufile__"); - wv.addJavascriptInterface(new NativeEnvironment(this), "__environment__"); - wv.addJavascriptInterface(new NativeShell(wv), "__shell__"); - wv.addJavascriptInterface(new NativeBuildConfig(), "__buildconfig__"); - wv.addJavascriptInterface(new NativeOS(this), "__os__"); - wv.addJavascriptInterface(new NativeView(this, wv), "__view__"); - wv.addJavascriptInterface(new NativeStorage(this), "__nativeStorage__"); - wv.addJavascriptInterface(new NativeLog(), "__log__"); - wv.addJavascriptInterface(new NativeSuZip(), "__suzip__"); - - wv.setWebChromeClient(new SystemWebChromeClient((SystemWebViewEngine) wve) { - @Override - public boolean onConsoleMessage(ConsoleMessage consoleMessage) { - switch (consoleMessage.messageLevel()) { - case LOG -> Log.i("MMRLWebViewClient", consoleMessage.message()); - case DEBUG -> Log.d("MMRLWebViewClient", consoleMessage.message()); - case WARNING -> Log.w("MMRLWebViewClient", consoleMessage.message()); - case ERROR -> Log.e("MMRLWebViewClient", consoleMessage.message()); - default -> Log.v("MMRLWebViewClient", consoleMessage.message()); - } - return true; - } - }); - } - - private void adjustWebViewHeight(int keypadHeight) { - FrameLayout.LayoutParams params = (FrameLayout.LayoutParams) wv.getLayoutParams(); - params.height = rootView.getHeight() - keypadHeight; - wv.setLayoutParams(params); - } - - private void resetWebViewHeight() { - FrameLayout.LayoutParams params = (FrameLayout.LayoutParams) wv.getLayoutParams(); - params.height = LinearLayout.LayoutParams.MATCH_PARENT; - wv.setLayoutParams(params); - } - - private String mmrlUserAgent() { - return "MMRL/" + BuildConfig.VERSION_NAME + " (Linux; Android " + Build.VERSION.RELEASE + "; " + Build.MODEL + " Build/" + Build.DISPLAY + ")"; - } - - private final boolean isEmulator = (Build.BRAND.startsWith("generic") && Build.DEVICE.startsWith("generic")) - || Build.FINGERPRINT.startsWith("generic") - || Build.FINGERPRINT.startsWith("unknown") - || Build.HARDWARE.contains("goldfish") - || Build.HARDWARE.contains("ranchu") - || Build.MODEL.contains("google_sdk") - || Build.MODEL.contains("Emulator") - || Build.MODEL.contains("Android SDK built for x86") - || Build.MANUFACTURER.contains("Genymotion") - || Build.PRODUCT.contains("sdk_google") - || Build.PRODUCT.contains("google_sdk") - || Build.PRODUCT.contains("sdk") - || Build.PRODUCT.contains("sdk_x86") - || Build.PRODUCT.contains("sdk_gphone64_arm64") - || Build.PRODUCT.contains("vbox86p") - || Build.PRODUCT.contains("emulator") - || Build.PRODUCT.contains("simulator"); -} diff --git a/app/src/main/java/com/dergoogler/plugin/ChooserPlugin.java b/app/src/main/java/com/dergoogler/plugin/ChooserPlugin.java deleted file mode 100644 index f94e0b31..00000000 --- a/app/src/main/java/com/dergoogler/plugin/ChooserPlugin.java +++ /dev/null @@ -1,514 +0,0 @@ -package com.dergoogler.plugin; - -import android.annotation.SuppressLint; -import android.app.Activity; -import android.content.ClipData; -import android.content.ContentResolver; -import android.content.ContentUris; -import android.content.Context; -import android.content.Intent; -import android.database.Cursor; -import android.net.Uri; -import android.os.Build; -import android.os.Environment; -import android.provider.DocumentsContract; -import android.provider.MediaStore; -import android.provider.OpenableColumns; -import android.util.Base64; -import android.util.Log; - -import androidx.loader.content.CursorLoader; - -import com.topjohnwu.superuser.io.SuFile; - -import java.io.ByteArrayOutputStream; -import java.io.File; -import java.io.FileOutputStream; -import java.io.InputStream; -import java.io.IOException; -import java.lang.Exception; -import java.util.List; -import java.util.Objects; - -import org.apache.cordova.CallbackContext; -import org.apache.cordova.CordovaPlugin; -import org.apache.cordova.PluginResult; -import org.json.JSONArray; -import org.json.JSONException; -import org.json.JSONObject; - - -public class ChooserPlugin extends CordovaPlugin { - private static final String ACTION_OPEN = "getFile"; - private static final int PICK_FILE_REQUEST = 1; - private static final String TAG = "Chooser"; - - public static byte[] getBytesFromInputStream(InputStream is) throws IOException { - ByteArrayOutputStream os = new ByteArrayOutputStream(); - byte[] buffer = new byte[0xFFFF]; - - for (int len = is.read(buffer); len != -1; len = is.read(buffer)) { - os.write(buffer, 0, len); - } - - return os.toByteArray(); - } - - public static String getDisplayName(ContentResolver contentResolver, Uri uri) { - String[] projection = {MediaStore.MediaColumns.DISPLAY_NAME}; - Cursor metaCursor = contentResolver.query(uri, projection, null, null, null); - - if (metaCursor != null) { - try { - if (metaCursor.moveToFirst()) { - return metaCursor.getString(0); - } - } finally { - metaCursor.close(); - } - } - - return "File"; - } - - - private CallbackContext callback; - private Boolean includeData; - - public void chooseFile(CallbackContext callbackContext, String accept, Boolean includeData, boolean allowMulti) { - Intent intent = new Intent(Intent.ACTION_GET_CONTENT); - intent.setType("*/*"); - if (!accept.equals("*/*")) { - intent.putExtra(Intent.EXTRA_MIME_TYPES, accept.split(",")); - } - intent.addCategory(Intent.CATEGORY_OPENABLE); - intent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, allowMulti); - intent.putExtra(Intent.EXTRA_LOCAL_ONLY, true); - this.includeData = includeData; - - Intent chooser = Intent.createChooser(intent, "Select File"); - cordova.startActivityForResult(this, chooser, ChooserPlugin.PICK_FILE_REQUEST); - - PluginResult pluginResult = new PluginResult(PluginResult.Status.NO_RESULT); - pluginResult.setKeepCallback(true); - this.callback = callbackContext; - callbackContext.sendPluginResult(pluginResult); - } - - @Override - public boolean execute( - String action, - JSONArray args, - CallbackContext callbackContext - ) { - try { - if (action.equals(ChooserPlugin.ACTION_OPEN)) { - this.chooseFile(callbackContext, args.getString(0), args.getBoolean(1),args.getBoolean(2)); - return true; - } - } catch (JSONException err) { - this.callback.error("Execute failed: " + err.toString()); - } - - return false; - } - - @Override - public void onActivityResult(int requestCode, int resultCode, Intent data) { - try { - if (requestCode == ChooserPlugin.PICK_FILE_REQUEST && this.callback != null) { - if (resultCode == Activity.RESULT_OK) { - ClipData clipdata = data.getClipData(); - Uri uriData = data.getData(); - JSONArray result = new JSONArray(); - - Context appContext = this.cordova.getActivity().getApplicationContext(); - - if (clipdata != null) { - - int count = data.getClipData().getItemCount(); - int currentItem = 0; - while (currentItem < count) { - Uri uri = data.getClipData().getItemAt(currentItem).getUri(); - currentItem = currentItem + 1; - result.put(getPath(appContext, uri)); - } - - this.callback.success(result); - } else if (uriData != null) { - result.put(getPath(appContext, uriData)); - this.callback.success(result); - } else { - this.callback.error("File URI was null."); - } - } else if (resultCode == Activity.RESULT_CANCELED) { - this.callback.success("RESULT_CANCELED"); - } else { - this.callback.error(resultCode); - } - } - } catch (Exception err) { - this.callback.error("Failed to read file: " + err.toString()); - } - } - - - /** - * @param uri The Uri to check. - * @return Whether the Uri authority is ExternalStorageProvider. - */ - private static boolean isExternalStorageDocument(Uri uri) { - return "com.android.externalstorage.documents".equals(uri.getAuthority()); - } - - /** - * @param uri The Uri to check. - * @return Whether the Uri authority is DownloadsProvider. - */ - private static boolean isDownloadsDocument(Uri uri) { - return "com.android.providers.downloads.documents".equals(uri.getAuthority()); - } - - /** - * @param uri The Uri to check. - * @return Whether the Uri authority is MediaProvider. - */ - private static boolean isMediaDocument(Uri uri) { - return "com.android.providers.media.documents".equals(uri.getAuthority()); - } - - /** - * @param uri The Uri to check. - * @return Whether the Uri authority is Google Photos. - */ - private static boolean isGooglePhotosUri(Uri uri) { - return ("com.google.android.apps.photos.content".equals(uri.getAuthority()) - || "com.google.android.apps.photos.contentprovider".equals(uri.getAuthority())); - } - - /** - * @param uri The Uri to check. - * @return Whether the Uri authority is Google Drive. - */ - private static boolean isGoogleDriveUri(Uri uri) { - return "com.google.android.apps.docs.storage".equals(uri.getAuthority()) || "com.google.android.apps.docs.storage.legacy".equals(uri.getAuthority()); - } - - /** - * @param uri The Uri to check. - * @return Whether the Uri authority is One Drive. - */ - private static boolean isOneDriveUri(Uri uri) { - return "com.microsoft.skydrive.content.external".equals(uri.getAuthority()); - } - - /** - * Get the value of the data column for this Uri. This is useful for - * MediaStore Uris, and other file-based ContentProviders. - * - * @param context The context. - * @param uri The Uri to query. - * @param selection (Optional) Filter used in the query. - * @param selectionArgs (Optional) Selection arguments used in the query. - * @return The value of the _data column, which is typically a file path. - */ - private static String getDataColumn(Context context, Uri uri, String selection, - String[] selectionArgs) { - - Cursor cursor = null; - final String column = "_data"; - final String[] projection = { - column - }; - - try { - cursor = context.getContentResolver().query(uri, projection, selection, selectionArgs, - null); - if (cursor != null && cursor.moveToFirst()) { - final int column_index = cursor.getColumnIndexOrThrow(column); - return cursor.getString(column_index); - } - } finally { - if (cursor != null) - cursor.close(); - } - return null; - } - - /** - * Get content:// from segment list - * In the new Uri Authority of Google Photos, the last segment is not the content:// anymore - * So let's iterate through all segments and find the content uri! - * - * @param segments The list of segment - */ - private static String getContentFromSegments(List segments) { - String contentPath = ""; - - for (String item : segments) { - if (item.startsWith("content://")) { - contentPath = item; - break; - } - } - - return contentPath; - } - - /** - * Check if a file exists on device - * - * @param filePath The absolute file path - */ - private static boolean fileExists(String filePath) { - File file = new SuFile(filePath); - - return file.exists(); - } - - /** - * Get full file path from external storage - * - * @param pathData The storage type and the relative path - */ - private static String getPathFromExtSD(String[] pathData) { - final String type = pathData[0]; - final String relativePath = "/" + pathData[1]; - String fullPath = ""; - - // on my Sony devices (4.4.4 & 5.1.1), `type` is a dynamic string - // something like "71F8-2C0A", some kind of unique id per storage - // don't know any API that can get the root path of that storage based on its id. - // - // so no "primary" type, but let the check here for other devices - if ("primary".equalsIgnoreCase(type)) { - fullPath = Environment.getExternalStorageDirectory() + relativePath; - if (fileExists(fullPath)) { - return fullPath; - } - } - - //fix some devices(Android Q),'type' like "71F8-2C0A" - //but "primary".equalsIgnoreCase(type) is false - fullPath = "/storage/" + type + "/" + relativePath; - if (fileExists(fullPath)) { - return fullPath; - } - - // Environment.isExternalStorageRemovable() is `true` for external and internal storage - // so we cannot relay on it. - // - // instead, for each possible path, check if file exists - // we'll start with secondary storage as this could be our (physically) removable sd card - fullPath = System.getenv("SECONDARY_STORAGE") + relativePath; - if (fileExists(fullPath)) { - return fullPath; - } - - fullPath = System.getenv("EXTERNAL_STORAGE") + relativePath; - if (fileExists(fullPath)) { - return fullPath; - } - - return ""; - } - - /** - * sometimes in raw type, the second part is a valid filepath - * - * @param rawPath The raw path - */ - private static String getRawFilepath(String rawPath) { - final String[] split = rawPath.split(":"); - if (fileExists(split[1])) { - return split[1]; - } - - return ""; - } - - /** - * Get a file path from a Uri. This will get the the path for Storage Access - * Framework Documents, as well as the _data field for the MediaStore and - * other file-based ContentProviders.
- *
- * Callers should check whether the path is local before assuming it - * represents a local file. - * - * @param context The context. - * @param uri The Uri to query. - */ - private static String getPath(final Context context, final Uri uri) { - - Log.d(TAG, "File - " + - "Authority: " + uri.getAuthority() + - ", Fragment: " + uri.getFragment() + - ", Port: " + uri.getPort() + - ", Query: " + uri.getQuery() + - ", Scheme: " + uri.getScheme() + - ", Host: " + uri.getHost() + - ", Segments: " + uri.getPathSegments().toString() - ); - - // DocumentProvider - if (DocumentsContract.isDocumentUri(context, uri)) { - // ExternalStorageProvider - if (isExternalStorageDocument(uri)) { - final String docId = DocumentsContract.getDocumentId(uri); - final String[] split = docId.split(":"); - final String type = split[0]; - - String fullPath = getPathFromExtSD(split); - if (fullPath != "") { - return fullPath; - } else { - return null; - } - } - // DownloadsProvider - else if (isDownloadsDocument(uri)) { - // thanks to https://github.com/hiddentao/cordova-plugin-filepath/issues/34#issuecomment-430129959 - Cursor cursor = null; - try { - cursor = context.getContentResolver().query(uri, new String[]{MediaStore.MediaColumns.DISPLAY_NAME}, null, null, null); - if (cursor != null && cursor.moveToFirst()) { - String fileName = cursor.getString(0); - String path = Environment.getExternalStorageDirectory().toString() + "/Download/" + fileName; - if (fileExists(path)) { - return path; - } - } - } finally { - if (cursor != null) - cursor.close(); - } - // - final String id = DocumentsContract.getDocumentId(uri); - - // sometimes in raw type, the second part is a valid filepath - final String rawFilepath = getRawFilepath(id); - if (!Objects.equals(rawFilepath, "")) { - return rawFilepath; - } - - String[] contentUriPrefixesToTry = new String[]{ - "content://downloads/public_downloads", - "content://downloads/my_downloads" - }; - - for (String contentUriPrefix : contentUriPrefixesToTry) { - Uri contentUri = ContentUris.withAppendedId(Uri.parse(contentUriPrefix), Long.parseLong(id)); - try { - String path = getDataColumn(context, contentUri, null, null); - if (path != null) { - return path; - } - } catch (Exception ignored) { - } - } - - try { - return getDriveFilePath(uri, context); - } catch (Exception e) { - return uri.getPath(); - } - - } - // MediaProvider - else if (isMediaDocument(uri)) { - final String docId = DocumentsContract.getDocumentId(uri); - final String[] split = docId.split(":"); - final String type = split[0]; - - Uri contentUri = null; - if ("image".equals(type)) { - contentUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI; - } else if ("video".equals(type)) { - contentUri = MediaStore.Video.Media.EXTERNAL_CONTENT_URI; - } else if ("audio".equals(type)) { - contentUri = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI; - } else { - contentUri = MediaStore.Files.getContentUri("external"); - } - - final String selection = "_id=?"; - final String[] selectionArgs = new String[]{ - split[1] - }; - - return getDataColumn(context, contentUri, selection, selectionArgs); - } else if (isGoogleDriveUri(uri)) { - return getDriveFilePath(uri, context); - } - } - // MediaStore (and general) - else if ("content".equalsIgnoreCase(uri.getScheme())) { - - // Return the remote address - if (isGooglePhotosUri(uri)) { - if (uri.toString().contains("mediakey")) { - return getDriveFilePath(uri, context); - } else { - String contentPath = getContentFromSegments(uri.getPathSegments()); - if (!Objects.equals(contentPath, "")) { - return getPath(context, Uri.parse(contentPath)); - } else { - return null; - } - } - } - - if (isGoogleDriveUri(uri) || isOneDriveUri(uri)) { - return getDriveFilePath(uri, context); - } - - return getDataColumn(context, uri, null, null); - } - // File - else if ("file".equalsIgnoreCase(uri.getScheme())) { - return uri.getPath(); - } - - return null; - } - - private static String getDriveFilePath(Uri uri, Context context) { - Uri returnUri = uri; - Cursor returnCursor = context.getContentResolver().query(returnUri, null, null, null, null); - /* - * Get the column indexes of the data in the Cursor, - * * move to the first row in the Cursor, get the data, - * * and display it. - * */ - int nameIndex = returnCursor.getColumnIndex(OpenableColumns.DISPLAY_NAME); - int sizeIndex = returnCursor.getColumnIndex(OpenableColumns.SIZE); - returnCursor.moveToFirst(); - String name = (returnCursor.getString(nameIndex)); - String size = (Long.toString(returnCursor.getLong(sizeIndex))); - File file = new File(context.getCacheDir(), name); - try { - InputStream inputStream = context.getContentResolver().openInputStream(uri); - FileOutputStream outputStream = new FileOutputStream(file); - int read = 0; - int maxBufferSize = 1 * 1024 * 1024; - int bytesAvailable = inputStream.available(); - - //int bufferSize = 1024; - int bufferSize = Math.min(bytesAvailable, maxBufferSize); - - final byte[] buffers = new byte[bufferSize]; - while ((read = inputStream.read(buffers)) != -1) { - outputStream.write(buffers, 0, read); - } - Log.e("File Size", "Size " + file.length()); - inputStream.close(); - outputStream.close(); - Log.e("File Path", "Path " + file.getPath()); - Log.e("File Size", "Size " + file.length()); - } catch (Exception e) { - Log.e("Exception", e.getMessage()); - } - return file.getPath(); - } - -} \ No newline at end of file diff --git a/app/src/main/java/com/dergoogler/plugin/DownloadPlugin.java b/app/src/main/java/com/dergoogler/plugin/DownloadPlugin.java deleted file mode 100644 index 28fffc49..00000000 --- a/app/src/main/java/com/dergoogler/plugin/DownloadPlugin.java +++ /dev/null @@ -1,120 +0,0 @@ -package com.dergoogler.plugin; - -import android.os.Handler; -import android.os.Looper; - -import com.topjohnwu.superuser.io.SuFileOutputStream; - -import org.apache.cordova.CordovaPlugin; -import org.apache.cordova.CallbackContext; -import org.apache.cordova.PluginResult; -import org.json.JSONArray; -import org.json.JSONException; -import org.json.JSONObject; - -import java.io.BufferedInputStream; -import java.io.InputStream; -import java.io.OutputStream; -import java.net.HttpURLConnection; -import java.net.URL; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; - -public class DownloadPlugin extends CordovaPlugin { - private static final String TAG = "DownloadPlugin"; - private final ExecutorService executor = Executors.newSingleThreadExecutor(); - private final Handler handler = new Handler(Looper.getMainLooper()); - private CallbackContext downloadCallbackContext = null; - - @Override - public boolean execute(String action, JSONArray args, CallbackContext callbackContext) throws JSONException { - if (action.equals("start")) { - String url = args.getString(0); - String dest = args.getString(1); - this.downloadCallbackContext = callbackContext; - executor.execute(() -> downloadFile(url, dest, callbackContext)); - return true; - } - return false; - } - - private void downloadFile(String fileUrl, String fileDest, CallbackContext callbackContext) { - InputStream input = null; - OutputStream output = null; - HttpURLConnection connection = null; - try { - URL url = new URL(fileUrl); - connection = (HttpURLConnection) url.openConnection(); - connection.connect(); - - if (connection.getResponseCode() != HttpURLConnection.HTTP_OK) { - String error = "Server returned HTTP " + connection.getResponseCode() + " " + connection.getResponseMessage(); - handler.post(() -> updateError(error)); - return; - } - - int fileLength = connection.getContentLength(); - - input = new BufferedInputStream(connection.getInputStream()); - output = SuFileOutputStream.open(fileDest); - - byte[] data = new byte[4096]; - long total = 0; - int count; - while ((count = input.read(data)) != -1) { - total += count; - if (fileLength > 0) { - int progress = (int) (total * 100 / fileLength); - JSONObject progressObj = new JSONObject(); - progressObj.put("type", "downloading"); - progressObj.put("state", progress); - handler.post(() -> updateResult(progressObj)); - } - output.write(data, 0, count); - } - JSONObject progressObj = new JSONObject(); - progressObj.put("type", "finished"); - progressObj.put("state", null); - handler.post(() -> updateResult(progressObj)); - } catch (Exception e) { - handler.post(() -> updateError(e.toString())); - } finally { - try { - if (output != null) { - output.close(); - } - if (input != null) { - input.close(); - } - } catch (Exception ignored) { - } - if (connection != null) { - connection.disconnect(); - } - } - } - - private void updateError(String err) { - sendUpdate(PluginResult.Status.ERROR, err); - } - - private void updateResult(JSONObject line) { - sendUpdate(PluginResult.Status.OK, line); - } - - private void sendUpdate(PluginResult.Status status, String res) { - if (this.downloadCallbackContext != null) { - PluginResult result = new PluginResult(status, res); - result.setKeepCallback(true); - this.downloadCallbackContext.sendPluginResult(result); - } - } - - private void sendUpdate(PluginResult.Status status, JSONObject res) { - if (this.downloadCallbackContext != null) { - PluginResult result = new PluginResult(status, res); - result.setKeepCallback(true); - this.downloadCallbackContext.sendPluginResult(result); - } - } -} diff --git a/app/src/main/java/com/dergoogler/plugin/FetchPlugin.java b/app/src/main/java/com/dergoogler/plugin/FetchPlugin.java deleted file mode 100644 index 91eb28a4..00000000 --- a/app/src/main/java/com/dergoogler/plugin/FetchPlugin.java +++ /dev/null @@ -1,170 +0,0 @@ -package com.dergoogler.plugin; - -import android.util.Base64; -import android.util.Log; - -import okhttp3.Callback; -import okhttp3.Headers; -import okhttp3.MediaType; -import okhttp3.OkHttpClient; -import okhttp3.Request; -import okhttp3.RequestBody; -import okhttp3.Response; -import okhttp3.Call; - -import org.apache.cordova.CallbackContext; -import org.apache.cordova.CordovaPlugin; -import org.apache.cordova.PluginResult; -import org.json.JSONArray; -import org.json.JSONException; -import org.json.JSONObject; - -import java.io.IOException; -import java.util.concurrent.TimeUnit; - -public class FetchPlugin extends CordovaPlugin { - - public static final String LOG_TAG = "FetchPlugin"; - private static CallbackContext callbackContext; - - private OkHttpClient mClient = new OkHttpClient(); - public static final MediaType MEDIA_TYPE_MARKDOWN = MediaType.parse("application/x-www-form-urlencoded; charset=utf-8"); - - private static final long DEFAULT_TIMEOUT = 10; - - @Override - public boolean execute(final String action, final JSONArray data, final CallbackContext callbackContext) { - - if (action.equals("fetch")) { - - try { - String method = data.getString(0); - // Log.v(LOG_TAG, "execute: method = " + method.toString()); - - String urlString = data.getString(1); - // Log.v(LOG_TAG, "execute: urlString = " + urlString.toString()); - - String postBody = data.getString(2); - // Log.v(LOG_TAG, "execute: postBody = " + postBody.toString()); - - JSONObject headers = data.getJSONObject(3); - if (headers.has("map") && headers.getJSONObject("map") != null) { - headers = headers.getJSONObject("map"); - } - - // Log.v(LOG_TAG, "execute: headers = " + headers.toString()); - - Request.Builder requestBuilder = new Request.Builder(); - - // method + postBody - if (postBody != null && !postBody.equals("null")) { - // requestBuilder.post(RequestBody.create(MEDIA_TYPE_MARKDOWN, postBody.toString())); - String contentType; - if (headers.has("content-type")) { - JSONArray contentTypeHeaders = headers.getJSONArray("content-type"); - contentType = contentTypeHeaders.getString(0); - } else { - contentType = "application/json"; - } - requestBuilder.post(RequestBody.create(MediaType.parse(contentType), postBody.toString())); - } else { - requestBuilder.method(method, null); - } - - // url - requestBuilder.url(urlString); - - // headers - if (headers != null && headers.names() != null && headers.names().length() > 0) { - for (int i = 0; i < headers.names().length(); i++) { - - String headerName = headers.names().getString(i); - JSONArray headerValues = headers.getJSONArray(headers.names().getString(i)); - - if (headerValues.length() > 0) { - String headerValue = headerValues.getString(0); - // Log.v(LOG_TAG, "key = " + headerName + " value = " + headerValue); - requestBuilder.addHeader(headerName, headerValue); - } - } - } - - Request request = requestBuilder.build(); - - mClient.newCall(request).enqueue(new Callback() { - @Override - public void onFailure(Call call, IOException throwable) { - throwable.printStackTrace(); - callbackContext.sendPluginResult(new PluginResult(PluginResult.Status.ERROR, throwable.getMessage())); - } - - @Override - public void onResponse(Call call, Response response) throws IOException { - - JSONObject result = new JSONObject(); - try { - Headers responseHeaders = response.headers(); - - JSONObject allHeaders = new JSONObject(); - - if (responseHeaders != null) { - for (int i = 0; i < responseHeaders.size(); i++) { - if (responseHeaders.name(i).compareToIgnoreCase("set-cookie") == 0 && - allHeaders.has(responseHeaders.name(i))) { - allHeaders.put(responseHeaders.name(i), allHeaders.get(responseHeaders.name(i)) + ",\n" + responseHeaders.value(i)); - continue; - } - allHeaders.put(responseHeaders.name(i), responseHeaders.value(i)); - } - } - - result.put("headers", allHeaders); - - if (response.body().contentType().type().equals("image")) { - result.put("isBlob", true); - result.put("body", Base64.encodeToString(response.body().bytes(), Base64.DEFAULT)); - } else { - result.put("body", response.body().string()); - } - - result.put("statusText", response.message()); - result.put("status", response.code()); - result.put("url", response.request().url().toString()); - - } catch (Exception e) { - e.printStackTrace(); - } - - // Log.v(LOG_TAG, "HTTP code: " + response.code()); - // Log.v(LOG_TAG, "returning: " + result.toString()); - - callbackContext.sendPluginResult(new PluginResult(PluginResult.Status.OK, result)); - } - }); - - } catch (JSONException e) { - Log.e(LOG_TAG, "execute: Got JSON Exception " + e.getMessage()); - callbackContext.error(e.getMessage()); - } - - } else if (action.equals("setTimeout")) { - this.setTimeout(data.optLong(0, DEFAULT_TIMEOUT)); - } else { - Log.e(LOG_TAG, "Invalid action : " + action); - callbackContext.sendPluginResult(new PluginResult(PluginResult.Status.INVALID_ACTION)); - return false; - } - - return true; - } - - private void setTimeout(long seconds) { - // Log.v(LOG_TAG, "setTimeout: " + seconds); - - mClient = mClient.newBuilder() - .connectTimeout(seconds, TimeUnit.SECONDS) - .readTimeout(seconds, TimeUnit.SECONDS) - .writeTimeout(seconds, TimeUnit.SECONDS) - .build(); - } -} \ No newline at end of file diff --git a/app/src/main/java/com/dergoogler/plugin/TerminalPlugin.java b/app/src/main/java/com/dergoogler/plugin/TerminalPlugin.java deleted file mode 100644 index b2ed5b06..00000000 --- a/app/src/main/java/com/dergoogler/plugin/TerminalPlugin.java +++ /dev/null @@ -1,137 +0,0 @@ -package com.dergoogler.plugin; - -import android.util.Log; - -import com.dergoogler.util.Json; -import com.topjohnwu.superuser.Shell; -import com.topjohnwu.superuser.io.SuFile; - -import org.apache.cordova.CallbackContext; -import org.apache.cordova.CordovaPlugin; -import org.apache.cordova.PluginResult; -import org.json.JSONArray; -import org.json.JSONException; -import org.json.JSONObject; - -import java.io.BufferedReader; -import java.io.IOException; -import java.io.InputStream; -import java.io.InputStreamReader; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.Iterator; -import java.util.List; -import java.util.Map; -import java.util.Scanner; - -public class TerminalPlugin extends CordovaPlugin { - private static final String TAG = "TerminalPlugin"; - private CallbackContext terminalCallbackContext = null; - - private int ProcessCode = 1000; - - @Override - public boolean execute(String action, JSONArray data, CallbackContext callbackContext) throws JSONException { - switch (action) { - case "exec": - String cmd = data.getString(0); - JSONObject envp = data.getJSONObject(1); - String cwd = data.getString(2); - - this.terminalCallbackContext = callbackContext; - String[] commands = {"su", "-c", cmd}; - - - cordova.getThreadPool().execute(() -> { - try { - run(envp, cwd, commands); - } catch (IOException | JSONException e) { - Log.e(TAG + ":execute", e.toString()); - } - }); - return true; - case "test": - String msg = data.getString(0); - Log.i(TAG, msg); - return true; - - default: - return false; - } - - } - - public void run(JSONObject envp, String cwd, String... command) throws IOException, JSONException { - ProcessBuilder pb = new ProcessBuilder(command); - - if (envp != null) { - Map m = pb.environment(); - m.putAll(Json.toMap(envp)); - } - - pb.directory(new SuFile(cwd)); - Process process = pb.start(); - - try (BufferedReader stdoutReader = new BufferedReader(new InputStreamReader(process.getInputStream()))) { - String line; - while ((line = stdoutReader.readLine()) != null) { - JSONObject progressObj = new JSONObject(); - progressObj.put("stdout", line); - progressObj.put("stderr", null); - updateTerminalOutput(progressObj); - } - } catch (Exception e) { - Log.e(TAG + ":run -> stdout", e.toString()); - } - - try (BufferedReader stderrReader = new BufferedReader(new InputStreamReader(process.getErrorStream()))) { - String line; - while ((line = stderrReader.readLine()) != null) { - JSONObject progressObj = new JSONObject(); - progressObj.put("stdout", null); - progressObj.put("stderr", line); - updateTerminalOutput(progressObj); - } - } catch (Exception e) { - Log.e(TAG + ":run -> stderr", e.toString()); - } - - int exitCode; - try { - exitCode = process.waitFor(); - updateTerminalExit(exitCode); - process.destroy(); - } catch (InterruptedException e) { - Log.e(TAG + ":run -> exit", e.toString()); - updateTerminalExit(500); - process.destroy(); - } - } - - - private void updateTerminalOutput(JSONObject line) { - sendUpdate(PluginResult.Status.OK, line); - } - - private void updateTerminalExit(int code) { - sendUpdate(PluginResult.Status.ERROR, code); - } - - - private void sendUpdate(PluginResult.Status status, int line) { - if (this.terminalCallbackContext != null) { - PluginResult result = new PluginResult(status, line); - result.setKeepCallback(true); - this.terminalCallbackContext.sendPluginResult(result); - } - } - - - private void sendUpdate(PluginResult.Status status, JSONObject obj) { - if (this.terminalCallbackContext != null) { - PluginResult result = new PluginResult(status, obj); - result.setKeepCallback(true); - this.terminalCallbackContext.sendPluginResult(result); - } - } -} diff --git a/app/src/main/java/com/dergoogler/util/Json.java b/app/src/main/java/com/dergoogler/util/Json.java deleted file mode 100644 index 4ed8c7ea..00000000 --- a/app/src/main/java/com/dergoogler/util/Json.java +++ /dev/null @@ -1,50 +0,0 @@ -package com.dergoogler.util; - -import org.json.JSONArray; -import org.json.JSONException; -import org.json.JSONObject; - -import java.util.ArrayList; -import java.util.HashMap; -import java.util.Iterator; -import java.util.List; -import java.util.Map; - -public class Json { - public static Map toMap(JSONObject jsonobj) throws JSONException { - Map map = new HashMap(); - Iterator keys = jsonobj.keys(); - while (keys.hasNext()) { - String key = keys.next(); - String value = jsonobj.getString(key); - map.put(key, value); - } - return map; - } - - public static List toList(JSONArray array) throws JSONException { - List list = new ArrayList(); - for (int i = 0; i < array.length(); i++) { - Object value = array.get(i); - if (value instanceof JSONArray) { - value = toList((JSONArray) value); - } else if (value instanceof JSONObject) { - value = toMap((JSONObject) value); - } - list.add(value); - } - return list; - } - - public static String[] getStringArray(JSONArray jsonArray) { - String[] stringArray = null; - if (jsonArray != null) { - int length = jsonArray.length(); - stringArray = new String[length]; - for (int i = 0; i < length; i++) { - stringArray[i] = jsonArray.optString(i); - } - } - return stringArray; - } -} diff --git a/app/src/main/kotlin/com/dergoogler/mmrl/App.kt b/app/src/main/kotlin/com/dergoogler/mmrl/App.kt new file mode 100644 index 00000000..eed02d95 --- /dev/null +++ b/app/src/main/kotlin/com/dergoogler/mmrl/App.kt @@ -0,0 +1,29 @@ +package com.dergoogler.mmrl + +import android.app.Application +import com.dergoogler.mmrl.app.utils.NotificationUtils +import com.dergoogler.mmrl.network.NetworkUtils +import com.dergoogler.mmrl.utils.timber.DebugTree +import com.dergoogler.mmrl.utils.timber.ReleaseTree +import dagger.hilt.android.HiltAndroidApp +import dev.dergoogler.mmrl.compat.ServiceManagerCompat +import timber.log.Timber + +@HiltAndroidApp +class App : Application() { + init { + if (BuildConfig.DEBUG) { + Timber.plant(DebugTree()) + } else { + Timber.plant(ReleaseTree()) + } + } + + override fun onCreate() { + super.onCreate() + + ServiceManagerCompat.setHiddenApiExemptions() + NotificationUtils.init(this) + NetworkUtils.setCacheDir(cacheDir) + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/com/dergoogler/mmrl/Compat.kt b/app/src/main/kotlin/com/dergoogler/mmrl/Compat.kt new file mode 100644 index 00000000..9c4588c2 --- /dev/null +++ b/app/src/main/kotlin/com/dergoogler/mmrl/Compat.kt @@ -0,0 +1,63 @@ +package com.dergoogler.mmrl + +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue +import com.dergoogler.mmrl.datastore.WorkingMode +import dev.dergoogler.mmrl.compat.ServiceManagerCompat +import dev.dergoogler.mmrl.compat.stub.IFileManager +import dev.dergoogler.mmrl.compat.stub.IModuleManager +import dev.dergoogler.mmrl.compat.stub.IPowerManager +import dev.dergoogler.mmrl.compat.stub.IServiceManager +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import timber.log.Timber + +object Compat { + private var mServiceOrNull: IServiceManager? = null + private val mService get() = checkNotNull(mServiceOrNull) { + "IServiceManager haven't been received" + } + + var isAlive by mutableStateOf(false) + private set + + private val _isAliveFlow = MutableStateFlow(false) + val isAliveFlow get() = _isAliveFlow.asStateFlow() + + val moduleManager: IModuleManager get() = mService.moduleManager + val fileManager: IFileManager get() = mService.fileManager + val powerManager: IPowerManager get() = mService.powerManager + + private fun state(): Boolean { + isAlive = mServiceOrNull != null + _isAliveFlow.value = isAlive + + return isAlive + } + + suspend fun init(mode: WorkingMode) = when { + isAlive -> true + else -> try { + mServiceOrNull = when (mode) { + WorkingMode.MODE_SHIZUKU -> ServiceManagerCompat.fromShizuku() + WorkingMode.MODE_ROOT -> ServiceManagerCompat.fromLibSu() + else -> null + } + + state() + } catch (e: Exception) { + Timber.e(e) + + mServiceOrNull = null + state() + } + } + + fun get(fallback: T, block: Compat.() -> T): T { + return when { + isAlive -> block(this) + else -> fallback + } + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/com/dergoogler/mmrl/app/Const.kt b/app/src/main/kotlin/com/dergoogler/mmrl/app/Const.kt new file mode 100644 index 00000000..62f09191 --- /dev/null +++ b/app/src/main/kotlin/com/dergoogler/mmrl/app/Const.kt @@ -0,0 +1,19 @@ +package com.dergoogler.mmrl.app + +import android.os.Environment +import java.io.File + +object Const { + val PUBLIC_DOWNLOADS: File = Environment.getExternalStoragePublicDirectory( + Environment.DIRECTORY_DOWNLOADS + ) + + const val SANMER_GITHUB_URL = "https://github.com/SanmerDev" + const val GOOGLER_GITHUB_URL = "https://github.com/DerGoogler" + const val RESOURCES_URL = "https://github.com/DerGoogler/MMRL/wiki" + const val TRANSLATE_URL = "https://example.com/translate" + const val GITHUB_URL = "https://github.com/DerGoogler/MMRL" + const val TELEGRAM_URL = "https://t.me/GooglersRepo" + const val DEMO_REPO_URL = "https://gr.dergoogler.com/gmr/" + const val SPDX_URL = "https://spdx.org/licenses/%s.json" +} \ No newline at end of file diff --git a/app/src/main/kotlin/com/dergoogler/mmrl/app/Event.kt b/app/src/main/kotlin/com/dergoogler/mmrl/app/Event.kt new file mode 100644 index 00000000..c87d6519 --- /dev/null +++ b/app/src/main/kotlin/com/dergoogler/mmrl/app/Event.kt @@ -0,0 +1,17 @@ +package com.dergoogler.mmrl.app + +enum class Event { + NON, + LOADING, + SUCCEEDED, + FAILED; + + companion object { + val Event.isNon get() = this == NON + val Event.isLoading get() = this == LOADING + val Event.isSucceeded get() = this == SUCCEEDED + val Event.isFailed get() = this == FAILED + val Event.isFinished get() = isSucceeded || isFailed + val Event.isNotReady get() = isNon || isFailed + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/com/dergoogler/mmrl/app/utils/NotificationUtils.kt b/app/src/main/kotlin/com/dergoogler/mmrl/app/utils/NotificationUtils.kt new file mode 100644 index 00000000..574b9f1f --- /dev/null +++ b/app/src/main/kotlin/com/dergoogler/mmrl/app/utils/NotificationUtils.kt @@ -0,0 +1,26 @@ +package com.dergoogler.mmrl.app.utils + +import android.app.NotificationChannel +import android.app.NotificationManager +import android.content.Context +import androidx.core.app.NotificationManagerCompat +import com.dergoogler.mmrl.R + +object NotificationUtils { + const val CHANNEL_ID_DOWNLOAD = "DOWNLOAD" + const val NOTIFICATION_ID_DOWNLOAD = 1024 + + fun init(context: Context) { + val channels = listOf( + NotificationChannel(CHANNEL_ID_DOWNLOAD, + context.getString(R.string.notification_name_download), + NotificationManager.IMPORTANCE_HIGH + ) + ) + + NotificationManagerCompat.from(context).apply { + createNotificationChannels(channels) + deleteUnlistedNotificationChannels(channels.map { it.id }) + } + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/com/dergoogler/mmrl/compat/BuildCompat.kt b/app/src/main/kotlin/com/dergoogler/mmrl/compat/BuildCompat.kt new file mode 100644 index 00000000..fdf15d21 --- /dev/null +++ b/app/src/main/kotlin/com/dergoogler/mmrl/compat/BuildCompat.kt @@ -0,0 +1,18 @@ +package com.dergoogler.mmrl.compat + +import android.os.Build +import androidx.annotation.ChecksSdkIntAtLeast + +object BuildCompat { + @get:ChecksSdkIntAtLeast(api = Build.VERSION_CODES.TIRAMISU) + val atLeastT get() = Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU + + @get:ChecksSdkIntAtLeast(api = Build.VERSION_CODES.S) + val atLeastS get() = Build.VERSION.SDK_INT >= Build.VERSION_CODES.S + + @get:ChecksSdkIntAtLeast(api = Build.VERSION_CODES.R) + val atLeastR get() = Build.VERSION.SDK_INT >= Build.VERSION_CODES.R + + @get:ChecksSdkIntAtLeast(api = Build.VERSION_CODES.P) + val atLeastP get() = Build.VERSION.SDK_INT >= Build.VERSION_CODES.P +} \ No newline at end of file diff --git a/app/src/main/kotlin/com/dergoogler/mmrl/compat/MediaStoreCompat.kt b/app/src/main/kotlin/com/dergoogler/mmrl/compat/MediaStoreCompat.kt new file mode 100644 index 00000000..57851559 --- /dev/null +++ b/app/src/main/kotlin/com/dergoogler/mmrl/compat/MediaStoreCompat.kt @@ -0,0 +1,61 @@ +package com.dergoogler.mmrl.compat + +import android.content.Context +import android.net.Uri +import android.provider.DocumentsContract +import android.provider.OpenableColumns +import android.system.Os +import androidx.core.net.toFile +import androidx.documentfile.provider.DocumentFile +import java.io.File + +object MediaStoreCompat { + private fun Context.getDisplayNameForUri(uri: Uri): String { + if (uri.scheme == "file") { + return uri.toFile().name + } + + require(uri.scheme == "content") { "Uri lacks 'content' scheme: $uri" } + + val projection = arrayOf(OpenableColumns.DISPLAY_NAME) + contentResolver.query(uri, projection, null, null, null)?.use { cursor -> + val displayNameColumn = cursor.getColumnIndexOrThrow(OpenableColumns.DISPLAY_NAME) + if (cursor.moveToFirst()) { + return cursor.getString(displayNameColumn) + } + } + + return uri.toString() + } + + fun Context.getPathForUri(uri: Uri): String { + if (uri.scheme == "file") { + return uri.toFile().path + } + + require(uri.scheme == "content") { "Uri lacks 'content' scheme: $uri" } + + val real = if (DocumentsContract.isTreeUri(uri)) { + DocumentFile.fromTreeUri(this, uri)?.uri ?: uri + } else { + uri + } + + return contentResolver.openFileDescriptor(real, "r")?.use { + Os.readlink("/proc/self/fd/${it.fd}") + } ?: uri.toString() + } + + fun Context.getFileForUri(uri: Uri) = File(getPathForUri(uri)) + + fun Context.copyToDir(uri: Uri, dir: File): File { + val tmp = dir.resolve(getDisplayNameForUri(uri)) + contentResolver.openInputStream(uri)?.buffered()?.use { input -> + tmp.outputStream().use { output -> + input.copyTo(output) + } + } + + return tmp + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/com/dergoogler/mmrl/compat/PermissionCompat.kt b/app/src/main/kotlin/com/dergoogler/mmrl/compat/PermissionCompat.kt new file mode 100644 index 00000000..55a1c91e --- /dev/null +++ b/app/src/main/kotlin/com/dergoogler/mmrl/compat/PermissionCompat.kt @@ -0,0 +1,71 @@ +package com.dergoogler.mmrl.compat + +import android.app.Activity +import android.content.Context +import android.content.ContextWrapper +import android.content.pm.PackageManager +import androidx.activity.result.ActivityResultRegistryOwner +import androidx.activity.result.contract.ActivityResultContracts +import androidx.core.content.ContextCompat +import java.util.UUID + +object PermissionCompat { + data class PermissionState( + private val results: Map + ) { + val allGranted = results.all { it.value } + + override fun toString(): String { + return results.toString() + } + } + + private fun Context.findActivity(): Activity? { + var context = this + while (context is ContextWrapper) { + if (context is Activity) return context + context = context.baseContext + } + + return null + } + + fun checkPermissions( + context: Context, + permissions: List + ): PermissionState { + val results = permissions.associateWith { + ContextCompat.checkSelfPermission( + context, it + ) == PackageManager.PERMISSION_GRANTED + } + + return PermissionState(results) + } + + fun requestPermissions( + context: Context, + permissions: List, + callback: (PermissionState) -> Unit + ) { + val state = checkPermissions(context, permissions) + if (state.allGranted) { + callback(state) + return + } + + val activity = context.findActivity() + if (activity !is ActivityResultRegistryOwner) return + + val activityResultRegistry = activity.activityResultRegistry + val key = UUID.randomUUID().toString() + val launcher = activityResultRegistry.register( + key, + ActivityResultContracts.RequestMultiplePermissions() + ) { results -> + callback(PermissionState(results)) + } + + launcher.launch(permissions.toTypedArray()) + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/com/dergoogler/mmrl/database/AppDatabase.kt b/app/src/main/kotlin/com/dergoogler/mmrl/database/AppDatabase.kt new file mode 100644 index 00000000..aada4ce8 --- /dev/null +++ b/app/src/main/kotlin/com/dergoogler/mmrl/database/AppDatabase.kt @@ -0,0 +1,226 @@ +package com.dergoogler.mmrl.database + +import android.content.Context +import androidx.room.Database +import androidx.room.Room +import androidx.room.RoomDatabase +import androidx.room.TypeConverters +import androidx.room.migration.Migration +import com.dergoogler.mmrl.database.dao.JoinDao +import com.dergoogler.mmrl.database.dao.LocalDao +import com.dergoogler.mmrl.database.dao.OnlineDao +import com.dergoogler.mmrl.database.dao.RepoDao +import com.dergoogler.mmrl.database.dao.VersionDao +import com.dergoogler.mmrl.database.entity.LocalModuleEntity +import com.dergoogler.mmrl.database.entity.LocalModuleUpdatable +import com.dergoogler.mmrl.database.entity.OnlineModuleEntity +import com.dergoogler.mmrl.database.entity.Repo +import com.dergoogler.mmrl.database.entity.VersionItemEntity +import com.dergoogler.mmrl.utils.StringListTypeConverter + +@Database( + entities = [ + Repo::class, + LocalModuleUpdatable::class, + OnlineModuleEntity::class, + VersionItemEntity::class, + LocalModuleEntity::class + ], + version = 1 +) +@TypeConverters(StringListTypeConverter::class) +abstract class AppDatabase : RoomDatabase() { + abstract fun repoDao(): RepoDao + abstract fun onlineDao(): OnlineDao + abstract fun versionDao(): VersionDao + abstract fun localDao(): LocalDao + abstract fun joinDao(): JoinDao + + companion object { + /** + * Only migrate data for [Repo] and [LocalModuleUpdatable] + */ + fun build(context: Context) = + Room.databaseBuilder(context, + AppDatabase::class.java, "mmrl") +// .addMigrations( +// MIGRATION_3_4, +// MIGRATION_4_5, +// MIGRATION_5_6, +// MIGRATION_6_7, +// MIGRATION_7_8, +// MIGRATION_8_9, +// MIGRATION_9_10 +// ) + .build() + + private val MIGRATION_3_4 = Migration(3, 4) { + it.execSQL("CREATE TABLE IF NOT EXISTS localModules (" + + "id TEXT NOT NULL, " + + "name TEXT NOT NULL, " + + "version TEXT NOT NULL, " + + "versionCode INTEGER NOT NULL, " + + "author TEXT NOT NULL, " + + "description TEXT NOT NULL, " + + "state TEXT NOT NULL, " + + "PRIMARY KEY(id))") + + it.execSQL("CREATE TABLE IF NOT EXISTS onlineModules (" + + "id TEXT NOT NULL, " + + "repoUrl TEXT NOT NULL, " + + "name TEXT NOT NULL, " + + "version TEXT NOT NULL, " + + "versionCode INTEGER NOT NULL, " + + "author TEXT NOT NULL, " + + "description TEXT NOT NULL, " + + "license TEXT NOT NULL, " + + "PRIMARY KEY(id, repoUrl))") + + it.execSQL("DROP TABLE online_module") + it.execSQL("ALTER TABLE repo RENAME TO repos") + } + + private val MIGRATION_4_5 = Migration(4, 5) { + it.execSQL("CREATE TABLE IF NOT EXISTS versions (" + + "id TEXT NOT NULL, " + + "repoUrl TEXT NOT NULL, " + + "timestamp REAL NOT NULL, " + + "version TEXT NOT NULL, " + + "versionCode INTEGER NOT NULL, " + + "zipUrl TEXT NOT NULL, " + + "changelog TEXT NOT NULL, " + + "PRIMARY KEY(id, repoUrl, versionCode))") + + it.execSQL("CREATE TABLE IF NOT EXISTS repos_new (" + + "url TEXT NOT NULL, " + + "name TEXT NOT NULL, " + + "enable INTEGER NOT NULL, " + + "version INTEGER NOT NULL, " + + "timestamp REAL NOT NULL, " + + "size INTEGER NOT NULL, " + + "PRIMARY KEY(url))") + + it.execSQL("INSERT INTO repos_new (" + + "url, name, enable, version, timestamp, size) " + + "SELECT " + + "url, name, enable, 0, timestamp, size " + + "FROM repos") + + it.execSQL("DROP TABLE repos") + it.execSQL("ALTER TABLE repos_new RENAME TO repos") + + it.execSQL("CREATE TABLE IF NOT EXISTS onlineModules_new (" + + "id TEXT NOT NULL, " + + "repoUrl TEXT NOT NULL, " + + "name TEXT NOT NULL, " + + "version TEXT NOT NULL, " + + "versionCode INTEGER NOT NULL, " + + "author TEXT NOT NULL, " + + "description TEXT NOT NULL, " + + "type TEXT NOT NULL, " + + "added REAL NOT NULL, " + + "license TEXT NOT NULL, " + + "homepage TEXT NOT NULL, " + + "source TEXT NOT NULL, " + + "support TEXT NOT NULL, " + + "donate TEXT NOT NULL, " + + "PRIMARY KEY(id, repoUrl))") + + it.execSQL("DROP TABLE onlineModules") + it.execSQL("ALTER TABLE onlineModules_new RENAME TO onlineModules") + } + + private val MIGRATION_5_6 = Migration(5, 6) { + it.execSQL("CREATE TABLE IF NOT EXISTS localModules_new (" + + "id TEXT NOT NULL, " + + "name TEXT NOT NULL, " + + "version TEXT NOT NULL, " + + "versionCode INTEGER NOT NULL, " + + "author TEXT NOT NULL, " + + "description TEXT NOT NULL, " + + "state TEXT NOT NULL, " + + "updateJson TEXT NOT NULL, " + + "PRIMARY KEY(id))") + + it.execSQL("DROP TABLE localModules") + it.execSQL("ALTER TABLE localModules_new RENAME TO localModules") + } + + private val MIGRATION_6_7 = Migration(6, 7) { + it.execSQL("CREATE TABLE IF NOT EXISTS localModules_new (" + + "id TEXT NOT NULL, " + + "name TEXT NOT NULL, " + + "version TEXT NOT NULL, " + + "versionCode INTEGER NOT NULL, " + + "author TEXT NOT NULL, " + + "description TEXT NOT NULL, " + + "state TEXT NOT NULL, " + + "updateJson TEXT NOT NULL, " + + "ignoreUpdates INTEGER NOT NULL, " + + "PRIMARY KEY(id))") + + it.execSQL("DROP TABLE localModules") + it.execSQL("ALTER TABLE localModules_new RENAME TO localModules") + } + + private val MIGRATION_7_8 = Migration(7, 8) { + it.execSQL("CREATE TABLE IF NOT EXISTS localModules_new (" + + "id TEXT NOT NULL, " + + "name TEXT NOT NULL, " + + "version TEXT NOT NULL, " + + "versionCode INTEGER NOT NULL, " + + "author TEXT NOT NULL, " + + "description TEXT NOT NULL, " + + "state TEXT NOT NULL, " + + "updateJson TEXT NOT NULL, " + + "PRIMARY KEY(id))") + + it.execSQL("CREATE TABLE IF NOT EXISTS localModules_updatable (" + + "id TEXT NOT NULL, " + + "updatable INTEGER NOT NULL, " + + "PRIMARY KEY(id))") + + it.execSQL("DROP TABLE localModules") + it.execSQL("ALTER TABLE localModules_new RENAME TO localModules") + } + + private val MIGRATION_8_9 = Migration(8, 9) { + it.execSQL("CREATE TABLE IF NOT EXISTS localModules_new (" + + "id TEXT NOT NULL, " + + "name TEXT NOT NULL, " + + "version TEXT NOT NULL, " + + "versionCode INTEGER NOT NULL, " + + "author TEXT NOT NULL, " + + "description TEXT NOT NULL, " + + "state TEXT NOT NULL, " + + "updateJson TEXT NOT NULL, " + + "lastUpdated INTEGER NOT NULL, " + + "PRIMARY KEY(id))") + + it.execSQL("DROP TABLE localModules") + it.execSQL("ALTER TABLE localModules_new RENAME TO localModules") + } + + private val MIGRATION_9_10 = Migration(9, 10) { + it.execSQL("CREATE TABLE IF NOT EXISTS onlineModules_new (" + + "id TEXT NOT NULL, " + + "repoUrl TEXT NOT NULL, " + + "name TEXT NOT NULL, " + + "version TEXT NOT NULL, " + + "versionCode INTEGER NOT NULL, " + + "author TEXT NOT NULL, " + + "description TEXT NOT NULL, " + + "type TEXT NOT NULL, " + + "added REAL NOT NULL, " + + "license TEXT NOT NULL, " + + "homepage TEXT NOT NULL, " + + "source TEXT NOT NULL, " + + "support TEXT NOT NULL, " + + "donate TEXT NOT NULL, " + + "PRIMARY KEY(id, repoUrl))") + + it.execSQL("DROP TABLE onlineModules") + it.execSQL("ALTER TABLE onlineModules_new RENAME TO onlineModules") + } + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/com/dergoogler/mmrl/database/dao/JoinDao.kt b/app/src/main/kotlin/com/dergoogler/mmrl/database/dao/JoinDao.kt new file mode 100644 index 00000000..7ce80492 --- /dev/null +++ b/app/src/main/kotlin/com/dergoogler/mmrl/database/dao/JoinDao.kt @@ -0,0 +1,35 @@ +package com.dergoogler.mmrl.database.dao + +import androidx.room.Dao +import androidx.room.Query +import com.dergoogler.mmrl.database.entity.OnlineModuleEntity +import com.dergoogler.mmrl.database.entity.VersionItemEntity +import com.dergoogler.mmrl.model.online.ModulesJson +import kotlinx.coroutines.flow.Flow + +@Dao +interface JoinDao { + @Query( + "SELECT m.* " + + "FROM onlineModules m " + + "JOIN repos r ON m.repoUrl = r.url " + + "WHERE r.enable = 1 AND r.version = :version" + ) + fun getOnlineAllAsFlow(version: Int = ModulesJson.CURRENT_VERSION): Flow> + + @Query( + "SELECT m.* " + + "FROM onlineModules m " + + "JOIN repos r ON m.repoUrl = r.url " + + "WHERE m.id = :id AND m.repoUrl = :repoUrl AND r.enable = 1 AND r.version = :version LIMIT 1" + ) + suspend fun getOnlineByIdAndUrl(id: String, repoUrl: String, version: Int = ModulesJson.CURRENT_VERSION): OnlineModuleEntity + + @Query( + "SELECT v.* " + + "FROM versions v " + + "JOIN repos r ON v.repoUrl = r.url " + + "WHERE v.id = :id AND r.enable = 1 AND r.version = :version ORDER BY v.versionCode DESC" + ) + suspend fun getVersionById(id: String, version: Int = ModulesJson.CURRENT_VERSION): List +} \ No newline at end of file diff --git a/app/src/main/kotlin/com/dergoogler/mmrl/database/dao/LocalDao.kt b/app/src/main/kotlin/com/dergoogler/mmrl/database/dao/LocalDao.kt new file mode 100644 index 00000000..0e5943e4 --- /dev/null +++ b/app/src/main/kotlin/com/dergoogler/mmrl/database/dao/LocalDao.kt @@ -0,0 +1,40 @@ +package com.dergoogler.mmrl.database.dao + +import androidx.room.Dao +import androidx.room.Delete +import androidx.room.Insert +import androidx.room.OnConflictStrategy +import androidx.room.Query +import com.dergoogler.mmrl.database.entity.LocalModuleEntity +import com.dergoogler.mmrl.database.entity.LocalModuleUpdatable +import kotlinx.coroutines.flow.Flow + +@Dao +interface LocalDao { + @Query("SELECT * FROM localModules") + fun getAllAsFlow(): Flow> + + @Query("SELECT * FROM localModules WHERE id = :id LIMIT 1") + suspend fun getByIdOrNull(id: String): LocalModuleEntity? + + @Insert(onConflict = OnConflictStrategy.REPLACE) + suspend fun insert(value: LocalModuleEntity) + + @Insert(onConflict = OnConflictStrategy.REPLACE) + suspend fun insert(list: List) + + @Query("DELETE FROM localModules") + suspend fun deleteAll() + + @Query("SELECT * FROM localModules_updatable") + suspend fun getUpdatableTagAll(): List + + @Insert(onConflict = OnConflictStrategy.REPLACE) + suspend fun insertUpdatableTag(value: LocalModuleUpdatable) + + @Query("SELECT * FROM localModules_updatable WHERE id = :id LIMIT 1") + suspend fun hasUpdatableTagOrNull(id: String): LocalModuleUpdatable? + + @Delete + suspend fun deleteUpdatableTag(values: List) +} \ No newline at end of file diff --git a/app/src/main/kotlin/com/dergoogler/mmrl/database/dao/OnlineDao.kt b/app/src/main/kotlin/com/dergoogler/mmrl/database/dao/OnlineDao.kt new file mode 100644 index 00000000..f4136b0f --- /dev/null +++ b/app/src/main/kotlin/com/dergoogler/mmrl/database/dao/OnlineDao.kt @@ -0,0 +1,19 @@ +package com.dergoogler.mmrl.database.dao + +import androidx.room.Dao +import androidx.room.Insert +import androidx.room.OnConflictStrategy +import androidx.room.Query +import com.dergoogler.mmrl.database.entity.OnlineModuleEntity + +@Dao +interface OnlineDao { + @Query("SELECT * FROM onlineModules WHERE id = :id") + suspend fun getAllById(id: String): List + + @Insert(onConflict = OnConflictStrategy.REPLACE) + suspend fun insert(list: List) + + @Query("DELETE from onlineModules WHERE repoUrl = :repoUrl") + suspend fun deleteByUrl(repoUrl: String) +} \ No newline at end of file diff --git a/app/src/main/kotlin/com/dergoogler/mmrl/database/dao/RepoDao.kt b/app/src/main/kotlin/com/dergoogler/mmrl/database/dao/RepoDao.kt new file mode 100644 index 00000000..6ad8aeb3 --- /dev/null +++ b/app/src/main/kotlin/com/dergoogler/mmrl/database/dao/RepoDao.kt @@ -0,0 +1,27 @@ +package com.dergoogler.mmrl.database.dao + +import androidx.room.Dao +import androidx.room.Delete +import androidx.room.Insert +import androidx.room.OnConflictStrategy +import androidx.room.Query +import com.dergoogler.mmrl.database.entity.Repo +import kotlinx.coroutines.flow.Flow + +@Dao +interface RepoDao { + @Query("SELECT * FROM repos") + fun getAllAsFlow(): Flow> + + @Query("SELECT * FROM repos") + suspend fun getAll(): List + + @Query("SELECT * FROM repos WHERE url = :url LIMIT 1") + suspend fun getByUrl(url: String): Repo + + @Insert(onConflict = OnConflictStrategy.REPLACE) + suspend fun insert(value: Repo) + + @Delete + suspend fun delete(value: Repo) +} \ No newline at end of file diff --git a/app/src/main/kotlin/com/dergoogler/mmrl/database/dao/VersionDao.kt b/app/src/main/kotlin/com/dergoogler/mmrl/database/dao/VersionDao.kt new file mode 100644 index 00000000..b38c14fc --- /dev/null +++ b/app/src/main/kotlin/com/dergoogler/mmrl/database/dao/VersionDao.kt @@ -0,0 +1,16 @@ +package com.dergoogler.mmrl.database.dao + +import androidx.room.Dao +import androidx.room.Insert +import androidx.room.OnConflictStrategy +import androidx.room.Query +import com.dergoogler.mmrl.database.entity.VersionItemEntity + +@Dao +interface VersionDao { + @Insert(onConflict = OnConflictStrategy.REPLACE) + suspend fun insert(list: List) + + @Query("DELETE from versions WHERE repoUrl = :repoUrl") + suspend fun deleteByUrl(repoUrl: String) +} \ No newline at end of file diff --git a/app/src/main/kotlin/com/dergoogler/mmrl/database/di/DatabaseModule.kt b/app/src/main/kotlin/com/dergoogler/mmrl/database/di/DatabaseModule.kt new file mode 100644 index 00000000..7f741307 --- /dev/null +++ b/app/src/main/kotlin/com/dergoogler/mmrl/database/di/DatabaseModule.kt @@ -0,0 +1,45 @@ +package com.dergoogler.mmrl.database.di + +import android.content.Context +import com.dergoogler.mmrl.database.AppDatabase +import com.dergoogler.mmrl.database.dao.JoinDao +import com.dergoogler.mmrl.database.dao.LocalDao +import com.dergoogler.mmrl.database.dao.OnlineDao +import com.dergoogler.mmrl.database.dao.RepoDao +import com.dergoogler.mmrl.database.dao.VersionDao +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.android.qualifiers.ApplicationContext +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +object DatabaseModule { + @Provides + @Singleton + fun providesAppDatabase( + @ApplicationContext context: Context + ): AppDatabase = AppDatabase.build(context) + + @Provides + @Singleton + fun providesRepoDao(db: AppDatabase): RepoDao = db.repoDao() + + @Provides + @Singleton + fun providesOnlineDao(db: AppDatabase): OnlineDao = db.onlineDao() + + @Provides + @Singleton + fun providesVersionDao(db: AppDatabase): VersionDao = db.versionDao() + + @Provides + @Singleton + fun providesLocalDao(db: AppDatabase): LocalDao = db.localDao() + + @Provides + @Singleton + fun providesJoinDao(db: AppDatabase): JoinDao = db.joinDao() +} \ No newline at end of file diff --git a/app/src/main/kotlin/com/dergoogler/mmrl/database/entity/Local.kt b/app/src/main/kotlin/com/dergoogler/mmrl/database/entity/Local.kt new file mode 100644 index 00000000..e9f69504 --- /dev/null +++ b/app/src/main/kotlin/com/dergoogler/mmrl/database/entity/Local.kt @@ -0,0 +1,49 @@ +package com.dergoogler.mmrl.database.entity + +import androidx.room.Entity +import androidx.room.PrimaryKey +import com.dergoogler.mmrl.model.local.LocalModule +import com.dergoogler.mmrl.model.local.State + +@Entity(tableName = "localModules") +data class LocalModuleEntity( + @PrimaryKey val id: String, + val name: String, + val version: String, + val versionCode: Int, + val author: String, + val description: String, + val state: String, + val updateJson: String, + val lastUpdated: Long +) { + constructor(original: LocalModule) : this( + id = original.id, + name = original.name, + version = original.version, + versionCode = original.versionCode, + author = original.author, + description = original.description, + state = original.state.name, + updateJson = original.updateJson, + lastUpdated = original.lastUpdated + ) + + fun toModule() = LocalModule( + id = id, + name = name, + version = version, + versionCode = versionCode, + author = author, + description = description, + updateJson = updateJson, + state = State.valueOf(state), + lastUpdated = lastUpdated + ) +} + +@Entity(tableName = "localModules_updatable") +data class LocalModuleUpdatable( + @PrimaryKey val id: String, + val updatable: Boolean +) \ No newline at end of file diff --git a/app/src/main/kotlin/com/dergoogler/mmrl/database/entity/Online.kt b/app/src/main/kotlin/com/dergoogler/mmrl/database/entity/Online.kt new file mode 100644 index 00000000..84f3d347 --- /dev/null +++ b/app/src/main/kotlin/com/dergoogler/mmrl/database/entity/Online.kt @@ -0,0 +1,203 @@ +package com.dergoogler.mmrl.database.entity + +import androidx.room.Embedded +import androidx.room.Entity +import androidx.room.TypeConverters +import com.dergoogler.mmrl.model.online.ModuleFeatures +import com.dergoogler.mmrl.model.online.ModuleNote +import com.dergoogler.mmrl.model.online.ModuleRoot +import com.dergoogler.mmrl.model.online.OnlineModule +import com.dergoogler.mmrl.model.online.TrackJson +import com.squareup.moshi.Json + +@Entity(tableName = "onlineModules", primaryKeys = ["id", "repoUrl"]) +data class OnlineModuleEntity( + val id: String, + val repoUrl: String, + val name: String, + val version: String, + val versionCode: Int, + val author: String, + val description: String, + + val maxApi: Int? = null, + val minApi: Int? = null, + + val size: Int? = null, + val categories: List? = null, + val icon: String? = null, + val homepage: String? = null, + val donate: String? = null, + val support: String? = null, + val cover: String? = null, + val screenshots: List? = null, + val license: String? = "", + val readme: String? = null, + val require: List? = null, + val verified: Boolean? = null, + + @Embedded val root: ModuleRootEntity? = null, + @Embedded val note: ModuleNoteEntity? = null, + @Embedded val features: ModuleFeaturesEntity? = null, + @Embedded val track: TrackJsonEntity +) { + constructor( + original: OnlineModule, + repoUrl: String + ) : this( + id = original.id, + repoUrl = repoUrl, + name = original.name, + version = original.version, + versionCode = original.versionCode, + author = original.author, + description = original.description, + track = TrackJsonEntity(original.track), + note = ModuleNoteEntity(original.note), + root = ModuleRootEntity(original.root), + features = ModuleFeaturesEntity(original.features), + maxApi = original.maxApi, + minApi = original.minApi, + size = original.size, + categories = original.categories, + icon = original.icon, + homepage = original.homepage, + donate = original.donate, + support = original.support, + cover = original.cover, + screenshots = original.screenshots, + license = original.license, + readme = original.readme, + require = original.require, + verified = original.verified + ) + + fun toModule() = OnlineModule( + id = id, + name = name, + version = version, + versionCode = versionCode, + author = author, + description = description, + track = track.toTrack(), + note = note?.toNote(), + root = root?.toRoot(), + features = features?.toFeatures(), + versions = listOf(), + maxApi = maxApi, + minApi = minApi, + size = size, + categories = categories, + icon = icon, + homepage = homepage, + donate = donate, + support = support, + cover = cover, + screenshots = screenshots, + license = license, + readme = readme, + require = require, + verified = verified + ) +} + +@Entity(tableName = "track") +@TypeConverters +data class TrackJsonEntity( + val type: String, + val added: Float? = 0f, + val source: String, + val antifeatures: List? = null, +) { + constructor(original: TrackJson) : this( + type = original.type.name, + added = original.added, + source = original.source, + antifeatures = original.antifeatures, + ) + + fun toTrack() = TrackJson( + typeName = type, + added = added, + source = source, + antifeatures = antifeatures + ) +} + +@Entity(tableName = "note") +@TypeConverters +data class ModuleNoteEntity( + val title: String? = null, + val message: String? = null, +) { + constructor(original: ModuleNote?) : this( + title = original?.title, + message = original?.message, + ) + + fun toNote() = ModuleNote( + title = title, + message = message, + ) +} + +@Entity(tableName = "root") +@TypeConverters +data class ModuleRootEntity( + val magisk: String? = null, + val kernelsu: String? = null, + val apatch: String? = null, +) { + constructor(original: ModuleRoot?) : this( + magisk = original?.magisk, + kernelsu = original?.kernelsu, + apatch = original?.apatch, + ) + + fun toRoot() = ModuleRoot( + magisk = magisk, + kernelsu = kernelsu, + apatch = apatch, + ) +} + +@Entity(tableName = "root") +@TypeConverters +data class ModuleFeaturesEntity( + val service: Boolean? = false, + @Json(name = "post_fs_data") val postFsData: Boolean? = false, + val resetprop: Boolean? = false, + val sepolicy: Boolean? = false, + val zygisk: Boolean? = false, + val apks: Boolean? = false, + val webroot: Boolean? = false, + @Json(name = "post_mount") val postMount: Boolean? = false, + @Json(name = "boot_completed") val bootCompleted: Boolean? = false, +// val modconf: Boolean? = false, +) { + constructor(original: ModuleFeatures?) : this( + service = original?.service, + postFsData = original?.postFsData, + resetprop = original?.resetprop, + sepolicy = original?.sepolicy, + zygisk = original?.zygisk, + apks = original?.apks, + webroot = original?.webroot, + postMount = original?.postMount, + bootCompleted = original?.bootCompleted, +// modconf = original?.modconf, + ) + + fun toFeatures() = ModuleFeatures( + service = service, + postFsData = postFsData, + resetprop = resetprop, + sepolicy = sepolicy, + zygisk = zygisk, + apks = apks, + webroot = webroot, + postMount = postMount, + bootCompleted = bootCompleted, +// modconf = modconf, + ) +} \ No newline at end of file diff --git a/app/src/main/kotlin/com/dergoogler/mmrl/database/entity/Repo.kt b/app/src/main/kotlin/com/dergoogler/mmrl/database/entity/Repo.kt new file mode 100644 index 00000000..9bd2c004 --- /dev/null +++ b/app/src/main/kotlin/com/dergoogler/mmrl/database/entity/Repo.kt @@ -0,0 +1,63 @@ +package com.dergoogler.mmrl.database.entity + +import androidx.room.Embedded +import androidx.room.Entity +import androidx.room.PrimaryKey +import com.dergoogler.mmrl.model.online.ModulesJson + +@Entity(tableName = "repos") +data class Repo( + @PrimaryKey val url: String, + val name: String = url, + val enable: Boolean = true, + val submission: String? = null, + val website: String? = null, + val donate: String? = null, + val support: String? = null, + @Embedded val metadata: RepoMetadata = RepoMetadata.default() +) { + val isCompatible get() = metadata.version == ModulesJson.CURRENT_VERSION + + override fun equals(other: Any?): Boolean { + return when (other) { + is Repo -> url == other.url + else -> false + } + } + + override fun hashCode(): Int { + return url.hashCode() + } + + fun copy(modulesJson: ModulesJson) = copy( + name = modulesJson.name, + website = modulesJson.website, + support = modulesJson.support, + donate = modulesJson.donate, + submission = modulesJson.submission, + metadata = RepoMetadata( + version = modulesJson.metadata.version, + timestamp = modulesJson.metadata.timestamp, + size = modulesJson.modules.size + ) + ) + + companion object { + fun String.toRepo() = Repo(url = this) + } +} + +@Entity(tableName = "metadata") +data class RepoMetadata( + val version: Int, + val timestamp: Float, + val size: Int +) { + companion object { + fun default() = RepoMetadata( + version = ModulesJson.CURRENT_VERSION, + timestamp = 0f, + size = 0 + ) + } +} diff --git a/app/src/main/kotlin/com/dergoogler/mmrl/database/entity/Version.kt b/app/src/main/kotlin/com/dergoogler/mmrl/database/entity/Version.kt new file mode 100644 index 00000000..1f5f56ac --- /dev/null +++ b/app/src/main/kotlin/com/dergoogler/mmrl/database/entity/Version.kt @@ -0,0 +1,38 @@ +package com.dergoogler.mmrl.database.entity + +import androidx.room.Entity +import com.dergoogler.mmrl.model.online.VersionItem + +@Entity(tableName = "versions", primaryKeys = ["id", "repoUrl", "versionCode"]) +data class VersionItemEntity( + val id: String, + val repoUrl: String, + val timestamp: Float, + val version: String, + val versionCode: Int, + val zipUrl: String, + val changelog: String +) { + constructor( + original: VersionItem, + id: String, + repoUrl: String + ) : this( + id = id, + repoUrl= repoUrl, + timestamp = original.timestamp, + version = original.version, + versionCode = original.versionCode, + zipUrl = original.zipUrl, + changelog = original.changelog + ) + + fun toItem() = VersionItem( + repoUrl= repoUrl, + timestamp = timestamp, + version = version, + versionCode = versionCode, + zipUrl = zipUrl, + changelog = changelog + ) +} \ No newline at end of file diff --git a/app/src/main/kotlin/com/dergoogler/mmrl/datastore/UserPreferencesCompat.kt b/app/src/main/kotlin/com/dergoogler/mmrl/datastore/UserPreferencesCompat.kt new file mode 100644 index 00000000..562f3190 --- /dev/null +++ b/app/src/main/kotlin/com/dergoogler/mmrl/datastore/UserPreferencesCompat.kt @@ -0,0 +1,81 @@ +package com.dergoogler.mmrl.datastore + +import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.runtime.Composable +import com.dergoogler.mmrl.app.Const +import dev.dergoogler.mmrl.compat.BuildCompat +import com.dergoogler.mmrl.datastore.modules.ModulesMenuCompat +import com.dergoogler.mmrl.datastore.repository.RepositoryMenuCompat +import com.dergoogler.mmrl.ui.theme.Colors +import java.io.File + +data class UserPreferencesCompat( + val workingMode: WorkingMode, + val darkMode: DarkMode, + val themeColor: Int, + val deleteZipFile: Boolean, + val useDoh: Boolean, + val downloadPath: File, + val repositoryMenu: RepositoryMenuCompat, + val modulesMenu: ModulesMenuCompat +) { + constructor(original: UserPreferences) : this( + workingMode = original.workingMode, + darkMode = original.darkMode, + themeColor = original.themeColor, + deleteZipFile = original.deleteZipFile, + useDoh = original.useDoh, + downloadPath = original.downloadPath.ifEmpty{ Const.PUBLIC_DOWNLOADS.absolutePath }.let(::File), + repositoryMenu = when { + original.hasRepositoryMenu() -> RepositoryMenuCompat(original.repositoryMenu) + else -> RepositoryMenuCompat.default() + }, + modulesMenu = when { + original.hasModulesMenu() -> ModulesMenuCompat(original.modulesMenu) + else -> ModulesMenuCompat.default() + } + ) + + @Composable + fun isDarkMode() = when (darkMode) { + DarkMode.ALWAYS_OFF -> false + DarkMode.ALWAYS_ON -> true + else -> isSystemInDarkTheme() + } + + fun toProto(): UserPreferences = UserPreferences.newBuilder() + .setWorkingMode(workingMode) + .setDarkMode(darkMode) + .setThemeColor(themeColor) + .setDeleteZipFile(deleteZipFile) + .setUseDoh(useDoh) + .setDownloadPath(downloadPath.path) + .setRepositoryMenu(repositoryMenu.toProto()) + .setModulesMenu(modulesMenu.toProto()) + .build() + + companion object { + fun default() = UserPreferencesCompat( + workingMode = WorkingMode.FIRST_SETUP, + darkMode = DarkMode.FOLLOW_SYSTEM, + themeColor = if (BuildCompat.atLeastS) Colors.Dynamic.id else Colors.Pourville.id, + deleteZipFile = false, + useDoh = false, + downloadPath = Const.PUBLIC_DOWNLOADS, + repositoryMenu = RepositoryMenuCompat.default(), + modulesMenu = ModulesMenuCompat.default() + ) + + val WorkingMode.isRoot: Boolean get() { + return this == WorkingMode.MODE_ROOT ||this == WorkingMode.MODE_SHIZUKU + } + + val WorkingMode.isNonRoot: Boolean get() { + return this == WorkingMode.MODE_NON_ROOT + } + + val WorkingMode.isSetup: Boolean get() { + return this == WorkingMode.FIRST_SETUP + } + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/com/dergoogler/mmrl/datastore/UserPreferencesDataSource.kt b/app/src/main/kotlin/com/dergoogler/mmrl/datastore/UserPreferencesDataSource.kt new file mode 100644 index 00000000..2a4111fa --- /dev/null +++ b/app/src/main/kotlin/com/dergoogler/mmrl/datastore/UserPreferencesDataSource.kt @@ -0,0 +1,79 @@ +package com.dergoogler.mmrl.datastore + +import androidx.datastore.core.DataStore +import com.dergoogler.mmrl.datastore.modules.ModulesMenuCompat +import com.dergoogler.mmrl.datastore.repository.RepositoryMenuCompat +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import java.io.File +import javax.inject.Inject + +class UserPreferencesDataSource @Inject constructor( + private val userPreferences: DataStore +) { + val data get() = userPreferences.data + + suspend fun setWorkingMode(value: WorkingMode) = withContext(Dispatchers.IO) { + userPreferences.updateData { + it.copy( + workingMode = value + ) + } + } + + suspend fun setDarkTheme(value: DarkMode) = withContext(Dispatchers.IO) { + userPreferences.updateData { + it.copy( + darkMode = value + ) + } + } + + suspend fun setThemeColor(value: Int) = withContext(Dispatchers.IO) { + userPreferences.updateData { + it.copy( + themeColor = value + ) + } + } + + suspend fun setDeleteZipFile(value: Boolean) = withContext(Dispatchers.IO) { + userPreferences.updateData { + it.copy( + deleteZipFile = value + ) + } + } + + suspend fun setUseDoh(value: Boolean) = withContext(Dispatchers.IO) { + userPreferences.updateData { + it.copy( + useDoh = value + ) + } + } + + suspend fun setDownloadPath(value: File) = withContext(Dispatchers.IO) { + userPreferences.updateData { + it.copy( + downloadPath = value + ) + } + } + + suspend fun setRepositoryMenu(value: RepositoryMenuCompat) = withContext(Dispatchers.IO) { + userPreferences.updateData { + it.copy( + repositoryMenu = value + ) + } + } + + suspend fun setModulesMenu(value: ModulesMenuCompat) = withContext(Dispatchers.IO) { + userPreferences.updateData { + it.copy( + modulesMenu = value + ) + } + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/com/dergoogler/mmrl/datastore/UserPreferencesSerializer.kt b/app/src/main/kotlin/com/dergoogler/mmrl/datastore/UserPreferencesSerializer.kt new file mode 100644 index 00000000..6b84cc34 --- /dev/null +++ b/app/src/main/kotlin/com/dergoogler/mmrl/datastore/UserPreferencesSerializer.kt @@ -0,0 +1,23 @@ +package com.dergoogler.mmrl.datastore + +import androidx.datastore.core.CorruptionException +import androidx.datastore.core.Serializer +import com.google.protobuf.InvalidProtocolBufferException +import java.io.InputStream +import java.io.OutputStream +import javax.inject.Inject + +class UserPreferencesSerializer @Inject constructor() : Serializer { + override val defaultValue = UserPreferencesCompat.default() + + override suspend fun readFrom(input: InputStream) = + try { + UserPreferences.parseFrom(input).let(::UserPreferencesCompat) + } catch (e: InvalidProtocolBufferException) { + throw CorruptionException("cannot read proto", e) + } + + override suspend fun writeTo(t: UserPreferencesCompat, output: OutputStream) { + t.toProto().writeTo(output) + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/com/dergoogler/mmrl/datastore/di/DataStoreModule.kt b/app/src/main/kotlin/com/dergoogler/mmrl/datastore/di/DataStoreModule.kt new file mode 100644 index 00000000..77b5cd2f --- /dev/null +++ b/app/src/main/kotlin/com/dergoogler/mmrl/datastore/di/DataStoreModule.kt @@ -0,0 +1,30 @@ +package com.dergoogler.mmrl.datastore.di + +import android.content.Context +import androidx.datastore.core.DataStore +import androidx.datastore.core.DataStoreFactory +import androidx.datastore.dataStoreFile +import com.dergoogler.mmrl.datastore.UserPreferencesCompat +import com.dergoogler.mmrl.datastore.UserPreferencesSerializer +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.android.qualifiers.ApplicationContext +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +object DataStoreModule { + @Provides + @Singleton + fun providesUserPreferencesDataStore( + @ApplicationContext context: Context, + userPreferencesSerializer: UserPreferencesSerializer + ): DataStore = + DataStoreFactory.create( + serializer = userPreferencesSerializer + ) { + context.dataStoreFile("user_preferences.pb") + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/com/dergoogler/mmrl/datastore/modules/ModulesMenuCompat.kt b/app/src/main/kotlin/com/dergoogler/mmrl/datastore/modules/ModulesMenuCompat.kt new file mode 100644 index 00000000..88a64fa4 --- /dev/null +++ b/app/src/main/kotlin/com/dergoogler/mmrl/datastore/modules/ModulesMenuCompat.kt @@ -0,0 +1,33 @@ +package com.dergoogler.mmrl.datastore.modules + +import com.dergoogler.mmrl.datastore.repository.Option + +data class ModulesMenuCompat( + val option: Option, + val descending: Boolean, + val pinEnabled: Boolean, + val showUpdatedTime: Boolean +) { + constructor(original: ModulesMenu) : this( + option = original.option, + descending = original.descending, + pinEnabled = original.pinEnabled, + showUpdatedTime = original.showUpdatedTime + ) + + fun toProto(): ModulesMenu = ModulesMenu.newBuilder() + .setOption(option) + .setDescending(descending) + .setPinEnabled(pinEnabled) + .setShowUpdatedTime(showUpdatedTime) + .build() + + companion object { + fun default() = ModulesMenuCompat( + option = Option.NAME, + descending = false, + pinEnabled = false, + showUpdatedTime = true + ) + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/com/dergoogler/mmrl/datastore/repository/RepositoryMenuCompat.kt b/app/src/main/kotlin/com/dergoogler/mmrl/datastore/repository/RepositoryMenuCompat.kt new file mode 100644 index 00000000..6c083bf3 --- /dev/null +++ b/app/src/main/kotlin/com/dergoogler/mmrl/datastore/repository/RepositoryMenuCompat.kt @@ -0,0 +1,52 @@ +package com.dergoogler.mmrl.datastore.repository + +data class RepositoryMenuCompat( + val option: Option, + val descending: Boolean, + val pinInstalled: Boolean, + val pinUpdatable: Boolean, + val showIcon: Boolean, + val showLicense: Boolean, + val showUpdatedTime: Boolean, + val showCover: Boolean, + val showVerified: Boolean, + val showAntiFeatures: Boolean, +) { + constructor(original: RepositoryMenu) : this( + option = original.option, + descending = original.descending, + pinInstalled = original.pinInstalled, + pinUpdatable = original.pinUpdatable, + showIcon = original.showIcon, + showLicense = original.showLicense, + showUpdatedTime = original.showUpdatedTime, + showCover = original.showCover, + showVerified = original.showVerified, + showAntiFeatures = original.showAntiFeatures + ) + + fun toProto(): RepositoryMenu = RepositoryMenu.newBuilder() + .setOption(option) + .setDescending(descending) + .setPinInstalled(pinInstalled) + .setPinUpdatable(pinUpdatable) + .setShowIcon(showIcon) + .setShowLicense(showLicense) + .setShowUpdatedTime(showUpdatedTime) + .setShowCover(showCover) + .setShowVerified(showVerified).setShowAntiFeatures(showAntiFeatures) + .build() + + companion object { + fun default() = RepositoryMenuCompat( + option = Option.NAME, + descending = false, + pinInstalled = false, + pinUpdatable = true, + showIcon = true, + showLicense = true, + showUpdatedTime = true, + showCover = true, showVerified = true, showAntiFeatures = true + ) + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/com/dergoogler/mmrl/model/json/License.kt b/app/src/main/kotlin/com/dergoogler/mmrl/model/json/License.kt new file mode 100644 index 00000000..e2fe995a --- /dev/null +++ b/app/src/main/kotlin/com/dergoogler/mmrl/model/json/License.kt @@ -0,0 +1,15 @@ +package com.dergoogler.mmrl.model.json + +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +data class License( + val licenseText: String, + val name: String, + val licenseId: String, + val seeAlso: List, + val isOsiApproved: Boolean, + val isFsfLibre: Boolean = false, +) { + fun hasLabel() = isFsfLibre || isOsiApproved +} diff --git a/app/src/main/kotlin/com/dergoogler/mmrl/model/json/UpdateJson.kt b/app/src/main/kotlin/com/dergoogler/mmrl/model/json/UpdateJson.kt new file mode 100644 index 00000000..b1788a4e --- /dev/null +++ b/app/src/main/kotlin/com/dergoogler/mmrl/model/json/UpdateJson.kt @@ -0,0 +1,61 @@ +package com.dergoogler.mmrl.model.json + +import com.dergoogler.mmrl.model.online.VersionItem +import com.dergoogler.mmrl.network.NetworkUtils +import com.squareup.moshi.JsonClass +import com.squareup.moshi.Moshi +import com.squareup.moshi.adapter + +@JsonClass(generateAdapter = true) +data class UpdateJson( + val version: String, + val versionCode: Int, + val zipUrl: String, + val size: Int = 0, + val changelog: String = "" +) { + fun toItemOrNull(timestamp: Float): VersionItem? { + if (!NetworkUtils.isUrl(zipUrl)) return null + + val changelog = when { + !NetworkUtils.isUrl(changelog) -> "" + NetworkUtils.isBlobUrl(changelog) -> "" + else -> changelog + } + + return VersionItem( + timestamp = timestamp, + version = version, + versionCode = versionCode, + zipUrl = zipUrl, + size = size, + changelog = changelog + ) + } + + companion object { + suspend fun loadToVersionItem(url: String): VersionItem? { + if (!NetworkUtils.isUrl(url)) return null + + val result = NetworkUtils.request(url) { body, headers -> + val adapter = Moshi.Builder() + .build() + .adapter() + + adapter.fromJson(body.string()) to headers + } + + if (result.isSuccess) { + val (json, headers) = result.getOrThrow() + if (json != null) { + val t = headers.getInstant("Last-Modified")?.toEpochMilli() + val timestamp = (t ?: System.currentTimeMillis()) / 1000f + + return json.toItemOrNull(timestamp) + } + } + + return null + } + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/com/dergoogler/mmrl/model/local/LocalModule.kt b/app/src/main/kotlin/com/dergoogler/mmrl/model/local/LocalModule.kt new file mode 100644 index 00000000..80157c38 --- /dev/null +++ b/app/src/main/kotlin/com/dergoogler/mmrl/model/local/LocalModule.kt @@ -0,0 +1,21 @@ +package com.dergoogler.mmrl.model.local + +import com.dergoogler.mmrl.utils.Utils +import dev.dergoogler.mmrl.compat.content.LocalModule + +typealias LocalModule = LocalModule + +val LocalModule.versionDisplay get() = Utils.getVersionDisplay(version, versionCode) + +fun LocalModule.Companion.example() = + LocalModule( + id = "local_example", + name = "Example", + version = "2022.08.16", + versionCode = 1703, + author = "Sanmer", + description = "This is an example!", + updateJson = "", + state = State.ENABLE, + lastUpdated = 0L + ) \ No newline at end of file diff --git a/app/src/main/kotlin/com/dergoogler/mmrl/model/local/State.kt b/app/src/main/kotlin/com/dergoogler/mmrl/model/local/State.kt new file mode 100644 index 00000000..145f4e17 --- /dev/null +++ b/app/src/main/kotlin/com/dergoogler/mmrl/model/local/State.kt @@ -0,0 +1,5 @@ +package com.dergoogler.mmrl.model.local + +import dev.dergoogler.mmrl.compat.content.State + +typealias State = State \ No newline at end of file diff --git a/app/src/main/kotlin/com/dergoogler/mmrl/model/online/ModuleFeatures.kt b/app/src/main/kotlin/com/dergoogler/mmrl/model/online/ModuleFeatures.kt new file mode 100644 index 00000000..08c4dc63 --- /dev/null +++ b/app/src/main/kotlin/com/dergoogler/mmrl/model/online/ModuleFeatures.kt @@ -0,0 +1,26 @@ +package com.dergoogler.mmrl.model.online + + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import io.github.z4kn4fein.semver.constraints.toConstraint +import io.github.z4kn4fein.semver.satisfies +import io.github.z4kn4fein.semver.toVersionOrNull + +@JsonClass(generateAdapter = true) +data class ModuleFeatures( + val service: Boolean? = null, + @Json(name = "post_fs_data") val postFsData: Boolean? = null, + val resetprop: Boolean? = null, + val sepolicy: Boolean? = null, + val zygisk: Boolean? = null, + val apks: Boolean? = null, + val webroot: Boolean? = null, + @Json(name = "post_mount") val postMount: Boolean? = null, + @Json(name = "boot_completed") val bootCompleted: Boolean? = null, +// val modconf: Boolean? = false, +) { + fun isNotEmpty() = + service != null || postFsData != null || resetprop != null || sepolicy != null || zygisk != null || apks != null || webroot != null || postMount != null || bootCompleted != null //|| modconf != null +} + diff --git a/app/src/main/kotlin/com/dergoogler/mmrl/model/online/ModuleNote.kt b/app/src/main/kotlin/com/dergoogler/mmrl/model/online/ModuleNote.kt new file mode 100644 index 00000000..fc0fec09 --- /dev/null +++ b/app/src/main/kotlin/com/dergoogler/mmrl/model/online/ModuleNote.kt @@ -0,0 +1,14 @@ +package com.dergoogler.mmrl.model.online + +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +data class ModuleNote( + val title: String? = null, + val message: String? = null, +) { + fun isNotEmpty() = + title.orEmpty().isNotEmpty() || message.orEmpty() + .isNotEmpty() +} + diff --git a/app/src/main/kotlin/com/dergoogler/mmrl/model/online/ModuleRoot.kt b/app/src/main/kotlin/com/dergoogler/mmrl/model/online/ModuleRoot.kt new file mode 100644 index 00000000..c23f9c7c --- /dev/null +++ b/app/src/main/kotlin/com/dergoogler/mmrl/model/online/ModuleRoot.kt @@ -0,0 +1,99 @@ +package com.dergoogler.mmrl.model.online + +import com.squareup.moshi.JsonClass +import io.github.z4kn4fein.semver.constraints.toConstraint +import io.github.z4kn4fein.semver.satisfies +import io.github.z4kn4fein.semver.toVersionOrNull +import java.util.Locale + +@JsonClass(generateAdapter = true) +data class ModuleRoot( + val magisk: String? = null, + val kernelsu: String? = null, + val apatch: String? = null, +) { + private fun isNotEmpty() = + magisk.orEmpty().isNotEmpty() || kernelsu.orEmpty().isNotEmpty() || apatch.orEmpty() + .isNotEmpty() + + fun isNotSupported(version: String): Boolean { + val parsedRootProvider = version.replace(Regex("^.*:"), "").lowercase(Locale.ROOT) + val parsedVersion = version.replace(Regex(":.*$"), "").toVersionOrNull(strict = false) + + if (isNotEmpty()) { + if (parsedVersion != null) { + return when (parsedRootProvider) { + "magisk" -> { + parsedVersion satisfies magisk!!.toConstraint() + } + + "kernelsu" -> { + parsedVersion satisfies kernelsu!!.toConstraint() + } + + "apatch" -> { + parsedVersion satisfies apatch!!.toConstraint() + } + + else -> true + } + } else { + return true + } + } else { + return true + } + } +} + +data class Version(val major: Int, val minor: Int, val patch: Int) : Comparable { + companion object { + fun parse(version: String): Version { + // Remove any prefix "v" and split by '.' + val cleanedVersion = version.removePrefix("v") + val parts = cleanedVersion.split(".") + + // Extract major, minor, and patch (default to 0 if not provided) + val major = parts.getOrNull(0)?.toInt() ?: 0 + val minor = parts.getOrNull(1)?.toInt() ?: 0 + val patch = parts.getOrNull(2)?.toInt() ?: 0 + + return Version(major, minor, patch) + } + } + + override fun compareTo(other: Version): Int { + return when { + major != other.major -> major - other.major + minor != other.minor -> minor - other.minor + else -> patch - other.patch + } + } +} + +fun isVersionSatisfies(version: String, constraint: String): Boolean { + val versionParts = constraint.split(" ", limit = 2) + val operator = versionParts[0] + val versionToCompare = Version.parse(versionParts[1]) + + return when (operator) { + ">=" -> Version.parse(version) >= versionToCompare + ">" -> Version.parse(version) > versionToCompare + "<=" -> Version.parse(version) <= versionToCompare + "<" -> Version.parse(version) < versionToCompare + "==" -> Version.parse(version) == versionToCompare + "!=" -> Version.parse(version) != versionToCompare + else -> throw IllegalArgumentException("Unknown operator: $operator") + } +} + +fun main() { + val version = "v1.2.3" + val constraint = ">= 1.0.0" + + if (isVersionSatisfies(version, constraint)) { + println("$version satisfies the constraint $constraint") + } else { + println("$version does not satisfy the constraint $constraint") + } +} diff --git a/app/src/main/kotlin/com/dergoogler/mmrl/model/online/ModulesJson.kt b/app/src/main/kotlin/com/dergoogler/mmrl/model/online/ModulesJson.kt new file mode 100644 index 00000000..752994d0 --- /dev/null +++ b/app/src/main/kotlin/com/dergoogler/mmrl/model/online/ModulesJson.kt @@ -0,0 +1,31 @@ +package com.dergoogler.mmrl.model.online + +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +data class ModulesJson( + val name: String, + val submission: String? = null, + val website: String? = null, + val donate: String? = null, + val support: String? = null, + val metadata: ModulesJsonMetadata = ModulesJsonMetadata.default(), + val modules: List +) { + companion object { + const val CURRENT_VERSION = 1 + } +} + +@JsonClass(generateAdapter = true) +data class ModulesJsonMetadata( + val version: Int, + val timestamp: Float +) { + companion object { + fun default() = ModulesJsonMetadata( + version = 0, + timestamp = 0f + ) + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/com/dergoogler/mmrl/model/online/OnlineModule.kt b/app/src/main/kotlin/com/dergoogler/mmrl/model/online/OnlineModule.kt new file mode 100644 index 00000000..f34772e9 --- /dev/null +++ b/app/src/main/kotlin/com/dergoogler/mmrl/model/online/OnlineModule.kt @@ -0,0 +1,87 @@ +package com.dergoogler.mmrl.model.online + +import com.dergoogler.mmrl.utils.Utils +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +data class OnlineModule( + val id: String, + val name: String, + val version: String, + val versionCode: Int, + val author: String, + val description: String, + val track: TrackJson, + val versions: List, + + val maxApi: Int? = null, + val minApi: Int? = null, + + val size: Int? = null, + val categories: List? = null, + val icon: String? = null, + val homepage: String? = null, + val donate: String? = null, + val support: String? = null, + val cover: String? = null, + val screenshots: List? = null, + val license: String? = "", + val readme: String? = null, + val require: List? = null, + val verified: Boolean? = null, + + val root: ModuleRoot? =null, + val note: ModuleNote? = null, + val features: ModuleFeatures? = null +) { + val versionDisplay get() = Utils.getVersionDisplay(version, versionCode) + val hasLicense + get() = license.orEmpty().isNotBlank() + && license.orEmpty().uppercase() != "UNKNOWN" + + val hasRequire = require.orEmpty().isNotEmpty() + val hasIcon = icon.orEmpty().isNotEmpty() + val hasHomepage = homepage.orEmpty().isNotEmpty() + val hasDonate = donate.orEmpty().isNotEmpty() + val hasSupport = support.orEmpty().isNotEmpty() + val hasCover = cover.orEmpty().isNotEmpty() + val hasScreenshots = screenshots.orEmpty().isNotEmpty() + val hasRoot = root != null + val hasNote = note != null + val hasReadme = readme.orEmpty().isNotEmpty() + val hasCategories = categories.orEmpty().isNotEmpty() + val hasMaxApi = maxApi != null + val hasMinApi = minApi != null + val hasSize = size != null + val isVerified = verified != null && verified + + + override fun equals(other: Any?): Boolean { + return when (other) { + is OnlineModule -> id == other.id + else -> false + } + } + + override fun hashCode(): Int { + return id.hashCode() + } + + companion object { + fun example() = OnlineModule( + id = "online_example", + name = "Example", + version = "2022.08.16", + versionCode = 1703, + author = "Sanmer", + description = "This is an example!", + license = "GPL-3.0", + track = TrackJson( + typeName = "ONLINE_JSON", + added = 0f, + antifeatures = emptyList() + ), + versions = emptyList() + ) + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/com/dergoogler/mmrl/model/online/TrackJson.kt b/app/src/main/kotlin/com/dergoogler/mmrl/model/online/TrackJson.kt new file mode 100644 index 00000000..116c4b14 --- /dev/null +++ b/app/src/main/kotlin/com/dergoogler/mmrl/model/online/TrackJson.kt @@ -0,0 +1,24 @@ +package com.dergoogler.mmrl.model.online + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +data class TrackJson( + @Json(name = "type") val typeName: String, + val added: Float? = 0f, + val source: String = "", + val antifeatures: List? = null, +) { + val type = TrackType.valueOf(typeName) + val hasAntifeatures = antifeatures.orEmpty().isNotEmpty() +} + +enum class TrackType { + UNKNOWN, + ONLINE_JSON, + ONLINE_ZIP, + GIT, + LOCAL_JSON, + LOCAL_ZIP, +} \ No newline at end of file diff --git a/app/src/main/kotlin/com/dergoogler/mmrl/model/online/VersionItem.kt b/app/src/main/kotlin/com/dergoogler/mmrl/model/online/VersionItem.kt new file mode 100644 index 00000000..6283efff --- /dev/null +++ b/app/src/main/kotlin/com/dergoogler/mmrl/model/online/VersionItem.kt @@ -0,0 +1,19 @@ +package com.dergoogler.mmrl.model.online + +import com.dergoogler.mmrl.utils.Utils +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +data class VersionItem( + @Json(ignore = true) val repoUrl: String = "", + val timestamp: Float, + val version: String, + val versionCode: Int, + val zipUrl: String, + val size: Int? = null, + val changelog: String = "" +) { + val versionDisplay get() = Utils.getVersionDisplay(version, versionCode) + val hasSize = size != null +} \ No newline at end of file diff --git a/app/src/main/kotlin/com/dergoogler/mmrl/model/state/OnlineState.kt b/app/src/main/kotlin/com/dergoogler/mmrl/model/state/OnlineState.kt new file mode 100644 index 00000000..7cd00fc6 --- /dev/null +++ b/app/src/main/kotlin/com/dergoogler/mmrl/model/state/OnlineState.kt @@ -0,0 +1,42 @@ +package com.dergoogler.mmrl.model.state + +import com.dergoogler.mmrl.model.local.LocalModule +import com.dergoogler.mmrl.model.online.OnlineModule + +data class OnlineState( + val installed: Boolean, + val updatable: Boolean, + val hasLicense: Boolean, + val lastUpdated: Float +) { + @Suppress("FloatingPointLiteralPrecision") + companion object { + fun OnlineModule.createState( + local: LocalModule?, + hasUpdatableTag: Boolean, + ): OnlineState { + val installed = local != null && local.id == id + && local.author == author + + val updatable = if (installed && hasUpdatableTag) { + local!!.versionCode < versionCode + } else { + false + } + + return OnlineState( + installed = installed, + updatable = updatable, + hasLicense = license.orEmpty().isNotBlank(), + lastUpdated = versions.firstOrNull()?.timestamp ?: 1473339588.0f + ) + } + + fun example() = OnlineState( + installed = true, + updatable = false, + hasLicense = true, + lastUpdated = 1660640580.0f + ) + } +} diff --git a/app/src/main/kotlin/com/dergoogler/mmrl/model/state/RepoState.kt b/app/src/main/kotlin/com/dergoogler/mmrl/model/state/RepoState.kt new file mode 100644 index 00000000..1b830ef3 --- /dev/null +++ b/app/src/main/kotlin/com/dergoogler/mmrl/model/state/RepoState.kt @@ -0,0 +1,49 @@ +package com.dergoogler.mmrl.model.state + +import androidx.compose.runtime.Immutable +import com.dergoogler.mmrl.database.entity.Repo +import com.dergoogler.mmrl.database.entity.RepoMetadata + +@Immutable +data class RepoState( + val url: String, + val name: String, + val enable: Boolean, + val compatible: Boolean, + val version: Int, + val timestamp: Float, + val submission: String? = null, + val website: String? = null, + val donate: String? = null, + val support: String? = null, + val size: Int +) { + constructor(repo: Repo) : this( + url = repo.url, + name = repo.name, + enable = repo.enable, + compatible = repo.isCompatible, + version = repo.metadata.version, + timestamp = repo.metadata.timestamp, + website = repo.website, + support = repo.support, + submission = repo.submission, + donate = repo.donate, + size = repo.metadata.size + ) + + fun toRepo() = Repo( + url = url, + name = name, + enable = enable, + website = website, + support = support, + submission = submission, + donate = donate, + metadata = RepoMetadata( + version = version, + timestamp = timestamp, + size = size + ) + ) +} \ No newline at end of file diff --git a/app/src/main/kotlin/com/dergoogler/mmrl/network/DnsResolver.kt b/app/src/main/kotlin/com/dergoogler/mmrl/network/DnsResolver.kt new file mode 100644 index 00000000..d14a06c1 --- /dev/null +++ b/app/src/main/kotlin/com/dergoogler/mmrl/network/DnsResolver.kt @@ -0,0 +1,41 @@ +package com.dergoogler.mmrl.network + +import okhttp3.Dns +import okhttp3.HttpUrl.Companion.toHttpUrl +import okhttp3.OkHttpClient +import okhttp3.dnsoverhttps.DnsOverHttps +import java.net.InetAddress +import java.net.UnknownHostException + +class DnsResolver( + private val client: OkHttpClient, + private val useDoh: Boolean +) : Dns { + private val doh by lazy { + DnsOverHttps.Builder().client(client) + .url("https://cloudflare-dns.com/dns-query".toHttpUrl()) + .bootstrapDnsHosts(listOf( + InetAddress.getByName("162.159.36.1"), + InetAddress.getByName("162.159.46.1"), + InetAddress.getByName("1.1.1.1"), + InetAddress.getByName("1.0.0.1"), + InetAddress.getByName("2606:4700:4700::1111"), + InetAddress.getByName("2606:4700:4700::1001"), + InetAddress.getByName("2606:4700:4700::0064"), + InetAddress.getByName("2606:4700:4700::6400") + )) + .resolvePrivateAddresses(true) + .build() + } + + override fun lookup(hostname: String): List { + if (useDoh) { + try { + return doh.lookup(hostname) + } catch (_: UnknownHostException) { + + } + } + return Dns.SYSTEM.lookup(hostname) + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/com/dergoogler/mmrl/network/NetworkUtils.kt b/app/src/main/kotlin/com/dergoogler/mmrl/network/NetworkUtils.kt new file mode 100644 index 00000000..5cbb3de1 --- /dev/null +++ b/app/src/main/kotlin/com/dergoogler/mmrl/network/NetworkUtils.kt @@ -0,0 +1,151 @@ +package com.dergoogler.mmrl.network + +import com.dergoogler.mmrl.BuildConfig +import com.squareup.moshi.Moshi +import com.squareup.moshi.adapter +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import okhttp3.Cache +import okhttp3.ConnectionSpec +import okhttp3.Headers +import okhttp3.HttpUrl.Companion.toHttpUrlOrNull +import okhttp3.OkHttpClient +import okhttp3.Request +import okhttp3.ResponseBody +import okhttp3.logging.HttpLoggingInterceptor +import retrofit2.Retrofit +import retrofit2.converter.moshi.MoshiConverterFactory +import timber.log.Timber +import java.io.File +import java.io.OutputStream +import java.util.Locale + +object NetworkUtils { + private var useDoh: Boolean = false + private var cacheDirOrNull: File? = null + private val cacheOrNull: Cache? get() = cacheDirOrNull?.let { + Cache(File(it, "okhttp"), 10 * 1024 * 1024) + } + + fun setCacheDir(dir: File) { + cacheDirOrNull = dir + } + + fun setEnableDoh(doh: Boolean) { + useDoh = doh + } + + fun isHTML(text: String) = + "|||" + .toRegex() + .containsMatchIn(text) + + fun isUrl(url: String) = url.toHttpUrlOrNull() != null + + fun isBlobUrl(url: String) = + "https://github.com/[^/]+/[^/]+/blob/.+" + .toRegex() + .matches(url) + + fun createOkHttpClient(): OkHttpClient { + val builder = OkHttpClient.Builder().cache(cacheOrNull) + + if (BuildConfig.DEBUG) { + builder.addInterceptor( + HttpLoggingInterceptor { Timber.i(it) } + .apply { + level = HttpLoggingInterceptor.Level.BASIC + } + ) + } else { + builder.connectionSpecs(listOf(ConnectionSpec.MODERN_TLS)) + } + + builder.dns(DnsResolver(builder.build(), useDoh)) + + builder.addInterceptor { chain -> + val request = chain.request().newBuilder() + request.header("User-Agent", "MMRL/${BuildConfig.VERSION_CODE}") + request.header("Accept-Language", Locale.getDefault().toLanguageTag()) + chain.proceed(request.build()) + } + + return builder.build() + } + + fun createRetrofit(): Retrofit.Builder { + val client = createOkHttpClient() + + return Retrofit.Builder() + .addConverterFactory(MoshiConverterFactory.create()) + .client(client) + } + + suspend inline fun request( + url: String, + crossinline get: (ResponseBody, Headers) -> T + ) = withContext(Dispatchers.IO) { + runRequest(get = get) { + val client = createOkHttpClient() + val request = Request.Builder() + .url(url) + .build() + + client.newCall(request).execute() + } + } + + suspend fun requestString(url: String) = + request( + url = url, + get = { body, _ -> + body.string() + } + ) + + suspend inline fun requestJson( + url: String + ): Result { + val result = request(url) { body, _ -> + val adapter = Moshi.Builder() + .build() + .adapter() + + adapter.fromJson(body.string()) + } + + if (result.isSuccess) { + val json = result.getOrThrow() + if (json != null) return Result.success(json) + } + + return Result.failure(IllegalArgumentException()) + } + + suspend fun downloader( + url: String, + output: OutputStream, + onProgress: (Float) -> Unit + ) = request(url) { body, headers -> + val buffer = ByteArray(2048) + val input = body.byteStream() + + val all = body.contentLength() + var finished: Long = 0 + var readying: Int + + while (input.read(buffer).also { readying = it } != -1) { + output.write(buffer, 0, readying) + finished += readying.toLong() + + val progress = (finished * 1.0 / all).toFloat() + onProgress(progress) + } + + output.flush() + output.close() + input.close() + + return@request headers + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/com/dergoogler/mmrl/network/ResponseExt.kt b/app/src/main/kotlin/com/dergoogler/mmrl/network/ResponseExt.kt new file mode 100644 index 00000000..cf7a47e1 --- /dev/null +++ b/app/src/main/kotlin/com/dergoogler/mmrl/network/ResponseExt.kt @@ -0,0 +1,52 @@ +package com.dergoogler.mmrl.network + +inline fun runRequest( + run: () -> retrofit2.Response +): Result = try { + val response = run() + if (response.isSuccessful) { + val data = response.body() + if (data != null) { + Result.success(data) + }else { + Result.failure(NullPointerException()) + } + } else { + val errorBody = response.errorBody() + val error = errorBody?.string() ?: "404 Not Found" + + if (NetworkUtils.isHTML(error)) { + Result.failure(RuntimeException("404 Not Found")) + } else { + Result.failure(RuntimeException(error)) + } + } +} catch (e: Exception) { + Result.failure(e) +} + +inline fun runRequest( + get: (okhttp3.ResponseBody, okhttp3.Headers) -> T, + run: () -> okhttp3.Response +): Result = try { + val response = run() + val body = response.body + val headers = response.headers + if (response.isSuccessful) { + if (body != null) { + Result.success(get(body, headers)) + } else { + Result.failure(NullPointerException()) + } + } else { + val error = body?.string() ?: "404 Not Found" + + if (NetworkUtils.isHTML(error)) { + Result.failure(RuntimeException("404 Not Found")) + } else { + Result.failure(RuntimeException(error)) + } + } +} catch (e: Exception) { + Result.failure(e) +} \ No newline at end of file diff --git a/app/src/main/kotlin/com/dergoogler/mmrl/network/compose/Network.kt b/app/src/main/kotlin/com/dergoogler/mmrl/network/compose/Network.kt new file mode 100644 index 00000000..5a5f7276 --- /dev/null +++ b/app/src/main/kotlin/com/dergoogler/mmrl/network/compose/Network.kt @@ -0,0 +1,56 @@ +package com.dergoogler.mmrl.network.compose + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import com.dergoogler.mmrl.app.Event +import com.dergoogler.mmrl.network.NetworkUtils +import timber.log.Timber + +@Composable +fun runRequest( + get: suspend () -> Result, + onFailure: (Throwable) -> Unit = {}, + onSuccess: (T) -> Unit +): Event { + var event by remember { mutableStateOf(Event.LOADING) } + + LaunchedEffect(null) { + get().onSuccess { + event = Event.SUCCEEDED + onSuccess(it) + }.onFailure { + event = Event.FAILED + onFailure(it) + + Timber.e(it) + } + } + + return event +} + +@Composable +fun requestString( + url: String, + onFailure: (Throwable) -> Unit = {}, + onSuccess: (String) -> Unit +) = runRequest( + get = { NetworkUtils.requestString(url) }, + onSuccess = onSuccess, + onFailure = onFailure +) + +@Composable +inline fun requestJson( + url: String, + noinline onFailure: (Throwable) -> Unit = {}, + noinline onSuccess: (T) -> Unit +) = runRequest( + get = { NetworkUtils.requestJson(url) }, + onSuccess = onSuccess, + onFailure = onFailure +) \ No newline at end of file diff --git a/app/src/main/kotlin/com/dergoogler/mmrl/repository/LocalRepository.kt b/app/src/main/kotlin/com/dergoogler/mmrl/repository/LocalRepository.kt new file mode 100644 index 00000000..efacc2a4 --- /dev/null +++ b/app/src/main/kotlin/com/dergoogler/mmrl/repository/LocalRepository.kt @@ -0,0 +1,145 @@ +package com.dergoogler.mmrl.repository + +import com.dergoogler.mmrl.database.dao.JoinDao +import com.dergoogler.mmrl.database.dao.LocalDao +import com.dergoogler.mmrl.database.dao.OnlineDao +import com.dergoogler.mmrl.database.dao.RepoDao +import com.dergoogler.mmrl.database.dao.VersionDao +import com.dergoogler.mmrl.database.entity.LocalModuleEntity +import com.dergoogler.mmrl.database.entity.LocalModuleUpdatable +import com.dergoogler.mmrl.database.entity.OnlineModuleEntity +import com.dergoogler.mmrl.database.entity.Repo +import com.dergoogler.mmrl.database.entity.VersionItemEntity +import com.dergoogler.mmrl.model.local.LocalModule +import com.dergoogler.mmrl.model.online.OnlineModule +import com.dergoogler.mmrl.utils.extensions.merge +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.withContext +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class LocalRepository @Inject constructor( + private val repoDao: RepoDao, + private val onlineDao: OnlineDao, + private val versionDao: VersionDao, + private val localDao: LocalDao, + private val joinDao: JoinDao +) { + fun getLocalAllAsFlow() = localDao.getAllAsFlow().map { list -> + list.map { it.toModule() } + } + + suspend fun getLocalByIdOrNull(id: String) = withContext(Dispatchers.IO) { + localDao.getByIdOrNull(id)?.toModule() + } + + suspend fun insertLocal(value: LocalModule) = withContext(Dispatchers.IO) { + localDao.insert(LocalModuleEntity(value)) + } + + suspend fun insertLocal(list: List) = withContext(Dispatchers.IO) { + localDao.insert(list.map { LocalModuleEntity(it) }) + } + + suspend fun deleteLocalAll() = withContext(Dispatchers.IO) { + localDao.deleteAll() + } + + suspend fun insertUpdatableTag(id: String, updatable: Boolean) = withContext(Dispatchers.IO) { + localDao.insertUpdatableTag( + LocalModuleUpdatable( + id = id, + updatable = updatable + ) + ) + } + + suspend fun hasUpdatableTag(id: String) = withContext(Dispatchers.IO) { + localDao.hasUpdatableTagOrNull(id)?.updatable ?: true + } + + suspend fun clearUpdatableTag(new: List) = withContext(Dispatchers.IO) { + val removed = localDao.getUpdatableTagAll().filter { it.id !in new } + localDao.deleteUpdatableTag(removed) + } + + fun getRepoAllAsFlow() = repoDao.getAllAsFlow() + + suspend fun getRepoAll() = withContext(Dispatchers.IO) { + repoDao.getAll() + } + + suspend fun getRepoByUrl(url: String) = withContext(Dispatchers.IO) { + repoDao.getByUrl(url) + } + + suspend fun insertRepo(value: Repo) = withContext(Dispatchers.IO) { + repoDao.insert(value) + } + + suspend fun deleteRepo(value: Repo) = withContext(Dispatchers.IO) { + repoDao.delete(value) + } + + fun getOnlineAllAsFlow() = joinDao.getOnlineAllAsFlow().map { list -> + val values = mutableListOf() + list.forEach { entity -> + val new = entity.toModule() + + if (new in values) { + val old = values.first { it.id == new.id } + if (new.versionCode > old.versionCode) { + values.remove(old) + values.add(new.copy(versions = old.versions)) + } + } else { + values.add( + new.copy(versions = getVersionById(new.id)) + ) + } + } + + return@map values + } + + suspend fun getOnlineByIdAndUrl(id: String, repoUrl: String) = withContext(Dispatchers.IO) { + joinDao.getOnlineByIdAndUrl(id, repoUrl).toModule() + } + + suspend fun getOnlineAllById(id: String) = withContext(Dispatchers.IO) { + onlineDao.getAllById(id).map { it.toModule() } + } + + suspend fun insertOnline(list: List, repoUrl: String) = withContext(Dispatchers.IO) { + val versions = list.map { module -> + module.versions.map { + VersionItemEntity( + original = it, + id = module.id, + repoUrl = repoUrl + ) + } + }.merge() + + versionDao.insert(versions) + onlineDao.insert( + list.map { + OnlineModuleEntity( + original = it, + repoUrl = repoUrl + ) + } + ) + } + + suspend fun deleteOnlineByUrl(repoUrl: String) = withContext(Dispatchers.IO) { + versionDao.deleteByUrl(repoUrl) + onlineDao.deleteByUrl(repoUrl) + } + + suspend fun getVersionById(id: String) = withContext(Dispatchers.IO) { + joinDao.getVersionById(id).map { it.toItem() } + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/com/dergoogler/mmrl/repository/ModulesRepository.kt b/app/src/main/kotlin/com/dergoogler/mmrl/repository/ModulesRepository.kt new file mode 100644 index 00000000..405196f7 --- /dev/null +++ b/app/src/main/kotlin/com/dergoogler/mmrl/repository/ModulesRepository.kt @@ -0,0 +1,52 @@ +package com.dergoogler.mmrl.repository + +import com.dergoogler.mmrl.Compat +import com.dergoogler.mmrl.database.entity.Repo +import com.dergoogler.mmrl.network.runRequest +import com.dergoogler.mmrl.stub.IRepoManager +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import timber.log.Timber +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class ModulesRepository @Inject constructor( + private val localRepository: LocalRepository, +) { + suspend fun getLocalAll() = withContext(Dispatchers.IO) { + with(Compat.moduleManager.modules) { + localRepository.deleteLocalAll() + localRepository.insertLocal(this) + localRepository.clearUpdatableTag(map { it.id }) + } + } + + suspend fun getLocal(id: String) = withContext(Dispatchers.IO) { + val module = Compat.moduleManager.getModuleById(id) + localRepository.insertLocal(module) + } + + suspend fun getRepoAll(onlyEnable: Boolean = true) = + localRepository.getRepoAll().filter { + if (onlyEnable) it.enable else true + }.map { + getRepo(it) + } + + suspend fun getRepo(repo: Repo) = withContext(Dispatchers.IO) { + runRequest { + val api = IRepoManager.build(repo.url) + return@runRequest api.modules.execute() + }.onSuccess { modulesJson -> + localRepository.insertRepo(repo.copy(modulesJson)) + localRepository.deleteOnlineByUrl(repo.url) + localRepository.insertOnline( + list = modulesJson.modules, + repoUrl = repo.url + ) + }.onFailure { + Timber.e(it, "getRepo: ${repo.url}") + } + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/com/dergoogler/mmrl/repository/UserPreferencesRepository.kt b/app/src/main/kotlin/com/dergoogler/mmrl/repository/UserPreferencesRepository.kt new file mode 100644 index 00000000..94b4edc2 --- /dev/null +++ b/app/src/main/kotlin/com/dergoogler/mmrl/repository/UserPreferencesRepository.kt @@ -0,0 +1,33 @@ +package com.dergoogler.mmrl.repository + +import com.dergoogler.mmrl.datastore.DarkMode +import com.dergoogler.mmrl.datastore.UserPreferencesDataSource +import com.dergoogler.mmrl.datastore.WorkingMode +import com.dergoogler.mmrl.datastore.modules.ModulesMenuCompat +import com.dergoogler.mmrl.datastore.repository.RepositoryMenuCompat +import java.io.File +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class UserPreferencesRepository @Inject constructor( + private val userPreferencesDataSource: UserPreferencesDataSource +) { + val data get() = userPreferencesDataSource.data + + suspend fun setWorkingMode(value: WorkingMode) = userPreferencesDataSource.setWorkingMode(value) + + suspend fun setDarkTheme(value: DarkMode) = userPreferencesDataSource.setDarkTheme(value) + + suspend fun setThemeColor(value: Int) = userPreferencesDataSource.setThemeColor(value) + + suspend fun setDeleteZipFile(value: Boolean) = userPreferencesDataSource.setDeleteZipFile(value) + + suspend fun setUseDoh(value: Boolean) = userPreferencesDataSource.setUseDoh(value) + + suspend fun setDownloadPath(value: File) = userPreferencesDataSource.setDownloadPath(value) + + suspend fun setRepositoryMenu(value: RepositoryMenuCompat) = userPreferencesDataSource.setRepositoryMenu(value) + + suspend fun setModulesMenu(value: ModulesMenuCompat) = userPreferencesDataSource.setModulesMenu(value) +} \ No newline at end of file diff --git a/app/src/main/kotlin/com/dergoogler/mmrl/service/DownloadService.kt b/app/src/main/kotlin/com/dergoogler/mmrl/service/DownloadService.kt new file mode 100644 index 00000000..b11a5426 --- /dev/null +++ b/app/src/main/kotlin/com/dergoogler/mmrl/service/DownloadService.kt @@ -0,0 +1,282 @@ +package com.dergoogler.mmrl.service + +import android.Manifest +import android.annotation.SuppressLint +import android.app.Notification +import android.content.Context +import android.content.Intent +import android.os.Build +import android.os.Parcelable +import androidx.core.app.NotificationCompat +import androidx.core.app.NotificationManagerCompat +import androidx.core.app.ServiceCompat +import androidx.documentfile.provider.DocumentFile +import androidx.lifecycle.LifecycleService +import androidx.lifecycle.lifecycleScope +import com.dergoogler.mmrl.R +import com.dergoogler.mmrl.app.utils.NotificationUtils +import com.dergoogler.mmrl.compat.BuildCompat +import com.dergoogler.mmrl.compat.PermissionCompat +import com.dergoogler.mmrl.network.NetworkUtils +import com.dergoogler.mmrl.repository.UserPreferencesRepository +import com.dergoogler.mmrl.utils.extensions.parcelable +import dagger.hilt.android.AndroidEntryPoint +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.drop +import kotlinx.coroutines.flow.filter +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.flow.sample +import kotlinx.coroutines.isActive +import kotlinx.coroutines.launch +import kotlinx.parcelize.Parcelize +import timber.log.Timber +import java.io.FileNotFoundException +import javax.inject.Inject + +@AndroidEntryPoint +class DownloadService : LifecycleService() { + @Inject lateinit var userPreferencesRepository: UserPreferencesRepository + + private val tasks = mutableListOf() + + init { + lifecycleScope.launch { + while (isActive) { + delay(10_000L) + if (tasks.isEmpty()) stopSelf() + } + } + + progressFlow.drop(1) + .sample(500) + .flowOn(Dispatchers.IO) + .onEach { (item, progress) -> + if (progress != 0f) { + onProgressChanged(item, progress) + } + } + .launchIn(lifecycleScope) + } + + override fun onCreate() { + Timber.d("DownloadService onCreate") + super.onCreate() + + setForeground() + } + + override fun onDestroy() { + ServiceCompat.stopForeground(this, ServiceCompat.STOP_FOREGROUND_REMOVE) + + Timber.d("DownloadService onDestroy") + super.onDestroy() + } + + override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { + lifecycleScope.launch { + val item = intent?.taskItemOrNull ?: return@launch + val downloadPath = userPreferencesRepository.data + .first().downloadPath + .let { + if (!it.exists()) it.mkdirs() + DocumentFile.fromFile(it) + } + + val df = downloadPath.createFile("*/*", item.filename) + if (df == null) { + onDownloadFailed(item, "Failed to create file") + return@launch + } + + val output = try { + checkNotNull( + contentResolver.openOutputStream(df.uri) + ) + } catch (e: FileNotFoundException) { + onDownloadFailed(item, e.message) + return@launch + } + + val listener = object : IDownloadListener { + override fun getProgress(value: Float) { + progressFlow.value = item to value + listeners[item]?.getProgress(value) + } + + override fun onSuccess() { + onDownloadSucceeded(item) + + progressFlow.value = item to 0f + listeners[item]?.onSuccess() + tasks.remove(item) + } + + override fun onFailure(e: Throwable) { + onDownloadFailed(item, e.message) + + progressFlow.value = item to 0f + listeners[item]?.onFailure(e) + tasks.remove(item) + } + } + + tasks.add(item) + NetworkUtils.downloader( + url = item.url, + output = output, + onProgress = { + listener.getProgress(it) + } + ).onSuccess { + listener.onSuccess() + }.onFailure { + listener.onFailure(it) + } + } + return super.onStartCommand(intent, flags, startId) + } + + private fun onProgressChanged(item: TaskItem, progress: Float) { + val notification = baseNotificationBuilder() + .setContentTitle(item.title) + .setSubText(item.desc) + .setSilent(true) + .setOngoing(true) + .setGroup(GROUP_KEY) + .setProgress(100, (progress * 100).toInt(), false) + .build() + + notify(item.taskId, notification) + } + + private fun onDownloadSucceeded(item: TaskItem) { + val notification = baseNotificationBuilder() + .setContentTitle(item.title) + .setSubText(item.desc) + .setContentText(getString(R.string.message_download_success)) + .setSilent(true) + .build() + + notify(item.taskId, notification) + } + + private fun onDownloadFailed(item: TaskItem, message: String?) { + val notification = baseNotificationBuilder() + .setContentTitle(item.title) + .setSubText(item.desc) + .setContentText(message ?: getString(R.string.unknown_error)) + .build() + + notify(item.taskId, notification) + } + + private fun setForeground() { + val notification = baseNotificationBuilder() + .setContentTitle(getString(R.string.notification_name_download)) + .setSilent(true) + .setOngoing(true) + .setGroup(GROUP_KEY) + .setGroupSummary(true) + .build() + + startForeground(NotificationUtils.NOTIFICATION_ID_DOWNLOAD, notification) + } + + private fun baseNotificationBuilder() = + NotificationCompat.Builder(this, NotificationUtils.CHANNEL_ID_DOWNLOAD) + .setSmallIcon(R.drawable.launcher_outline) + + @SuppressLint("MissingPermission") + private fun notify(id: Int, notification: Notification) { + val granted = if (BuildCompat.atLeastT) { + PermissionCompat.checkPermissions( + this, + listOf(Manifest.permission.POST_NOTIFICATIONS) + ).allGranted + } else { + true + } + + NotificationManagerCompat.from(this).apply { + if (granted) notify(id, notification) + } + } + + @Parcelize + data class TaskItem( + val key: String, + val url: String, + val filename: String, + val title: String?, + val desc: String?, + val taskId: Int = System.currentTimeMillis().toInt(), + ) : Parcelable { + companion object { + fun empty() = TaskItem( + key = "", + url = "", + filename = "", + title = null, + desc = null, + taskId = -1 + ) + } + } + + interface IDownloadListener { + fun getProgress(value: Float) + fun onSuccess() + fun onFailure(e: Throwable) + } + + companion object { + private const val GROUP_KEY = "DOWNLOAD_SERVICE_GROUP_KEY" + private const val EXTRA_TASK = "com.dergoogler.mmrl.extra.TASK" + private val Intent.taskItemOrNull: TaskItem? get() = + parcelable(EXTRA_TASK) + + private val listeners = hashMapOf() + private val progressFlow = MutableStateFlow(TaskItem.empty() to 0f) + + fun getProgressByKey(key: String): Flow { + return progressFlow.filter { (item, _) -> + item.key == key + }.map { (_, progress) -> + progress + } + } + + fun start( + context: Context, + task: TaskItem, + listener: IDownloadListener + ) { + val permissions = mutableListOf() + if (Build.VERSION.SDK_INT <= 29) { + permissions.add(Manifest.permission.WRITE_EXTERNAL_STORAGE) + } + if (BuildCompat.atLeastT) { + permissions.add(Manifest.permission.POST_NOTIFICATIONS) + } + + PermissionCompat.requestPermissions(context, permissions) { state -> + if (state.allGranted) { + val intent = Intent(context, DownloadService::class.java) + intent.putExtra(EXTRA_TASK, task) + + listeners[task] = listener + context.startService(intent) + } else { + Timber.w("permissions: $state") + } + } + } + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/com/dergoogler/mmrl/stub/IRepoManager.kt b/app/src/main/kotlin/com/dergoogler/mmrl/stub/IRepoManager.kt new file mode 100644 index 00000000..fa8a6ca3 --- /dev/null +++ b/app/src/main/kotlin/com/dergoogler/mmrl/stub/IRepoManager.kt @@ -0,0 +1,22 @@ +package com.dergoogler.mmrl.stub + +import com.dergoogler.mmrl.model.online.ModulesJson +import com.dergoogler.mmrl.network.NetworkUtils +import retrofit2.Call +import retrofit2.create +import retrofit2.http.GET + +interface IRepoManager { + + @get:GET("json/modules.json") + val modules: Call + + companion object { + fun build(repoUrl: String): IRepoManager { + return NetworkUtils.createRetrofit() + .baseUrl(repoUrl) + .build() + .create() + } + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/com/dergoogler/mmrl/ui/activity/InstallActivity.kt b/app/src/main/kotlin/com/dergoogler/mmrl/ui/activity/InstallActivity.kt new file mode 100644 index 00000000..2ab2bffe --- /dev/null +++ b/app/src/main/kotlin/com/dergoogler/mmrl/ui/activity/InstallActivity.kt @@ -0,0 +1,89 @@ +package com.dergoogler.mmrl.ui.activity + +import android.content.Context +import android.content.Intent +import android.net.Uri +import android.os.Bundle +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.activity.enableEdgeToEdge +import androidx.activity.viewModels +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.getValue +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import androidx.lifecycle.lifecycleScope +import com.dergoogler.mmrl.repository.UserPreferencesRepository +import com.dergoogler.mmrl.ui.providable.LocalUserPreferences +import com.dergoogler.mmrl.ui.theme.AppTheme +import com.dergoogler.mmrl.utils.extensions.tmpDir +import com.dergoogler.mmrl.viewmodel.InstallViewModel +import dagger.hilt.android.AndroidEntryPoint +import kotlinx.coroutines.launch +import timber.log.Timber +import javax.inject.Inject + +@AndroidEntryPoint +class InstallActivity : ComponentActivity() { + @Inject lateinit var userPreferencesRepository: UserPreferencesRepository + private val viewModel: InstallViewModel by viewModels() + + override fun onCreate(savedInstanceState: Bundle?) { + Timber.d("InstallActivity onCreate") + super.onCreate(savedInstanceState) + enableEdgeToEdge() + + if (intent.data == null) { + finish() + } else { + initModule(intent) + } + + setContent { + val userPreferences by userPreferencesRepository.data + .collectAsStateWithLifecycle(initialValue = null) + + val preferences = if (userPreferences == null) { + return@setContent + } else { + checkNotNull(userPreferences) + } + + CompositionLocalProvider( + LocalUserPreferences provides preferences + ) { + AppTheme( + darkMode = preferences.isDarkMode(), + themeColor = preferences.themeColor + ) { + InstallScreen() + } + } + } + } + + override fun onDestroy() { + Timber.d("InstallActivity onDestroy") + tmpDir.deleteRecursively() + super.onDestroy() + } + + private fun initModule(intent: Intent) { + lifecycleScope.launch { + viewModel.loadModule( + context = applicationContext, + uri = checkNotNull(intent.data) + ) + } + } + + companion object { + fun start(context: Context, uri: Uri) { + val intent = Intent(context, InstallActivity::class.java) + .apply { + data = uri + } + + context.startActivity(intent) + } + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/com/dergoogler/mmrl/ui/activity/InstallScreen.kt b/app/src/main/kotlin/com/dergoogler/mmrl/ui/activity/InstallScreen.kt new file mode 100644 index 00000000..f09c7263 --- /dev/null +++ b/app/src/main/kotlin/com/dergoogler/mmrl/ui/activity/InstallScreen.kt @@ -0,0 +1,227 @@ +package com.dergoogler.mmrl.ui.activity + +import androidx.activity.compose.BackHandler +import androidx.activity.compose.rememberLauncherForActivityResult +import androidx.activity.result.contract.ActivityResultContracts +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.core.tween +import androidx.compose.animation.scaleIn +import androidx.compose.animation.scaleOut +import androidx.compose.foundation.focusable +import androidx.compose.foundation.horizontalScroll +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.foundation.rememberScrollState +import androidx.compose.material3.FloatingActionButton +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.SnackbarDuration +import androidx.compose.material3.SnackbarHost +import androidx.compose.material3.SnackbarHostState +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBarDefaults +import androidx.compose.material3.TopAppBarScrollBehavior +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.Modifier +import androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.focus.focusRequester +import androidx.compose.ui.input.key.Key +import androidx.compose.ui.input.key.key +import androidx.compose.ui.input.key.onKeyEvent +import androidx.compose.ui.input.nestedscroll.nestedScroll +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.unit.dp +import androidx.hilt.navigation.compose.hiltViewModel +import com.dergoogler.mmrl.R +import com.dergoogler.mmrl.app.Event +import com.dergoogler.mmrl.app.Event.Companion.isFinished +import com.dergoogler.mmrl.app.Event.Companion.isLoading +import com.dergoogler.mmrl.app.Event.Companion.isSucceeded +import com.dergoogler.mmrl.ui.component.NavigateUpTopBar +import com.dergoogler.mmrl.ui.utils.isScrollingUp +import com.dergoogler.mmrl.viewmodel.InstallViewModel +import kotlinx.coroutines.launch + +@Composable +fun InstallScreen( + viewModel: InstallViewModel = hiltViewModel() +) { + val scope = rememberCoroutineScope() + val snackbarHostState = remember { SnackbarHostState() } + val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior() + + val focusRequester = remember { FocusRequester() } + val listState = rememberLazyListState() + val isScrollingUp by listState.isScrollingUp() + val showFab by remember { + derivedStateOf { + isScrollingUp && viewModel.event.isSucceeded + } + } + + LaunchedEffect(focusRequester) { + focusRequester.requestFocus() + } + + BackHandler( + enabled = viewModel.event.isLoading, + onBack = {} + ) + + val context = LocalContext.current + val launcher = rememberLauncherForActivityResult( + ActivityResultContracts.CreateDocument("*/*") + ) { uri -> + if (uri == null) return@rememberLauncherForActivityResult + + scope.launch { + viewModel.writeLogsTo(context, uri) + .onSuccess { + val message = context.getString(R.string.install_logs_saved) + snackbarHostState.showSnackbar( + message = message, + duration = SnackbarDuration.Short + ) + }.onFailure { + val message = context.getString( + R.string.install_logs_save_failed, + it.message ?: context.getString(R.string.unknown_error) + ) + snackbarHostState.showSnackbar( + message = message, + duration = SnackbarDuration.Short + ) + } + } + } + + Scaffold( + modifier = Modifier + .onKeyEvent { + when (it.key) { + Key.VolumeUp, + Key.VolumeDown -> viewModel.event.isLoading + + else -> false + } + } + .focusRequester(focusRequester) + .focusable() + .nestedScroll(scrollBehavior.nestedScrollConnection), + topBar = { + TopBar( + exportLog = { launcher.launch(viewModel.logfile) }, + event = viewModel.event, + scrollBehavior = scrollBehavior + ) + }, + floatingActionButton = { + AnimatedVisibility( + visible = showFab, + enter = scaleIn( + animationSpec = tween(100), + initialScale = 0.8f + ), + exit = scaleOut( + animationSpec = tween(100), + targetScale = 0.8f + ) + ) { + FloatingButton( + reboot = viewModel::reboot + ) + } + }, + snackbarHost = { SnackbarHost(snackbarHostState) } + ) { + Console( + list = viewModel.console, + state = listState, + modifier = Modifier + .padding(it) + .fillMaxWidth() + .horizontalScroll(rememberScrollState()) + ) + } +} + +@Composable +private fun Console( + list: List, + state: LazyListState, + modifier: Modifier = Modifier, +) { + LaunchedEffect(list.size) { + state.animateScrollToItem(list.size) + } + + LazyColumn( + state = state, + modifier = modifier + ) { + items(list) { + Text( + text = it, + color = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.padding(horizontal = 8.dp), + style = MaterialTheme.typography.bodySmall.copy( + fontFamily = FontFamily.Monospace + ) + ) + } + } +} + +@Composable +private fun TopBar( + exportLog: () -> Unit, + event: Event, + scrollBehavior: TopAppBarScrollBehavior +) = NavigateUpTopBar( + title = stringResource(id = R.string.install_screen_title), + subtitle = stringResource(id = when (event) { + Event.LOADING -> R.string.install_flashing + Event.FAILED -> R.string.install_failure + else -> R.string.install_done + }), + scrollBehavior = scrollBehavior, + enable = event.isFinished, + actions = { + if (event.isFinished) { + IconButton( + onClick = exportLog + ) { + Icon( + painter = painterResource(id = R.drawable.device_floppy), + contentDescription = null + ) + } + } + } +) + +@Composable +private fun FloatingButton( + reboot: () -> Unit, +) = FloatingActionButton( + onClick = reboot +) { + Icon( + painter = painterResource(id = R.drawable.reload), + contentDescription = null + ) +} diff --git a/app/src/main/kotlin/com/dergoogler/mmrl/ui/activity/MainActivity.kt b/app/src/main/kotlin/com/dergoogler/mmrl/ui/activity/MainActivity.kt new file mode 100644 index 00000000..d03363c3 --- /dev/null +++ b/app/src/main/kotlin/com/dergoogler/mmrl/ui/activity/MainActivity.kt @@ -0,0 +1,117 @@ +package com.dergoogler.mmrl.ui.activity + +import android.content.ComponentName +import android.content.pm.PackageManager +import android.os.Bundle +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.activity.enableEdgeToEdge +import androidx.compose.animation.Crossfade +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue +import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import androidx.lifecycle.lifecycleScope +import com.dergoogler.mmrl.Compat +import com.dergoogler.mmrl.app.Const +import com.dergoogler.mmrl.database.entity.Repo.Companion.toRepo +import com.dergoogler.mmrl.datastore.UserPreferencesCompat.Companion.isRoot +import com.dergoogler.mmrl.datastore.UserPreferencesCompat.Companion.isSetup +import com.dergoogler.mmrl.datastore.WorkingMode +import com.dergoogler.mmrl.network.NetworkUtils +import com.dergoogler.mmrl.repository.LocalRepository +import com.dergoogler.mmrl.repository.UserPreferencesRepository +import com.dergoogler.mmrl.ui.providable.LocalUserPreferences +import com.dergoogler.mmrl.ui.theme.AppTheme +import dagger.hilt.android.AndroidEntryPoint +import kotlinx.coroutines.launch +import timber.log.Timber +import javax.inject.Inject + +@AndroidEntryPoint +class MainActivity : ComponentActivity() { + @Inject lateinit var userPreferencesRepository: UserPreferencesRepository + @Inject lateinit var localRepository: LocalRepository + + private var isLoading by mutableStateOf(true) + + override fun onCreate(savedInstanceState: Bundle?) { + val splashScreen = installSplashScreen() + super.onCreate(savedInstanceState) + enableEdgeToEdge() + + splashScreen.setKeepOnScreenCondition { isLoading } + + setContent { + val userPreferences by userPreferencesRepository.data + .collectAsStateWithLifecycle(initialValue = null) + + val preferences = if (userPreferences == null) { + return@setContent + } else { + isLoading = false + checkNotNull(userPreferences) + } + + LaunchedEffect(userPreferences) { + if (preferences.workingMode.isSetup) { + Timber.d("add default repository") + localRepository.insertRepo(Const.DEMO_REPO_URL.toRepo()) + } + + Compat.init(preferences.workingMode) + NetworkUtils.setEnableDoh(preferences.useDoh) + setInstallActivityEnabled(preferences.workingMode.isRoot) + } + + CompositionLocalProvider( + LocalUserPreferences provides preferences + ) { + AppTheme( + darkMode = preferences.isDarkMode(), + themeColor = preferences.themeColor + ) { + Crossfade( + targetState = preferences.workingMode.isSetup, + label = "MainActivity" + ) { isSetup -> + if (isSetup) { + SetupScreen( + setMode = ::setWorkingMode + ) + } else { + MainScreen() + } + } + } + } + } + } + + private fun setWorkingMode(value: WorkingMode) { + lifecycleScope.launch { + userPreferencesRepository.setWorkingMode(value) + } + } + + private fun setInstallActivityEnabled(enable: Boolean) { + val component = ComponentName( + this, InstallActivity::class.java + ) + + val state = if (enable) { + PackageManager.COMPONENT_ENABLED_STATE_ENABLED + } else { + PackageManager.COMPONENT_ENABLED_STATE_DISABLED + } + + packageManager.setComponentEnabledSetting( + component, + state, + PackageManager.DONT_KILL_APP + ) + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/com/dergoogler/mmrl/ui/activity/MainScreen.kt b/app/src/main/kotlin/com/dergoogler/mmrl/ui/activity/MainScreen.kt new file mode 100644 index 00000000..9faac6eb --- /dev/null +++ b/app/src/main/kotlin/com/dergoogler/mmrl/ui/activity/MainScreen.kt @@ -0,0 +1,109 @@ +package com.dergoogler.mmrl.ui.activity + +import androidx.compose.foundation.layout.imePadding +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.NavigationBar +import androidx.compose.material3.NavigationBarItem +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.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.navigation.NavController +import androidx.navigation.NavDestination.Companion.hierarchy +import androidx.navigation.compose.NavHost +import androidx.navigation.compose.currentBackStackEntryAsState +import androidx.navigation.compose.rememberNavController +import com.dergoogler.mmrl.datastore.UserPreferencesCompat.Companion.isRoot +import com.dergoogler.mmrl.ui.navigation.MainScreen +import com.dergoogler.mmrl.ui.navigation.graphs.modulesScreen +import com.dergoogler.mmrl.ui.navigation.graphs.repositoryScreen +import com.dergoogler.mmrl.ui.navigation.graphs.settingsScreen +import com.dergoogler.mmrl.ui.providable.LocalUserPreferences +import com.dergoogler.mmrl.ui.utils.navigatePopUpTo + +@Composable +fun MainScreen() { + val userPreferences = LocalUserPreferences.current + val navController = rememberNavController() + + Scaffold( + bottomBar = { + BottomNav( + navController = navController, + isRoot = userPreferences.workingMode.isRoot + ) + } + ) { + NavHost( + modifier = Modifier.padding(bottom = it.calculateBottomPadding()), + navController = navController, + startDestination = MainScreen.Repository.route + ) { + repositoryScreen( + navController = navController + ) + modulesScreen( + navController = navController + ) + settingsScreen( + navController = navController + ) + } + } +} + +@Composable +private fun BottomNav( + navController: NavController, + isRoot: Boolean +) { + val navBackStackEntry by navController.currentBackStackEntryAsState() + val currentDestination = navBackStackEntry?.destination + + val mainScreens by remember(isRoot) { + derivedStateOf { + if (isRoot) { + listOf(MainScreen.Repository, MainScreen.Modules, MainScreen.Settings) + } else { + listOf(MainScreen.Repository, MainScreen.Settings) + } + } + } + + NavigationBar( + modifier = Modifier.imePadding() + ) { + mainScreens.forEach { screen -> + val selected = currentDestination?.hierarchy?.any { it.route == screen.route } == true + + NavigationBarItem( + icon = { + Icon( + painter = painterResource(id = if (selected) { + screen.iconFilled + } else { + screen.icon + }), + contentDescription = null, + ) + }, + label = { + Text( + text = stringResource(id = screen.label), + style = MaterialTheme.typography.labelLarge + ) + }, + alwaysShowLabel = true, + selected = selected, + onClick = { if (!selected) navController.navigatePopUpTo(screen.route) } + ) + } + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/com/dergoogler/mmrl/ui/activity/ModConfActivity.kt b/app/src/main/kotlin/com/dergoogler/mmrl/ui/activity/ModConfActivity.kt new file mode 100644 index 00000000..847d364d --- /dev/null +++ b/app/src/main/kotlin/com/dergoogler/mmrl/ui/activity/ModConfActivity.kt @@ -0,0 +1,67 @@ +package com.dergoogler.mmrl.ui.activity + +import android.os.Bundle +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.compose.runtime.Composable +import android.content.Context +import android.content.Intent +import android.net.Uri +import dalvik.system.DexClassLoader +import androidx.compose.ui.platform.ComposeView +import dagger.hilt.android.AndroidEntryPoint + +@AndroidEntryPoint +class ModConfActivity : ComponentActivity() { + private lateinit var modConfCompose: Composable + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + val modId = intent.getStringExtra("MOD_ID") ?: return + + val fixedModId = modId.replace(Regex("[^a-zA-Z0-9._]"), "_") + + modConfCompose = + LoadComposableUtils.loadComposableFromDex( + this, + "/data/adb/modules/$modId/system/usr/share/mmrl/modconf/$modId.dex", + "com.dergoogler.modconf.${fixedModId}", + "ModConfScreen" + ) + + setContent { + modConfCompose + } + } + + + companion object { + fun start(context: Context, modId: String) { + val intent = Intent(context, ModConfActivity::class.java) + .apply { + putExtra("MOD_ID", modId) + } + + context.startActivity(intent) + } + } +} + +object LoadComposableUtils { + fun loadComposableFromDex( + context: Context, + dexPath: String, + className: String, + methodName: String + ): Composable { + val optimizedDir = context.getDir("dex", Context.MODE_PRIVATE) + val classLoader = + DexClassLoader(dexPath, "/data/adb", null, context.classLoader) + + val clazz = classLoader.loadClass(className) + val method = clazz.getDeclaredMethod(methodName) + + return method.invoke(null) as Composable + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/com/dergoogler/mmrl/ui/activity/ModConfScreen.kt b/app/src/main/kotlin/com/dergoogler/mmrl/ui/activity/ModConfScreen.kt new file mode 100644 index 00000000..819d58bd --- /dev/null +++ b/app/src/main/kotlin/com/dergoogler/mmrl/ui/activity/ModConfScreen.kt @@ -0,0 +1,12 @@ +package com.dergoogler.mmrl.ui.activity + +import androidx.compose.runtime.Composable +import androidx.hilt.navigation.compose.hiltViewModel +import com.dergoogler.mmrl.viewmodel.InstallViewModel + +@Composable +fun ModConfScreen( + viewModel: InstallViewModel = hiltViewModel() +) { + +} \ No newline at end of file diff --git a/app/src/main/kotlin/com/dergoogler/mmrl/ui/activity/SetupScreen.kt b/app/src/main/kotlin/com/dergoogler/mmrl/ui/activity/SetupScreen.kt new file mode 100644 index 00000000..719deaa9 --- /dev/null +++ b/app/src/main/kotlin/com/dergoogler/mmrl/ui/activity/SetupScreen.kt @@ -0,0 +1,56 @@ +package com.dergoogler.mmrl.ui.activity + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.height +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.res.stringResource +import androidx.compose.ui.unit.dp +import com.dergoogler.mmrl.R +import com.dergoogler.mmrl.datastore.WorkingMode +import com.dergoogler.mmrl.ui.screens.settings.workingmode.WorkingModeItem + +@Composable +fun SetupScreen( + setMode: (WorkingMode) -> Unit +) = Column( + modifier = Modifier + .background(color = MaterialTheme.colorScheme.background) + .fillMaxSize(), + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally +) { + Text( + text = stringResource(id = R.string.setup_mode), + style = MaterialTheme.typography.titleLarge, + color = MaterialTheme.colorScheme.onBackground + ) + + Spacer(modifier = Modifier.height(30.dp)) + WorkingModeItem( + title = stringResource(id = R.string.setup_root_title), + desc = stringResource(id = R.string.setup_root_desc), + onClick = { setMode(WorkingMode.MODE_ROOT) } + ) + + Spacer(modifier = Modifier.height(20.dp)) + WorkingModeItem( + title = stringResource(id = R.string.setup_shizuku_title), + desc = stringResource(id = R.string.setup_shizuku_desc), + onClick = { setMode(WorkingMode.MODE_SHIZUKU) } + ) + + Spacer(modifier = Modifier.height(20.dp)) + WorkingModeItem( + title = stringResource(id = R.string.setup_non_root_title), + desc = stringResource(id = R.string.setup_non_root_desc), + onClick = { setMode(WorkingMode.MODE_NON_ROOT) } + ) +} \ No newline at end of file diff --git a/app/src/main/kotlin/com/dergoogler/mmrl/ui/animate/EnterTransition.kt b/app/src/main/kotlin/com/dergoogler/mmrl/ui/animate/EnterTransition.kt new file mode 100644 index 00000000..0ef24944 --- /dev/null +++ b/app/src/main/kotlin/com/dergoogler/mmrl/ui/animate/EnterTransition.kt @@ -0,0 +1,52 @@ +package com.dergoogler.mmrl.ui.animate + +import androidx.compose.animation.core.FiniteAnimationSpec +import androidx.compose.animation.core.Spring +import androidx.compose.animation.core.VisibilityThreshold +import androidx.compose.animation.core.spring +import androidx.compose.animation.slideIn +import androidx.compose.ui.unit.IntOffset + +fun slideInBottomToTop( + animationSpec: FiniteAnimationSpec = + spring( + stiffness = Spring.StiffnessMediumLow, + visibilityThreshold = IntOffset.VisibilityThreshold + ) +) = slideIn( + initialOffset = { IntOffset(0, it.height) }, + animationSpec = animationSpec +) + +fun slideInTopToBottom( + animationSpec: FiniteAnimationSpec = + spring( + stiffness = Spring.StiffnessMediumLow, + visibilityThreshold = IntOffset.VisibilityThreshold + ) +) = slideIn( + initialOffset = { IntOffset(0, - it.height) }, + animationSpec = animationSpec +) + +fun slideInLeftToRight( + animationSpec: FiniteAnimationSpec = + spring( + stiffness = Spring.StiffnessMediumLow, + visibilityThreshold = IntOffset.VisibilityThreshold + ) +) = slideIn( + initialOffset = { IntOffset(- it.width, 0) }, + animationSpec = animationSpec +) + +fun slideInRightToLeft( + animationSpec: FiniteAnimationSpec = + spring( + stiffness = Spring.StiffnessMediumLow, + visibilityThreshold = IntOffset.VisibilityThreshold + ) +) = slideIn( + initialOffset = { IntOffset(it.width, 0) }, + animationSpec = animationSpec +) \ No newline at end of file diff --git a/app/src/main/kotlin/com/dergoogler/mmrl/ui/animate/ExitTransition.kt b/app/src/main/kotlin/com/dergoogler/mmrl/ui/animate/ExitTransition.kt new file mode 100644 index 00000000..4b119908 --- /dev/null +++ b/app/src/main/kotlin/com/dergoogler/mmrl/ui/animate/ExitTransition.kt @@ -0,0 +1,52 @@ +package com.dergoogler.mmrl.ui.animate + +import androidx.compose.animation.core.FiniteAnimationSpec +import androidx.compose.animation.core.Spring +import androidx.compose.animation.core.VisibilityThreshold +import androidx.compose.animation.core.spring +import androidx.compose.animation.slideOut +import androidx.compose.ui.unit.IntOffset + +fun slideOutTopToBottom( + animationSpec: FiniteAnimationSpec = + spring( + stiffness = Spring.StiffnessMediumLow, + visibilityThreshold = IntOffset.VisibilityThreshold + ) +) = slideOut( + targetOffset = { IntOffset(0, it.height) }, + animationSpec = animationSpec +) + +fun slideOutBottomToTop( + animationSpec: FiniteAnimationSpec = + spring( + stiffness = Spring.StiffnessMediumLow, + visibilityThreshold = IntOffset.VisibilityThreshold + ) +) = slideOut( + targetOffset = { IntOffset(0, - it.height) }, + animationSpec = animationSpec +) + +fun slideOutRightToLeft( + animationSpec: FiniteAnimationSpec = + spring( + stiffness = Spring.StiffnessMediumLow, + visibilityThreshold = IntOffset.VisibilityThreshold + ) +) = slideOut( + targetOffset = { IntOffset(- it.width, 0) }, + animationSpec = animationSpec +) + +fun slideOutLeftToRight( + animationSpec: FiniteAnimationSpec = + spring( + stiffness = Spring.StiffnessMediumLow, + visibilityThreshold = IntOffset.VisibilityThreshold + ) +) = slideOut( + targetOffset = { IntOffset(it.width, 0) }, + animationSpec = animationSpec +) \ No newline at end of file diff --git a/app/src/main/kotlin/com/dergoogler/mmrl/ui/component/Alert.kt b/app/src/main/kotlin/com/dergoogler/mmrl/ui/component/Alert.kt new file mode 100644 index 00000000..02c8bfe4 --- /dev/null +++ b/app/src/main/kotlin/com/dergoogler/mmrl/ui/component/Alert.kt @@ -0,0 +1,53 @@ +package com.dergoogler.mmrl.ui.component + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp + +@Composable +fun Alert( + modifier: Modifier = Modifier, + backgroundColor: Color = MaterialTheme.colorScheme.secondaryContainer, + textColor: Color = MaterialTheme.colorScheme.onSecondaryContainer, + title: String?, + message: String, +) { + Box( + modifier = Modifier + .fillMaxWidth() + .then(modifier), + ) { + Column( + modifier = Modifier + .fillMaxWidth() + .background( + color = backgroundColor, + shape = RoundedCornerShape(8.dp) + ) + .padding(12.dp), + verticalArrangement = Arrangement.spacedBy(4.dp) + ) { + if (title != null) { + Text( + text = title, + color = textColor, + style = MaterialTheme.typography.titleMedium, + fontWeight = FontWeight.Bold + ) + } + + Text( + text = message, + color = textColor, + style = MaterialTheme.typography.bodyMedium, + ) + } + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/com/dergoogler/mmrl/ui/component/AppBar.kt b/app/src/main/kotlin/com/dergoogler/mmrl/ui/component/AppBar.kt new file mode 100644 index 00000000..194fb9f3 --- /dev/null +++ b/app/src/main/kotlin/com/dergoogler/mmrl/ui/component/AppBar.kt @@ -0,0 +1,45 @@ +package com.dergoogler.mmrl.ui.component + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.width +import androidx.compose.material3.Icon +import androidx.compose.material3.LocalContentColor +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.res.painterResource +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import com.dergoogler.mmrl.BuildConfig +import com.dergoogler.mmrl.R + +@Composable +fun TopAppBarTitle( + text: String, + modifier: Modifier = Modifier +) = Row( + modifier = modifier, + horizontalArrangement = Arrangement.Start, + verticalAlignment = Alignment.CenterVertically +) { + Text( + text = text, + style = MaterialTheme.typography.titleLarge, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + color = LocalContentColor.current + ) + + if (BuildConfig.IS_DEV_VERSION) { + Spacer(modifier = Modifier.width(10.dp)) + Icon( + painter = painterResource(id = R.drawable.ci_label), + contentDescription = null, + tint = LocalContentColor.current + ) + } +} diff --git a/app/src/main/kotlin/com/dergoogler/mmrl/ui/component/CollapsingTopAppBar.kt b/app/src/main/kotlin/com/dergoogler/mmrl/ui/component/CollapsingTopAppBar.kt new file mode 100644 index 00000000..eda4dc64 --- /dev/null +++ b/app/src/main/kotlin/com/dergoogler/mmrl/ui/component/CollapsingTopAppBar.kt @@ -0,0 +1,526 @@ +package com.dergoogler.mmrl.ui.component + +import androidx.compose.animation.core.AnimationSpec +import androidx.compose.animation.core.AnimationState +import androidx.compose.animation.core.CubicBezierEasing +import androidx.compose.animation.core.DecayAnimationSpec +import androidx.compose.animation.core.FastOutLinearInEasing +import androidx.compose.animation.core.Spring +import androidx.compose.animation.core.animateDecay +import androidx.compose.animation.core.animateTo +import androidx.compose.animation.core.spring +import androidx.compose.animation.rememberSplineBasedDecay +import androidx.compose.foundation.gestures.Orientation +import androidx.compose.foundation.gestures.draggable +import androidx.compose.foundation.gestures.rememberDraggableState +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.ColumnScope +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.RowScope +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.WindowInsetsSides +import androidx.compose.foundation.layout.only +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.windowInsetsPadding +import androidx.compose.material3.ColorScheme +import androidx.compose.material3.LocalContentColor +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +import androidx.compose.material3.TopAppBarDefaults +import androidx.compose.material3.TopAppBarScrollBehavior +import androidx.compose.material3.TopAppBarState +import androidx.compose.material3.rememberTopAppBarState +import androidx.compose.material3.surfaceColorAtElevation +import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.Immutable +import androidx.compose.runtime.SideEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableFloatStateOf +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.clipToBounds +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.graphicsLayer +import androidx.compose.ui.graphics.lerp +import androidx.compose.ui.layout.AlignmentLine +import androidx.compose.ui.layout.LastBaseline +import androidx.compose.ui.layout.Layout +import androidx.compose.ui.layout.layoutId +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.semantics.clearAndSetSemantics +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.unit.Constraints +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.Velocity +import androidx.compose.ui.unit.dp +import androidx.compose.ui.util.fastFirst +import kotlin.math.abs +import kotlin.math.max +import kotlin.math.roundToInt + +@Composable +fun CollapsingTopAppBar( + title: @Composable () -> Unit, + content: @Composable ColumnScope.() -> Unit, + modifier: Modifier = Modifier, + navigationIcon: @Composable () -> Unit = {}, + actions: @Composable RowScope.() -> Unit = {}, + windowInsets: WindowInsets = CollapsingTopAppBarDefaults.windowInsets, + colors: CollapsingTopAppBarColors = CollapsingTopAppBarDefaults.topAppBarColors(), + scrollBehavior: TopAppBarScrollBehavior? = null +) { + CollapsingTopAppBarLayout( + title = { + Column( + modifier = Modifier.padding(end = TopAppBarTitleInset), + content = content + ) + }, + titleTextStyle = MaterialTheme.typography.titleLarge, + smallTitleTextStyle = MaterialTheme.typography.titleLarge, + smallTitle = title, + modifier = modifier, + navigationIcon = navigationIcon, + actions = actions, + colors = colors, + windowInsets = windowInsets, + pinnedHeight = 64.0.dp, + scrollBehavior = scrollBehavior + ) +} + +@Composable +private fun CollapsingTopAppBarLayout( + modifier: Modifier = Modifier, + title: @Composable () -> Unit, + titleTextStyle: TextStyle, + smallTitle: @Composable () -> Unit, + smallTitleTextStyle: TextStyle, + navigationIcon: @Composable () -> Unit, + actions: @Composable RowScope.() -> Unit, + windowInsets: WindowInsets, + colors: CollapsingTopAppBarColors, + pinnedHeight: Dp, + scrollBehavior: TopAppBarScrollBehavior? +) { + var maxHeightPx by remember { mutableFloatStateOf(0f) } + val pinnedHeightPx = with(LocalDensity.current) { + pinnedHeight.toPx() + } + + // Sets the app bar's height offset limit to hide just the bottom title area and keep top title + // visible when collapsed. + SideEffect { + if (scrollBehavior?.state?.heightOffsetLimit != pinnedHeightPx - maxHeightPx) { + scrollBehavior?.state?.heightOffsetLimit = pinnedHeightPx - maxHeightPx + } + } + + // Obtain the container Color from the TopAppBarColors using the `collapsedFraction`, as the + // bottom part of this TwoRowsTopAppBar changes color at the same rate the app bar expands or + // collapse. + // This will potentially animate or interpolate a transition between the container color and the + // container's scrolled color according to the app bar's scroll state. + val colorTransitionFraction = scrollBehavior?.state?.collapsedFraction ?: 0f + val appBarContainerColor = colors.containerColor(colorTransitionFraction) + + // Wrap the given actions in a Row. + val actionsRow = @Composable { + Row( + horizontalArrangement = Arrangement.End, + verticalAlignment = Alignment.CenterVertically, + content = actions + ) + } + val topTitleAlpha = TopTitleAlphaEasing.transform(colorTransitionFraction) + val bottomTitleAlpha = 1f - colorTransitionFraction + // Hide the top row title semantics when its alpha value goes below 0.5 threshold. + // Hide the bottom row title semantics when the top title semantics are active. + val hideTopRowSemantics = colorTransitionFraction < 0.5f + val hideBottomRowSemantics = !hideTopRowSemantics + + // Set up support for resizing the top app bar when vertically dragging the bar itself. + val appBarDragModifier = if (scrollBehavior != null && !scrollBehavior.isPinned) { + Modifier.draggable( + orientation = Orientation.Vertical, + state = rememberDraggableState { delta -> + scrollBehavior.state.heightOffset += delta + }, + onDragStopped = { velocity -> + settleAppBar( + scrollBehavior.state, + velocity, + scrollBehavior.flingAnimationSpec, + scrollBehavior.snapAnimationSpec + ) + } + ) + } else { + Modifier + } + + Surface( + modifier = modifier.then(appBarDragModifier), + color = appBarContainerColor + ) { + Column { + TopAppBarLayout( + modifier = Modifier + .windowInsetsPadding(windowInsets) + // clip after padding so we don't show the title over the inset area + .clipToBounds(), + heightPx = pinnedHeightPx, + navigationIconContentColor = + colors.navigationIconContentColor, + titleContentColor = colors.titleContentColor, + actionIconContentColor = + colors.actionIconContentColor, + title = smallTitle, + titleTextStyle = smallTitleTextStyle, + titleAlpha = topTitleAlpha, + titleVerticalArrangement = Arrangement.Center, + titleHorizontalArrangement = Arrangement.Start, + titleBottomPadding = 0, + hideTitleSemantics = hideTopRowSemantics, + navigationIcon = navigationIcon, + actions = actionsRow, + ) + + Layout( + content = { + Box( + Modifier + .layoutId("title") + .then(if (hideBottomRowSemantics) { + Modifier.clearAndSetSemantics { } + } else { + Modifier + }) + .graphicsLayer(alpha = bottomTitleAlpha) + ) { + ProvideContentColorTextStyle( + contentColor = colors.titleContentColor, + textStyle = titleTextStyle, + content = title) + } + }, + modifier = Modifier + // only apply the horizontal sides of the window insets padding, since the top + // padding will always be applied by the layout above + .windowInsetsPadding(windowInsets.only(WindowInsetsSides.Horizontal)) + .clipToBounds() + ) { measurables, constraints -> + val titlePlaceable = + measurables.fastFirst { it.layoutId == "title" } + .measure(constraints.copy(minWidth = 0)) + .apply { + maxHeightPx = pinnedHeightPx + height + } + + val heightPx = maxHeightPx - pinnedHeightPx + (scrollBehavior?.state?.heightOffset + ?: 0f) + + val layoutHeight = if (heightPx.isNaN()) 0 else heightPx.roundToInt() + layout(constraints.maxWidth, layoutHeight) { + titlePlaceable.placeRelative( + x = 0, + y = 0 + ) + } + } + } + } +} + +@Composable +private fun TopAppBarLayout( + modifier: Modifier, + heightPx: Float, + navigationIconContentColor: Color, + titleContentColor: Color, + actionIconContentColor: Color, + title: @Composable () -> Unit, + titleTextStyle: TextStyle, + titleAlpha: Float, + titleVerticalArrangement: Arrangement.Vertical, + titleHorizontalArrangement: Arrangement.Horizontal, + titleBottomPadding: Int, + hideTitleSemantics: Boolean, + navigationIcon: @Composable () -> Unit, + actions: @Composable () -> Unit, +) { + Layout( + content = { + Box( + Modifier + .layoutId("navigationIcon") + .padding(start = TopAppBarHorizontalPadding) + ) { + CompositionLocalProvider( + LocalContentColor provides navigationIconContentColor, + content = navigationIcon + ) + } + Box( + Modifier + .layoutId("title") + .padding(horizontal = TopAppBarHorizontalPadding) + .then(if (hideTitleSemantics) Modifier.clearAndSetSemantics { } else Modifier) + .graphicsLayer(alpha = titleAlpha) + ) { + ProvideContentColorTextStyle( + contentColor = titleContentColor, + textStyle = titleTextStyle, + content = title) + } + Box( + Modifier + .layoutId("actionIcons") + .padding(end = TopAppBarHorizontalPadding) + ) { + CompositionLocalProvider( + LocalContentColor provides actionIconContentColor, + content = actions + ) + } + }, + modifier = modifier + ) { measurables, constraints -> + val navigationIconPlaceable = + measurables.fastFirst { it.layoutId == "navigationIcon" } + .measure(constraints.copy(minWidth = 0)) + val actionIconsPlaceable = + measurables.fastFirst { it.layoutId == "actionIcons" } + .measure(constraints.copy(minWidth = 0)) + + val maxTitleWidth = if (constraints.maxWidth == Constraints.Infinity) { + constraints.maxWidth + } else { + (constraints.maxWidth - navigationIconPlaceable.width - actionIconsPlaceable.width) + .coerceAtLeast(0) + } + val titlePlaceable = + measurables.fastFirst { it.layoutId == "title" } + .measure(constraints.copy(minWidth = 0, maxWidth = maxTitleWidth)) + + // Locate the title's baseline. + val titleBaseline = + if (titlePlaceable[LastBaseline] != AlignmentLine.Unspecified) { + titlePlaceable[LastBaseline] + } else { + 0 + } + + val layoutHeight = if (heightPx.isNaN()) 0 else heightPx.roundToInt() + + layout(constraints.maxWidth, layoutHeight) { + // Navigation icon + navigationIconPlaceable.placeRelative( + x = 0, + y = (layoutHeight - navigationIconPlaceable.height) / 2 + ) + + // Title + titlePlaceable.placeRelative( + x = when (titleHorizontalArrangement) { + Arrangement.Center -> { + var baseX = (constraints.maxWidth - titlePlaceable.width) / 2 + if (baseX < navigationIconPlaceable.width) { + // May happen if the navigation is wider than the actions and the + // title is long. In this case, prioritize showing more of the title by + // offsetting it to the right. + baseX += (navigationIconPlaceable.width - baseX) + } else if (baseX + titlePlaceable.width > + constraints.maxWidth - actionIconsPlaceable.width + ) { + // May happen if the actions are wider than the navigation and the title + // is long. In this case, offset to the left. + baseX += ((constraints.maxWidth - actionIconsPlaceable.width) - + (baseX + titlePlaceable.width)) + } + baseX + } + + Arrangement.End -> + constraints.maxWidth - titlePlaceable.width - actionIconsPlaceable.width + // Arrangement.Start. + // An TopAppBarTitleInset will make sure the title is offset in case the + // navigation icon is missing. + else -> max(TopAppBarTitleInset.roundToPx(), navigationIconPlaceable.width) + }, + y = when (titleVerticalArrangement) { + Arrangement.Center -> (layoutHeight - titlePlaceable.height) / 2 + // Apply bottom padding from the title's baseline only when the Arrangement is + // "Bottom". + Arrangement.Bottom -> + if (titleBottomPadding == 0) layoutHeight - titlePlaceable.height + else layoutHeight - titlePlaceable.height - max( + 0, + titleBottomPadding - titlePlaceable.height + titleBaseline + ) + // Arrangement.Top + else -> 0 + } + ) + + // Action icons + actionIconsPlaceable.placeRelative( + x = constraints.maxWidth - actionIconsPlaceable.width, + y = (layoutHeight - actionIconsPlaceable.height) / 2 + ) + } + } +} + +private suspend fun settleAppBar( + state: TopAppBarState, + velocity: Float, + flingAnimationSpec: DecayAnimationSpec?, + snapAnimationSpec: AnimationSpec? +): Velocity { + // Check if the app bar is completely collapsed/expanded. If so, no need to settle the app bar, + // and just return Zero Velocity. + // Note that we don't check for 0f due to float precision with the collapsedFraction + // calculation. + if (state.collapsedFraction < 0.01f || state.collapsedFraction == 1f) { + return Velocity.Zero + } + var remainingVelocity = velocity + // In case there is an initial velocity that was left after a previous user fling, animate to + // continue the motion to expand or collapse the app bar. + if (flingAnimationSpec != null && abs(velocity) > 1f) { + var lastValue = 0f + AnimationState( + initialValue = 0f, + initialVelocity = velocity, + ) + .animateDecay(flingAnimationSpec) { + val delta = value - lastValue + val initialHeightOffset = state.heightOffset + state.heightOffset = initialHeightOffset + delta + val consumed = abs(initialHeightOffset - state.heightOffset) + lastValue = value + remainingVelocity = this.velocity + // avoid rounding errors and stop if anything is unconsumed + if (abs(delta - consumed) > 0.5f) this.cancelAnimation() + } + } + // Snap if animation specs were provided. + if (snapAnimationSpec != null) { + if (state.heightOffset < 0 && + state.heightOffset > state.heightOffsetLimit + ) { + AnimationState(initialValue = state.heightOffset).animateTo( + if (state.collapsedFraction < 0.5f) { + 0f + } else { + state.heightOffsetLimit + }, + animationSpec = snapAnimationSpec + ) { state.heightOffset = value } + } + } + + return Velocity(0f, remainingVelocity) +} + +// An easing function used to compute the alpha value that is applied to the top title part of a +// Medium or Large app bar. +/*@VisibleForTesting*/ +internal val TopTitleAlphaEasing = CubicBezierEasing(.8f, 0f, .8f, .15f) + +private val TopAppBarHorizontalPadding = 4.dp + +// A title inset when the App-Bar is a Medium or Large one. Also used to size a spacer when the +// navigation icon is missing. +private val TopAppBarTitleInset = 16.dp - TopAppBarHorizontalPadding + +@Immutable +class CollapsingTopAppBarColors internal constructor( + private val containerColor: Color, + private val scrolledContainerColor: Color, + internal val navigationIconContentColor: Color, + internal val titleContentColor: Color, + internal val actionIconContentColor: Color, +) { + @Composable + internal fun containerColor(colorTransitionFraction: Float): Color { + return lerp( + containerColor, + scrolledContainerColor, + FastOutLinearInEasing.transform(colorTransitionFraction) + ) + } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other == null || other !is CollapsingTopAppBarColors) return false + + if (containerColor != other.containerColor) return false + if (scrolledContainerColor != other.scrolledContainerColor) return false + if (navigationIconContentColor != other.navigationIconContentColor) return false + if (titleContentColor != other.titleContentColor) return false + if (actionIconContentColor != other.actionIconContentColor) return false + + return true + } + + override fun hashCode(): Int { + var result = containerColor.hashCode() + result = 31 * result + scrolledContainerColor.hashCode() + result = 31 * result + navigationIconContentColor.hashCode() + result = 31 * result + titleContentColor.hashCode() + result = 31 * result + actionIconContentColor.hashCode() + + return result + } +} + +object CollapsingTopAppBarDefaults { + @Composable + fun topAppBarColors( + containerColor: Color = MaterialTheme.colorScheme.surface, + scrolledContainerColor: Color = MaterialTheme.colorScheme.applyTonalElevation( + backgroundColor = containerColor, + elevation = 3.0.dp + ), + navigationIconContentColor: Color = MaterialTheme.colorScheme.onSurface, + titleContentColor: Color = MaterialTheme.colorScheme.onSurface, + actionIconContentColor: Color = MaterialTheme.colorScheme.onSurfaceVariant, + ) = CollapsingTopAppBarColors( + containerColor, + scrolledContainerColor, + navigationIconContentColor, + titleContentColor, + actionIconContentColor + ) + + @Composable + fun scrollBehavior( + state: TopAppBarState = rememberTopAppBarState(), + canScroll: () -> Boolean = { true }, + snapAnimationSpec: AnimationSpec? = spring(stiffness = Spring.StiffnessMediumLow), + flingAnimationSpec: DecayAnimationSpec? = rememberSplineBasedDecay() + ) = TopAppBarDefaults.exitUntilCollapsedScrollBehavior( + state, + canScroll, + snapAnimationSpec, + flingAnimationSpec + ) + + val windowInsets @Composable get() = TopAppBarDefaults.windowInsets + +} + +private fun ColorScheme.applyTonalElevation(backgroundColor: Color, elevation: Dp): Color { + return if (backgroundColor == surface) { + surfaceColorAtElevation(elevation) + } else { + backgroundColor + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/com/dergoogler/mmrl/ui/component/DropdownMenu.kt b/app/src/main/kotlin/com/dergoogler/mmrl/ui/component/DropdownMenu.kt new file mode 100644 index 00000000..0e1843bf --- /dev/null +++ b/app/src/main/kotlin/com/dergoogler/mmrl/ui/component/DropdownMenu.kt @@ -0,0 +1,67 @@ +package com.dergoogler.mmrl.ui.component + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.ColumnScope +import androidx.compose.foundation.shape.CornerBasedShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.DropdownMenu +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.DpOffset +import androidx.compose.ui.unit.dp +import androidx.compose.ui.window.PopupProperties +import com.dergoogler.mmrl.ui.utils.ProvideMenuShape + +@Composable +fun DropdownMenu( + expanded: Boolean, + onDismissRequest: () -> Unit, + modifier: Modifier = Modifier, + shape: CornerBasedShape = RoundedCornerShape(8.dp), + offset: DpOffset = DpOffset.Zero, + properties: PopupProperties = PopupProperties(focusable = true), + content: @Composable ColumnScope.() -> Unit +) { + ProvideMenuShape(shape) { + DropdownMenu( + expanded = expanded, + onDismissRequest = onDismissRequest, + modifier = modifier, + offset = offset, + properties = properties, + content = content + ) + } +} + +@Composable +fun DropdownMenu( + expanded: Boolean, + onDismissRequest: () -> Unit, + modifier: Modifier = Modifier, + shape: CornerBasedShape = RoundedCornerShape(8.dp), + contentAlignment: Alignment = Alignment.TopStart, + offset: DpOffset = DpOffset.Zero, + properties: PopupProperties = PopupProperties(focusable = true), + surface: @Composable () -> Unit, + content: @Composable ColumnScope.() -> Unit +) = Box { + surface() + + ProvideMenuShape(shape) { + Box( + modifier = Modifier.align(contentAlignment), + contentAlignment = contentAlignment + ) { + DropdownMenu( + expanded = expanded, + onDismissRequest = onDismissRequest, + modifier = modifier, + offset = offset, + properties = properties, + content = content + ) + } + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/com/dergoogler/mmrl/ui/component/LabelIcon.kt b/app/src/main/kotlin/com/dergoogler/mmrl/ui/component/LabelIcon.kt new file mode 100644 index 00000000..0867cdcb --- /dev/null +++ b/app/src/main/kotlin/com/dergoogler/mmrl/ui/component/LabelIcon.kt @@ -0,0 +1,40 @@ +package com.dergoogler.mmrl.ui.component + +import androidx.annotation.DrawableRes +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Icon +import androidx.compose.material3.LocalContentColor +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.Shape +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.unit.dp + +@Composable +fun LabelIcon( + @DrawableRes icon: Int, + containerColor: Color = MaterialTheme.colorScheme.primary, + shape: Shape = RoundedCornerShape(3.dp), +) { + Box( + modifier = Modifier + .background( + color = containerColor, + shape = shape + ), + contentAlignment = Alignment.Center + ) { + Icon( + modifier = Modifier.padding(horizontal = 4.dp), + painter = painterResource(id = icon), + contentDescription = null, + tint = LocalContentColor.current + ) + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/com/dergoogler/mmrl/ui/component/LabelItem.kt b/app/src/main/kotlin/com/dergoogler/mmrl/ui/component/LabelItem.kt new file mode 100644 index 00000000..c6f90039 --- /dev/null +++ b/app/src/main/kotlin/com/dergoogler/mmrl/ui/component/LabelItem.kt @@ -0,0 +1,47 @@ +package com.dergoogler.mmrl.ui.component + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape +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.Color +import androidx.compose.ui.graphics.Shape +import androidx.compose.ui.text.intl.Locale +import androidx.compose.ui.text.toUpperCase +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp + +@Composable +fun LabelItem( + text: String, + containerColor: Color = MaterialTheme.colorScheme.primary, + contentColor: Color = MaterialTheme.colorScheme.onPrimary, + shape: Shape = RoundedCornerShape(3.dp), + upperCase: Boolean = true +) { + if (text.isBlank()) return + + Box( + modifier = Modifier + .background( + color = containerColor, + shape = shape + ), + contentAlignment = Alignment.Center + ) { + Text( + text = when { + upperCase -> text.toUpperCase(Locale.current) + else -> text + }, + style = MaterialTheme.typography.labelSmall.copy(fontSize = 8.sp), + color = contentColor, + modifier = Modifier.padding(horizontal = 4.dp) + ) + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/com/dergoogler/mmrl/ui/component/LicenseContent.kt b/app/src/main/kotlin/com/dergoogler/mmrl/ui/component/LicenseContent.kt new file mode 100644 index 00000000..a3cd15eb --- /dev/null +++ b/app/src/main/kotlin/com/dergoogler/mmrl/ui/component/LicenseContent.kt @@ -0,0 +1,187 @@ +package com.dergoogler.mmrl.ui.component + +import androidx.compose.animation.Crossfade +import androidx.compose.foundation.BorderStroke +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.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +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.graphics.Color +import androidx.compose.ui.graphics.Shape +import androidx.compose.ui.graphics.painter.Painter +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import com.dergoogler.mmrl.R +import com.dergoogler.mmrl.app.Const +import com.dergoogler.mmrl.app.Event.Companion.isFailed +import com.dergoogler.mmrl.app.Event.Companion.isLoading +import com.dergoogler.mmrl.app.Event.Companion.isSucceeded +import com.dergoogler.mmrl.model.json.License +import com.dergoogler.mmrl.network.compose.requestJson +import timber.log.Timber + +@Composable +fun LicenseContent( + licenseId: String, + modifier: Modifier = Modifier +) { + var license: License? by remember { mutableStateOf(null) } + var message: String? by remember { mutableStateOf(null) } + val event = requestJson( + url = Const.SPDX_URL.format(licenseId), + onSuccess = { license = it }, + onFailure = { + message = it.message + Timber.e(it, "getLicense: $licenseId") + } + ) + + Crossfade( + targetState = event, + label = "LicenseContent" + ) { + when { + it.isLoading -> Loading( + minHeight = 200.dp + ) + it.isSucceeded -> ViewLicense( + license = checkNotNull(license), + modifier = modifier + ) + it.isFailed -> Failed( + message = message, + minHeight = 200.dp + ) + } + } +} + +@Composable +private fun ViewLicense( + license: License, + modifier: Modifier = Modifier +) = Column( + modifier = modifier, + verticalArrangement = Arrangement.spacedBy(16.dp), + horizontalAlignment = Alignment.CenterHorizontally +) { + Surface( + shape = RoundedCornerShape(15.dp), + tonalElevation = 6.dp, + border = BorderStroke(1.dp, color = MaterialTheme.colorScheme.outline) + ) { + Column( + modifier = Modifier + .padding(all = 16.dp) + .fillMaxWidth(), + verticalArrangement = Arrangement.spacedBy(10.dp) + ) { + Text( + text = license.name, + style = MaterialTheme.typography.bodyLarge, + fontWeight = FontWeight.Medium + ) + + if (license.seeAlso.isNotEmpty()) { + MarkdownText( + text = license.seeAlso.joinToString(separator = "\n") { + " - [${it}](${it})" + }, + style = MaterialTheme.typography.bodyMedium + ) + } + + if (license.hasLabel()) { + LabelsItem(license = license) + } + } + } + + Text( + modifier = Modifier + .verticalScroll(rememberScrollState()) + .padding(bottom = 18.dp) + .fillMaxWidth(), + text = license.licenseText, + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.outline + ) +} + +@Composable +private fun LabelsItem( + license: License +) = Row( + modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(10.dp) +) { + Spacer(modifier = Modifier.weight(1f)) + + if (license.isFsfLibre) { + LabelItem( + painter = painterResource(id = R.drawable.users), + text = stringResource(id = R.string.license_fsf_libre) + ) + } + + if (license.isOsiApproved) { + LabelItem( + painter = painterResource(id = R.drawable.brand_open_source), + text = stringResource(id = R.string.license_osi_approved) + ) + } +} + +@Composable +private fun LabelItem( + painter: Painter, + text: String, + containerColor: Color = Color.Transparent, + shape: Shape = RoundedCornerShape(10.dp) +) = Surface( + shape = shape, + color = containerColor, + border = BorderStroke(1.dp, MaterialTheme.colorScheme.outline), +) { + Row( + modifier = Modifier.padding(all = 8.dp), + horizontalArrangement = Arrangement.Start, + verticalAlignment = Alignment.CenterVertically + ) { + Icon( + painter = painter, + contentDescription = null, + modifier = Modifier.size(ButtonDefaults.IconSize) + ) + + Spacer(modifier = Modifier.width(6.dp)) + + Text( + text = text, + maxLines = 1, + style = MaterialTheme.typography.labelMedium + ) + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/com/dergoogler/mmrl/ui/component/Logo.kt b/app/src/main/kotlin/com/dergoogler/mmrl/ui/component/Logo.kt new file mode 100644 index 00000000..ff24a131 --- /dev/null +++ b/app/src/main/kotlin/com/dergoogler/mmrl/ui/component/Logo.kt @@ -0,0 +1,42 @@ +package com.dergoogler.mmrl.ui.component + +import androidx.annotation.DrawableRes +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material3.Icon +import androidx.compose.material3.LocalContentColor +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.Shape +import androidx.compose.ui.res.painterResource + +@Composable +fun Logo( + @DrawableRes icon: Int, + modifier: Modifier = Modifier, + shape: Shape = CircleShape, + contentColor: Color = MaterialTheme.colorScheme.onPrimary, + containerColor: Color = MaterialTheme.colorScheme.primary, + fraction: Float = 0.6f +) = Surface( + modifier = modifier, + shape = shape, + color = containerColor, + contentColor = contentColor +) { + Box( + contentAlignment = Alignment.Center + ) { + Icon( + modifier = Modifier.fillMaxSize(fraction), + painter = painterResource(id = icon), + contentDescription = null, + tint = LocalContentColor.current + ) + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/com/dergoogler/mmrl/ui/component/MenuChip.kt b/app/src/main/kotlin/com/dergoogler/mmrl/ui/component/MenuChip.kt new file mode 100644 index 00000000..191ed35e --- /dev/null +++ b/app/src/main/kotlin/com/dergoogler/mmrl/ui/component/MenuChip.kt @@ -0,0 +1,74 @@ +package com.dergoogler.mmrl.ui.component + +import androidx.compose.foundation.Canvas +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Done +import androidx.compose.material3.FilterChip +import androidx.compose.material3.FilterChipDefaults +import androidx.compose.material3.Icon +import androidx.compose.material3.LocalContentColor +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp + +@Composable +fun MenuChip( + selected: Boolean, + onClick: () -> Unit, + label: @Composable () -> Unit, + modifier: Modifier = Modifier, + enabled: Boolean = true, +) = FilterChip( + selected = selected, + onClick = onClick, + label = label, + modifier = modifier.height(FilterChipDefaults.Height), + enabled = enabled, + leadingIcon = { + if (!selected) { + Point(size = 8.dp) + } + }, + trailingIcon = { + if (selected) { + Icon( + imageVector = Icons.Filled.Done, + contentDescription = null, + modifier = Modifier.size(FilterChipDefaults.IconSize) + ) + } + }, + shape = CircleShape, + colors = FilterChipDefaults.filterChipColors( + iconColor = MaterialTheme.colorScheme.secondary, + selectedContainerColor = MaterialTheme.colorScheme.secondary, + selectedLabelColor = MaterialTheme.colorScheme.onSecondary, + selectedLeadingIconColor = MaterialTheme.colorScheme.onSecondary, + selectedTrailingIconColor = MaterialTheme.colorScheme.onSecondary + ), + border = FilterChipDefaults.filterChipBorder( + enabled = enabled, + selected = selected, + borderColor = MaterialTheme.colorScheme.secondary, + ) +) + +@Composable +private fun Point( + size: Dp, + color: Color = LocalContentColor.current +) = Canvas( + modifier = Modifier.size(size) +) { + drawCircle( + color = color, + radius = this.size.width / 2, + center = this.center + ) +} \ No newline at end of file diff --git a/app/src/main/kotlin/com/dergoogler/mmrl/ui/component/NavigateUpTopBar.kt b/app/src/main/kotlin/com/dergoogler/mmrl/ui/component/NavigateUpTopBar.kt new file mode 100644 index 00000000..00e376a0 --- /dev/null +++ b/app/src/main/kotlin/com/dergoogler/mmrl/ui/component/NavigateUpTopBar.kt @@ -0,0 +1,140 @@ +package com.dergoogler.mmrl.ui.component + +import android.content.Context +import androidx.activity.ComponentActivity +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.RowScope +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar +import androidx.compose.material3.TopAppBarColors +import androidx.compose.material3.TopAppBarDefaults +import androidx.compose.material3.TopAppBarScrollBehavior +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.painterResource +import androidx.compose.ui.text.style.TextOverflow +import androidx.navigation.NavController +import com.dergoogler.mmrl.R + +@Composable +fun NavigateUpTopBar( + title: String, + navController: NavController, + modifier: Modifier = Modifier, + subtitle: String? = null, + enable: Boolean = true, + actions: @Composable RowScope.() -> Unit = {}, + windowInsets: WindowInsets = TopAppBarDefaults.windowInsets, + colors: TopAppBarColors = TopAppBarDefaults.topAppBarColors(), + scrollBehavior: TopAppBarScrollBehavior? = null +) = NavigateUpTopBar( + modifier = modifier, + title = title, + subtitle = subtitle, + onBack = { navController.popBackStack() }, + actions = actions, + windowInsets = windowInsets, + colors = colors, + scrollBehavior = scrollBehavior, + enable = enable +) + +@Composable +fun NavigateUpTopBar( + title: String, + modifier: Modifier = Modifier, + context: Context = LocalContext.current, + subtitle: String? = null, + enable: Boolean = true, + actions: @Composable RowScope.() -> Unit = {}, + windowInsets: WindowInsets = TopAppBarDefaults.windowInsets, + colors: TopAppBarColors = TopAppBarDefaults.topAppBarColors(), + scrollBehavior: TopAppBarScrollBehavior? = null +) = NavigateUpTopBar( + modifier = modifier, + title = title, + subtitle = subtitle, + onBack = { (context as ComponentActivity).finish() }, + actions = actions, + windowInsets = windowInsets, + colors = colors, + scrollBehavior = scrollBehavior, + enable = enable +) + +@Composable +fun NavigateUpTopBar( + title: String, + onBack: () -> Unit, + modifier: Modifier = Modifier, + subtitle: String? = null, + enable: Boolean = true, + actions: @Composable RowScope.() -> Unit = {}, + windowInsets: WindowInsets = TopAppBarDefaults.windowInsets, + colors: TopAppBarColors = TopAppBarDefaults.topAppBarColors(), + scrollBehavior: TopAppBarScrollBehavior? = null +) = NavigateUpTopBar( + modifier = modifier, + title = { + Column( + horizontalAlignment = Alignment.Start + ) { + Text( + text = title, + style = MaterialTheme.typography.titleLarge, + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + subtitle?.let { + Text( + text = subtitle, + style = MaterialTheme.typography.titleSmall, + color = MaterialTheme.colorScheme.outline, + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + } + } + }, + onBack = onBack, + actions = actions, + windowInsets = windowInsets, + colors = colors, + scrollBehavior = scrollBehavior, + enable = enable +) + +@Composable +fun NavigateUpTopBar( + title: @Composable () -> Unit, + onBack: () -> Unit, + modifier: Modifier = Modifier, + enable: Boolean = true, + actions: @Composable RowScope.() -> Unit = {}, + windowInsets: WindowInsets = TopAppBarDefaults.windowInsets, + colors: TopAppBarColors = TopAppBarDefaults.topAppBarColors(), + scrollBehavior: TopAppBarScrollBehavior? = null +) = TopAppBar( + title = title, + modifier = modifier, + navigationIcon = { + IconButton( + onClick = { if (enable) onBack() } + ) { + Icon( + painter = painterResource(id = R.drawable.arrow_left), + contentDescription = null + ) + } + }, + actions = actions, + windowInsets = windowInsets, + colors = colors, + scrollBehavior = scrollBehavior +) \ No newline at end of file diff --git a/app/src/main/kotlin/com/dergoogler/mmrl/ui/component/NavigationBarsSpacer.kt b/app/src/main/kotlin/com/dergoogler/mmrl/ui/component/NavigationBarsSpacer.kt new file mode 100644 index 00000000..28097bc7 --- /dev/null +++ b/app/src/main/kotlin/com/dergoogler/mmrl/ui/component/NavigationBarsSpacer.kt @@ -0,0 +1,23 @@ +package com.dergoogler.mmrl.ui.component + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.asPaddingValues +import androidx.compose.foundation.layout.navigationBars +import androidx.compose.foundation.layout.padding +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier + +@Composable +fun NavigationBarsSpacer( + modifier: Modifier = Modifier +) { + val paddingValues = WindowInsets.navigationBars.asPaddingValues() + + Box( + modifier = Modifier.padding(paddingValues) + ) { + Spacer(modifier = modifier) + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/com/dergoogler/mmrl/ui/component/PageIndicator.kt b/app/src/main/kotlin/com/dergoogler/mmrl/ui/component/PageIndicator.kt new file mode 100644 index 00000000..28a4e516 --- /dev/null +++ b/app/src/main/kotlin/com/dergoogler/mmrl/ui/component/PageIndicator.kt @@ -0,0 +1,143 @@ +package com.dergoogler.mmrl.ui.component + +import androidx.annotation.DrawableRes +import androidx.annotation.StringRes +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.ColumnScope +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.defaultMinSize +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.ProvideTextStyle +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.StrokeCap +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.dergoogler.mmrl.R + +@Composable +fun PageIndicator( + icon: @Composable ColumnScope.() -> Unit, + text: @Composable ColumnScope.() -> Unit, + modifier: Modifier = Modifier, + minHeight: Dp? = null +) = Column( + modifier = modifier + then(if (minHeight != null) { + Modifier + .defaultMinSize(minHeight = minHeight) + .fillMaxWidth() + } else { + Modifier.fillMaxSize() + }), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center +) { + icon() + Spacer(modifier = Modifier.height(20.dp)) + ProvideTextStyle(value = PageIndicatorDefaults.textStyle) { + text() + } +} + +@Composable +fun PageIndicator( + @DrawableRes icon: Int, + text: String, + modifier: Modifier = Modifier, + minHeight: Dp? = null +) = PageIndicator( + modifier = modifier, + icon = { + Icon( + painter = painterResource(id = icon), + contentDescription = null, + tint = PageIndicatorDefaults.iconColor, + modifier = Modifier.size(PageIndicatorDefaults.iconSize) + ) + }, + text = { + Text( + text = text, + modifier = Modifier.padding(horizontal = 20.dp), + maxLines = 5, + overflow = TextOverflow.Ellipsis + ) + }, + minHeight = minHeight +) + +@Composable +fun PageIndicator( + @DrawableRes icon: Int, + @StringRes text: Int, + modifier: Modifier = Modifier, + minHeight: Dp? = null +) = PageIndicator( + modifier = modifier, + icon = icon, + text = stringResource(id = text), + minHeight = minHeight +) + +@Composable +fun Loading( + minHeight: Dp? = null +) = PageIndicator( + icon = { + CircularProgressIndicator( + modifier = Modifier.size(50.dp), + strokeWidth = 5.dp, + strokeCap = StrokeCap.Round + ) + }, + text = { + Text( + text = stringResource(id = R.string.message_loading), + style = MaterialTheme.typography.titleMedium, + color = MaterialTheme.colorScheme.outline + ) + }, + minHeight = minHeight +) + +@Composable +fun Failed( + message: String?, + minHeight: Dp? = null +) = PageIndicator( + icon = R.drawable.alert_triangle, + text = message ?: stringResource(id = R.string.unknown_error), + minHeight = minHeight +) + +object PageIndicatorDefaults { + val iconSize = 80.dp + val iconColor @Composable get() = MaterialTheme.colorScheme.outline.copy(0.5f) + + val textStyle @Composable get() = TextStyle( + color = MaterialTheme.colorScheme.outline.copy(0.5f), + fontSize = 20.sp, + fontFamily = FontFamily.SansSerif, + fontWeight = FontWeight.SemiBold, + textAlign = TextAlign.Center + ) +} \ No newline at end of file diff --git a/app/src/main/kotlin/com/dergoogler/mmrl/ui/component/SearchTopBar.kt b/app/src/main/kotlin/com/dergoogler/mmrl/ui/component/SearchTopBar.kt new file mode 100644 index 00000000..dd35afbb --- /dev/null +++ b/app/src/main/kotlin/com/dergoogler/mmrl/ui/component/SearchTopBar.kt @@ -0,0 +1,104 @@ +package com.dergoogler.mmrl.ui.component + +import androidx.compose.foundation.layout.RowScope +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.text.KeyboardActions +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.OutlinedTextFieldDefaults +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar +import androidx.compose.material3.TopAppBarColors +import androidx.compose.material3.TopAppBarDefaults +import androidx.compose.material3.TopAppBarScrollBehavior +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.focus.focusRequester +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalSoftwareKeyboardController +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.input.ImeAction +import androidx.compose.ui.text.input.KeyboardType +import androidx.compose.ui.unit.dp +import com.dergoogler.mmrl.R + +@Composable +fun SearchTopBar( + isSearch: Boolean, + query: String, + onQueryChange: (String) -> Unit, + onClose: () -> Unit, + title: @Composable () -> Unit, + modifier: Modifier = Modifier, + navigationIcon: @Composable () -> Unit = {}, + actions: @Composable RowScope.() -> Unit = {}, + windowInsets: WindowInsets = TopAppBarDefaults.windowInsets, + colors: TopAppBarColors = TopAppBarDefaults.topAppBarColors(), + scrollBehavior: TopAppBarScrollBehavior? = null +) = TopAppBar( + modifier = modifier, + actions = actions, + windowInsets = windowInsets, + colors = colors, + scrollBehavior = scrollBehavior, + navigationIcon = if (isSearch) { + { + IconButton( + onClick = onClose + ) { + Icon( + painter = painterResource(id = R.drawable.arrow_left), + contentDescription = null + ) + } + } + } else navigationIcon, + title = if (isSearch) { + { + val focusRequester = remember { FocusRequester() } + val keyboardController = LocalSoftwareKeyboardController.current + + LaunchedEffect(focusRequester) { + focusRequester.requestFocus() + keyboardController?.show() + } + + OutlinedTextField( + modifier = Modifier.focusRequester(focusRequester), + value = query, + onValueChange = onQueryChange, + keyboardOptions = KeyboardOptions( + keyboardType = KeyboardType.Text, + imeAction = ImeAction.Search + ), + keyboardActions = KeyboardActions { + defaultKeyboardAction(ImeAction.Search) + }, + shape = RoundedCornerShape(15.dp), + colors = OutlinedTextFieldDefaults.colors( + focusedBorderColor = Color.Transparent, + unfocusedBorderColor = Color.Transparent + ), + leadingIcon = { + Icon( + painter = painterResource(id = R.drawable.search), + contentDescription = null + ) + }, + placeholder = { + Text(text = stringResource(id = R.string.search_placeholder)) + }, + singleLine = true, + textStyle = MaterialTheme.typography.bodyLarge + ) + } + } else title +) \ No newline at end of file diff --git a/app/src/main/kotlin/com/dergoogler/mmrl/ui/component/SegmentedButtons.kt b/app/src/main/kotlin/com/dergoogler/mmrl/ui/component/SegmentedButtons.kt new file mode 100644 index 00000000..31f2d5fa --- /dev/null +++ b/app/src/main/kotlin/com/dergoogler/mmrl/ui/component/SegmentedButtons.kt @@ -0,0 +1,280 @@ +package com.dergoogler.mmrl.ui.component + +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.Crossfade +import androidx.compose.animation.ExitTransition +import androidx.compose.animation.core.tween +import androidx.compose.animation.fadeIn +import androidx.compose.animation.scaleIn +import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.Canvas +import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.RowScope +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.defaultMinSize +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.selection.selectableGroup +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Check +import androidx.compose.material.ripple.rememberRipple +import androidx.compose.material3.Icon +import androidx.compose.material3.LocalContentColor +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.ProvideTextStyle +import androidx.compose.material3.Surface +import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.Immutable +import androidx.compose.runtime.State +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberUpdatedState +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.Shape +import androidx.compose.ui.graphics.TransformOrigin +import androidx.compose.ui.layout.SubcomposeLayout +import androidx.compose.ui.semantics.Role +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp + +@Composable +fun SegmentedButtons( + modifier: Modifier = Modifier, + containerColor: Color = Color.Transparent, + contentColor: Color = LocalContentColor.current, + shape: Shape = SegmentedButtonsDefaults.Shape, + border: BorderStroke = SegmentedButtonsDefaults.border(), + segments: @Composable () -> Unit +) { + Surface( + modifier = modifier + .selectableGroup() + .defaultMinSize(minHeight = 40.dp), + shape = shape, + contentColor = contentColor, + color = containerColor, + border = border + ) { + SubcomposeLayout { constraints -> + val segmentsMeasurable = subcompose(SegmentSlots.Segment, segments) + val segmentCount = segmentsMeasurable.size + val segmentsPlaceable = segmentsMeasurable.map { + val width = it.maxIntrinsicWidth(constraints.maxHeight) + it.measure(constraints.copy(minWidth = 0, maxWidth = width)) + } + + val segmentedButtonsHeight = segmentsPlaceable.maxBy { it.height }.height + val segmentedButtonsWidth = segmentsPlaceable.sumOf { it.width } + + val divider = SegmentedButtonsDefaults.divider( + height = segmentedButtonsHeight.toDp(), + border = border + ) + val dividerWidth = border.width.roundToPx() + + layout(segmentedButtonsWidth, segmentedButtonsHeight) { + var x = 0 + segmentsPlaceable.forEachIndexed { index, placeableItem -> + placeableItem.placeRelative(x, 0) + x += placeableItem.width + + if (index == segmentCount - 1) return@forEachIndexed + + val dividerMeasurable = subcompose(index, divider) + val placeableDivider = dividerMeasurable + .first().measure(constraints.copy(minWidth = 0)) + + placeableDivider.placeRelative(x, 0) + x += dividerWidth + } + } + } + } +} + +@Composable +fun Segment( + selected: Boolean, + onClick: () -> Unit, + modifier: Modifier = Modifier, + enabled: Boolean = true, + colors: SegmentColors = SegmentedButtonsDefaults.buttonColor(), + contentPadding: PaddingValues = SegmentedButtonsDefaults.ContentPadding, + interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, + icon: (@Composable () -> Unit)? = { SegmentedButtonsDefaults.SegmentIcon(selected) }, + content: @Composable RowScope.() -> Unit +) { + val containerColor by colors.containerColor(selected) + val contentColor by colors.contentColor(selected) + + Surface( + modifier = modifier + .clickable( + enabled = enabled, + role = Role.Button, + onClick = onClick, + interactionSource = interactionSource, + indication = rememberRipple( + bounded = true, + color = colors.containerColor(selected).value + ) + ), + color = containerColor, + contentColor = contentColor, + ) { + CompositionLocalProvider(LocalContentColor provides contentColor) { + ProvideTextStyle(value = MaterialTheme.typography.labelLarge) { + Row( + modifier = Modifier.padding(contentPadding), + horizontalArrangement = Arrangement.Center, + verticalAlignment = Alignment.CenterVertically + ) { + if (icon != null) icon() + if (icon != null && selected) { + Spacer(modifier = Modifier.width(SegmentedButtonsDefaults.IconSpacing)) + } + + content() + } + } + } + } +} + +private enum class SegmentSlots { + Segment +} + +object SegmentedButtonsDefaults { + val IconSize = 18.dp + val IconSpacing = 8.dp + val Shape: Shape = CircleShape + val ContentPadding = PaddingValues(vertical = 8.dp, horizontal = 24.dp) + + @Composable + fun border( + width: Dp = 1.dp, + color: Color = MaterialTheme.colorScheme.outline + ) = BorderStroke( + width = width, + color = color + ) + + @Composable + fun buttonColor( + containerColor: Color = Color.Transparent, + contentColor: Color = MaterialTheme.colorScheme.onSurfaceVariant, + selectedContainerColor: Color = MaterialTheme.colorScheme.secondaryContainer, + selectedContentColor: Color = MaterialTheme.colorScheme.onSecondaryContainer + ) = SegmentColors( + containerColor = containerColor, + contentColor = contentColor, + selectedContainerColor = selectedContainerColor, + selectedContentColor = selectedContentColor + ) + + fun divider( + height: Dp, + border: BorderStroke + ) = @Composable { + Canvas( + modifier = Modifier + .height(height) + .width(border.width) + ) { + drawLine( + brush = border.brush, + start = Offset(center.x, 0f), + end = Offset(center.x, size.height), + strokeWidth = size.width + ) + } + } + + @Composable + fun ActiveIcon() { + Icon( + imageVector = Icons.Filled.Check, + contentDescription = null, + modifier = Modifier.size(IconSize) + ) + } + + @Composable + fun SegmentIcon( + active: Boolean, + activeContent: @Composable () -> Unit = { ActiveIcon() }, + inactiveContent: (@Composable () -> Unit)? = null + ) { + if (inactiveContent == null) { + AnimatedVisibility( + visible = active, + exit = ExitTransition.None, + enter = fadeIn(tween(350)) + scaleIn( + initialScale = 0f, + transformOrigin = TransformOrigin(0f, 1f), + animationSpec = tween(350), + ), + ) { + activeContent() + } + } else { + Crossfade( + targetState = active, + label = "SegmentIcon" + ) { + if (it) activeContent() else inactiveContent() + } + } + } +} + +@Immutable +class SegmentColors internal constructor( + private val containerColor: Color, + private val contentColor: Color, + private val selectedContainerColor: Color, + private val selectedContentColor: Color, +) { + @Composable + internal fun containerColor(selected: Boolean): State { + return rememberUpdatedState(if (selected) selectedContainerColor else containerColor) + } + + @Composable + internal fun contentColor(selected: Boolean): State { + return rememberUpdatedState(if (selected) selectedContentColor else contentColor) + } + + @Suppress("RedundantIf") + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other == null || other !is SegmentColors) return false + + if (containerColor != other.containerColor) return false + if (contentColor != other.contentColor) return false + if (selectedContainerColor != other.selectedContainerColor) return false + if (selectedContentColor != other.selectedContentColor) return false + + return true + } + + override fun hashCode(): Int { + var result = containerColor.hashCode() + result = 31 * result + contentColor.hashCode() + result = 31 * result + selectedContainerColor.hashCode() + result = 31 * result + selectedContentColor.hashCode() + return result + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/com/dergoogler/mmrl/ui/component/SettingItem.kt b/app/src/main/kotlin/com/dergoogler/mmrl/ui/component/SettingItem.kt new file mode 100644 index 00000000..972b75e4 --- /dev/null +++ b/app/src/main/kotlin/com/dergoogler/mmrl/ui/component/SettingItem.kt @@ -0,0 +1,212 @@ +package com.dergoogler.mmrl.ui.component + +import androidx.annotation.DrawableRes +import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.calculateStartPadding +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.selection.toggleable +import androidx.compose.material.ripple.rememberRipple +import androidx.compose.material3.Icon +import androidx.compose.material3.LocalContentColor +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Switch +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.Immutable +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.alpha +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalLayoutDirection +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.semantics.Role +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.unit.dp + +@Composable +fun SettingNormalItem( + title: String, + desc: String, + onClick: () -> Unit, + modifier: Modifier = Modifier, + contentPaddingValues: PaddingValues = PaddingValues(vertical = 16.dp, horizontal = 25.dp), + itemTextStyle: SettingItemTextStyle = SettingItemDefaults.itemStyle(), + interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, + @DrawableRes icon: Int? = null, + enabled: Boolean = true, +) { + val layoutDirection = LocalLayoutDirection.current + val start by remember { + derivedStateOf { contentPaddingValues.calculateStartPadding(layoutDirection) } + } + + Row( + modifier = modifier + .alpha(alpha = if (enabled) 1f else 0.5f) + .clickable( + enabled = enabled, + onClick = onClick, + interactionSource = interactionSource, + indication = rememberRipple() + ) + .padding(contentPaddingValues) + .fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically + ) { + icon?.let { + Icon( + modifier = Modifier.size(SettingItemDefaults.IconSize), + painter = painterResource(id = icon), + contentDescription = null, + tint = LocalContentColor.current + ) + + Spacer(modifier = Modifier.width(start)) + } + + Column( + modifier = Modifier.fillMaxWidth(), + verticalArrangement = Arrangement.Center + ) { + Text( + text = title, + style = itemTextStyle.titleTextStyle, + color = itemTextStyle.titleTextColor + ) + + Text( + text = desc, + style = itemTextStyle.descTextStyle, + color = itemTextStyle.descTextColor + ) + } + } +} + +@Composable +fun SettingSwitchItem( + title: String, + desc: String, + checked: Boolean, + onChange: (Boolean) -> Unit, + modifier: Modifier = Modifier, + contentPaddingValues: PaddingValues = PaddingValues(vertical = 16.dp, horizontal = 25.dp), + itemTextStyle: SettingItemTextStyle = SettingItemDefaults.itemStyle(), + interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, + @DrawableRes icon: Int? = null, + enabled: Boolean = true +) { + val layoutDirection = LocalLayoutDirection.current + val start by remember { + derivedStateOf { contentPaddingValues.calculateStartPadding(layoutDirection) } + } + + Row( + modifier = modifier + .alpha(alpha = if (enabled) 1f else 0.5f) + .toggleable( + value = checked, + enabled = enabled, + onValueChange = onChange, + role = Role.Switch, + interactionSource = interactionSource, + indication = rememberRipple() + ) + .padding(contentPaddingValues) + .fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically + ) { + icon?.let { + Icon( + modifier = Modifier.size(SettingItemDefaults.IconSize), + painter = painterResource(id = icon), + contentDescription = null + ) + + Spacer(modifier = Modifier.width(start)) + } + + Column( + modifier = Modifier + .weight(1f) + .padding(end = SettingItemDefaults.TextSwitchPadding), + verticalArrangement = Arrangement.Center + ) { + Text( + text = title, + style = itemTextStyle.titleTextStyle, + color = itemTextStyle.titleTextColor + ) + + Text( + text = desc, + style = itemTextStyle.descTextStyle, + color = itemTextStyle.descTextColor + ) + } + + Switch( + checked = checked, + onCheckedChange = null + ) + } +} + +@Immutable +class SettingItemTextStyle internal constructor( + val titleTextColor: Color, + val descTextColor: Color, + val titleTextStyle: TextStyle, + val descTextStyle: TextStyle +) { + @Suppress("RedundantIf") + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other == null || other !is SettingItemTextStyle) return false + + if (titleTextColor != other.titleTextColor) return false + if (descTextColor != other.descTextColor) return false + if (titleTextStyle != other.titleTextStyle) return false + if (descTextStyle != other.descTextStyle) return false + + return true + } + + override fun hashCode(): Int { + var result = titleTextColor.hashCode() + result = 31 * result + descTextColor.hashCode() + result = 31 * result + titleTextStyle.hashCode() + result = 31 * result + descTextStyle.hashCode() + return result + } +} + +object SettingItemDefaults { + val IconSize = 24.dp + val TextSwitchPadding = 16.dp + + @Composable + fun itemStyle( + titleTextColor: Color = LocalContentColor.current, + descTextColor: Color = MaterialTheme.colorScheme.outline, + titleTextStyle: TextStyle = MaterialTheme.typography.bodyLarge, + descTextStyle: TextStyle = MaterialTheme.typography.bodyMedium + ) = SettingItemTextStyle( + titleTextColor = titleTextColor, + descTextColor = descTextColor, + titleTextStyle = titleTextStyle, + descTextStyle = descTextStyle + ) +} \ No newline at end of file diff --git a/app/src/main/kotlin/com/dergoogler/mmrl/ui/component/Tab.kt b/app/src/main/kotlin/com/dergoogler/mmrl/ui/component/Tab.kt new file mode 100644 index 00000000..fac6a944 --- /dev/null +++ b/app/src/main/kotlin/com/dergoogler/mmrl/ui/component/Tab.kt @@ -0,0 +1,96 @@ +package com.dergoogler.mmrl.ui.component + +import androidx.compose.animation.animateColor +import androidx.compose.animation.core.LinearEasing +import androidx.compose.animation.core.tween +import androidx.compose.animation.core.updateTransition +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.ColumnScope +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.selection.selectable +import androidx.compose.material.ripple.rememberRipple +import androidx.compose.material3.LocalContentColor +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.ProvideTextStyle +import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.getValue +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.semantics.Role + +@Composable +fun Tab( + selected: Boolean, + onClick: () -> Unit, + modifier: Modifier = Modifier, + enabled: Boolean = true, + selectedContentColor: Color = LocalContentColor.current, + unselectedContentColor: Color = selectedContentColor, + interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, + content: @Composable ColumnScope.() -> Unit +) { + val ripple = rememberRipple(bounded = false, color = selectedContentColor) + + TabTransition(selectedContentColor, unselectedContentColor, selected) { + ProvideTextStyle(value = MaterialTheme.typography.titleSmall) { + Column( + modifier = Modifier + .selectable( + selected = selected, + onClick = onClick, + enabled = enabled, + role = Role.Tab, + interactionSource = interactionSource, + indication = ripple + ) + .then(modifier) + .fillMaxWidth(), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center, + content = content + ) + } + } +} + +@Composable +private fun TabTransition( + activeColor: Color, + inactiveColor: Color, + selected: Boolean, + content: @Composable () -> Unit +) { + val transition = updateTransition(selected, label = "Tab") + val color by transition.animateColor( + label = "color", + transitionSpec = { + if (false isTransitioningTo true) { + tween( + durationMillis = TabFadeInAnimationDuration, + delayMillis = TabFadeInAnimationDelay, + easing = LinearEasing + ) + } else { + tween( + durationMillis = TabFadeOutAnimationDuration, + easing = LinearEasing + ) + } + } + ) { + if (it) activeColor else inactiveColor + } + CompositionLocalProvider( + LocalContentColor provides color, + content = content + ) +} + +private const val TabFadeInAnimationDuration = 150 +private const val TabFadeInAnimationDelay = 100 +private const val TabFadeOutAnimationDuration = 100 \ No newline at end of file diff --git a/app/src/main/kotlin/com/dergoogler/mmrl/ui/component/Text.kt b/app/src/main/kotlin/com/dergoogler/mmrl/ui/component/Text.kt new file mode 100644 index 00000000..7dfa28cd --- /dev/null +++ b/app/src/main/kotlin/com/dergoogler/mmrl/ui/component/Text.kt @@ -0,0 +1,81 @@ +package com.dergoogler.mmrl.ui.component + +import android.text.method.LinkMovementMethod +import android.widget.TextView +import androidx.compose.material3.LocalContentColor +import androidx.compose.material3.LocalTextStyle +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.toArgb +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.viewinterop.AndroidView +import androidx.core.text.HtmlCompat +import io.noties.markwon.Markwon + +@Composable +internal fun ProvideContentColorTextStyle( + contentColor: Color, + textStyle: TextStyle, + content: @Composable () -> Unit +) { + val mergedStyle = LocalTextStyle.current.merge(textStyle) + CompositionLocalProvider( + LocalContentColor provides contentColor, + LocalTextStyle provides mergedStyle, + content = content + ) +} + +@Composable +fun HtmlText( + text: String, + modifier: Modifier = Modifier, + style: TextStyle = LocalTextStyle.current, + color: Color = LocalContentColor.current, +) { + val linkTextColor = MaterialTheme.colorScheme.primary.toArgb() + AndroidView( + modifier = modifier, + factory = { TextView(it) }, + update = { + it.movementMethod = LinkMovementMethod.getInstance() + it.setLinkTextColor(linkTextColor) + it.highlightColor = style.background.toArgb() + + it.textSize = style.fontSize.value + it.setTextColor(color.toArgb()) + it.setBackgroundColor(style.background.toArgb()) + it.text = HtmlCompat.fromHtml(text, HtmlCompat.FROM_HTML_MODE_COMPACT) + } + ) +} + +@Composable +fun MarkdownText( + text: String, + modifier: Modifier = Modifier, + style: TextStyle = LocalTextStyle.current, + color: Color = LocalContentColor.current, +) { + val context = LocalContext.current + val markdown = Markwon.create(context) + val linkTextColor = MaterialTheme.colorScheme.primary.toArgb() + + AndroidView( + modifier = modifier, + factory = { TextView(it) }, + update = { + it.setLinkTextColor(linkTextColor) + it.highlightColor = style.background.toArgb() + + it.textSize = style.fontSize.value + it.setTextColor(color.toArgb()) + it.setBackgroundColor(style.background.toArgb()) + markdown.setMarkdown(it, text) + } + ) +} \ No newline at end of file diff --git a/app/src/main/kotlin/com/dergoogler/mmrl/ui/component/TextFieldDialog.kt b/app/src/main/kotlin/com/dergoogler/mmrl/ui/component/TextFieldDialog.kt new file mode 100644 index 00000000..607b401c --- /dev/null +++ b/app/src/main/kotlin/com/dergoogler/mmrl/ui/component/TextFieldDialog.kt @@ -0,0 +1,147 @@ +package com.dergoogler.mmrl.ui.component + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.requiredWidth +import androidx.compose.foundation.layout.wrapContentHeight +import androidx.compose.material3.AlertDialogDefaults +import androidx.compose.material3.BasicAlertDialog +import androidx.compose.material3.LocalContentColor +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.ProvideTextStyle +import androidx.compose.material3.Surface +import androidx.compose.material3.TextFieldDefaults +import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.Shape +import androidx.compose.ui.platform.LocalSoftwareKeyboardController +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import androidx.compose.ui.window.DialogProperties + +@Composable +fun TextFieldDialog( + onDismissRequest: () -> Unit, + confirmButton: @Composable () -> Unit, + modifier: Modifier = Modifier, + dismissButton: @Composable (() -> Unit)? = null, + icon: @Composable (() -> Unit)? = null, + title: @Composable (() -> Unit)? = null, + shape: Shape = AlertDialogDefaults.shape, + containerColor: Color = AlertDialogDefaults.containerColor, + iconContentColor: Color = AlertDialogDefaults.iconContentColor, + titleContentColor: Color = AlertDialogDefaults.titleContentColor, + textContentColor: Color = AlertDialogDefaults.textContentColor, + tonalElevation: Dp = AlertDialogDefaults.TonalElevation, + properties: DialogProperties = DialogProperties(usePlatformDefaultWidth = false), + launchKeyboard: Boolean = true, + content: @Composable (FocusRequester) -> Unit +) = BasicAlertDialog( + onDismissRequest = onDismissRequest, + modifier = modifier.wrapContentHeight(), + properties = properties +) { + val focusRequester = remember { FocusRequester() } + val keyboardController = LocalSoftwareKeyboardController.current + + LaunchedEffect(focusRequester) { + if (launchKeyboard) { + focusRequester.requestFocus() + keyboardController?.show() + } + } + + Surface( + modifier = modifier.requiredWidth(TextFieldDefaults.MinWidth + 40.dp), + shape = shape, + color = containerColor, + tonalElevation = tonalElevation, + ) { + Column( + modifier = Modifier.padding(DialogPadding) + ) { + icon?.let { + CompositionLocalProvider(LocalContentColor provides iconContentColor) { + Box( + Modifier + .padding(IconPadding) + .align(Alignment.CenterHorizontally) + ) { + icon() + } + } + } + title?.let { + CompositionLocalProvider(LocalContentColor provides titleContentColor) { + val textStyle = MaterialTheme.typography.headlineSmall + ProvideTextStyle(textStyle) { + Box( + // Align the title to the center when an icon is present. + Modifier + .padding(TitlePadding) + .align( + if (icon == null) { + Alignment.Start + } else { + Alignment.CenterHorizontally + } + ) + ) { + title() + } + } + } + } + CompositionLocalProvider(LocalContentColor provides textContentColor) { + val textStyle = MaterialTheme.typography.bodyMedium + ProvideTextStyle(textStyle) { + Box( + Modifier + .weight(weight = 1f, fill = false) + .padding(TextPadding) + .align(Alignment.Start) + ) { + content(focusRequester) + } + } + } + + Box(modifier = Modifier.align(Alignment.End)) { + CompositionLocalProvider( + LocalContentColor provides MaterialTheme.colorScheme.primary + ) { + val textStyle = MaterialTheme.typography.labelLarge + ProvideTextStyle(value = textStyle) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(ButtonsMainAxisSpacing) + ) { + Spacer(modifier = Modifier.weight(1f)) + + dismissButton?.invoke() + confirmButton() + } + } + } + } + } + } +} + +private val DialogPadding = PaddingValues(all = 24.dp) +private val IconPadding = PaddingValues(bottom = 16.dp) +private val TitlePadding = PaddingValues(bottom = 16.dp) +private val TextPadding = PaddingValues(bottom = 24.dp) + +private val ButtonsMainAxisSpacing = 8.dp \ No newline at end of file diff --git a/app/src/main/kotlin/com/dergoogler/mmrl/ui/component/VersionItemBottomSheet.kt b/app/src/main/kotlin/com/dergoogler/mmrl/ui/component/VersionItemBottomSheet.kt new file mode 100644 index 00000000..c48c2879 --- /dev/null +++ b/app/src/main/kotlin/com/dergoogler/mmrl/ui/component/VersionItemBottomSheet.kt @@ -0,0 +1,295 @@ +package com.dergoogler.mmrl.ui.component + +import androidx.annotation.DrawableRes +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.animateContentSize +import androidx.compose.animation.core.Spring +import androidx.compose.animation.core.spring +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.ColumnScope +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.AlertDialogDefaults +import androidx.compose.material3.BottomSheetDefaults +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.ModalBottomSheet +import androidx.compose.material3.OutlinedButton +import androidx.compose.material3.SheetState +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.material3.rememberModalBottomSheetState +import androidx.compose.runtime.Composable +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.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.alpha +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import com.dergoogler.mmrl.R +import com.dergoogler.mmrl.app.Event.Companion.isLoading +import com.dergoogler.mmrl.app.Event.Companion.isSucceeded +import com.dergoogler.mmrl.model.online.VersionItem +import com.dergoogler.mmrl.network.compose.requestString +import com.dergoogler.mmrl.ui.utils.expandedShape +import kotlinx.coroutines.launch + +@Composable +fun VersionItemBottomSheet( + state: SheetState = rememberModalBottomSheetState(), + isUpdate: Boolean, + item: VersionItem, + isProviderAlive: Boolean, + onDownload: (Boolean) -> Unit, + onClose: () -> Unit +) { + val hasChangelog by remember { + derivedStateOf { item.changelog.isNotBlank() } + } + + ModalBottomSheet( + onDismissRequest = onClose, + sheetState = state, + shape = BottomSheetDefaults.expandedShape(15.dp), + windowInsets = WindowInsets(0), + dragHandle = { + if (hasChangelog) { + BottomSheetDefaults.DragHandle() + } else { + Text( + modifier = Modifier + .padding(all = 18.dp) + .fillMaxWidth(), + text = stringResource(id = R.string.view_module_version_dialog_desc), + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + } + ) { + when { + hasChangelog -> { + ButtonRow( + isUpdate = isUpdate, + enableInstall = isProviderAlive, + state = state, + onDownload = onDownload, + onClose = onClose + ) + ChangelogItem(url = item.changelog) + } + + else -> { + ButtonColumn( + isUpdate = isUpdate, + enableInstall = isProviderAlive, + state = state, + downloader = onDownload, + onClose = onClose + ) + } + } + + NavigationBarsSpacer() + } +} + +@Composable +private fun ColumnScope.ButtonRow( + isUpdate: Boolean, + enableInstall: Boolean, + state: SheetState, + onDownload: (Boolean) -> Unit, + onClose: () -> Unit +) = Row( + modifier = Modifier + .padding(horizontal = 18.dp) + .padding(bottom = 18.dp) + .align(Alignment.CenterHorizontally), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(10.dp) +) { + val scope = rememberCoroutineScope() + + OutlinedButton( + enabled = enableInstall, + onClick = { + onDownload(true) + scope.launch { + onClose() + state.hide() + } + }, + contentPadding = PaddingValues(vertical = 8.dp, horizontal = 16.dp) + ) { + Icon( + modifier = Modifier.size(20.dp), + painter = painterResource(id = R.drawable.device_mobile_down), + contentDescription = null + ) + + Spacer(modifier = Modifier.width(ButtonDefaults.IconSpacing)) + + Text( + text = stringResource(id = if (isUpdate) { + R.string.module_update + } else { + R.string.module_install + }) + ) + } + + OutlinedButton( + onClick = { + onDownload(false) + scope.launch { + onClose() + state.hide() + } + }, + contentPadding = PaddingValues(vertical = 8.dp, horizontal = 16.dp) + ) { + Icon( + modifier = Modifier.size(20.dp), + painter = painterResource(id = R.drawable.file_download), + contentDescription = null + ) + + Spacer(modifier = Modifier.width(ButtonDefaults.IconSpacing)) + + Text(text = stringResource(id = R.string.module_download)) + } +} + +@Composable +private fun ChangelogItem(url: String) { + var changelog by remember { mutableStateOf("") } + val event = requestString( + url = url, + onSuccess = { changelog = it } + ) + + Box( + modifier = Modifier + .animateContentSize(spring(stiffness = Spring.StiffnessLow)) + ) { + AnimatedVisibility( + visible = event.isLoading, + enter = fadeIn(), + exit = fadeOut() + ) { + Loading(minHeight = 200.dp) + } + + AnimatedVisibility( + visible = event.isSucceeded, + enter = fadeIn(), + exit = fadeOut() + ) { + MarkdownText( + text = changelog, + color = AlertDialogDefaults.textContentColor, + style = MaterialTheme.typography.bodyMedium, + modifier = Modifier + .verticalScroll(rememberScrollState()) + .padding(horizontal = 18.dp) + .padding(bottom = 18.dp) + ) + } + } +} + +@Composable +private fun ColumnScope.ButtonColumn( + isUpdate: Boolean, + enableInstall: Boolean, + state: SheetState, + downloader: (Boolean) -> Unit, + onClose: () -> Unit +) = Column( + modifier = Modifier + .padding(bottom = 18.dp) + .align(Alignment.CenterHorizontally), + horizontalAlignment = Alignment.CenterHorizontally +) { + val scope = rememberCoroutineScope() + + ButtonItem( + enabled = enableInstall, + onClick = { + downloader(true) + scope.launch { + onClose() + state.hide() + } + }, + icon = R.drawable.device_mobile_down, + text = stringResource(id = if (isUpdate) { + R.string.module_update + } else { + R.string.module_install + }) + ) + + ButtonItem( + onClick = { + downloader(false) + scope.launch { + onClose() + state.hide() + } + }, + icon = R.drawable.file_download, + text = stringResource(id = R.string.module_download) + ) +} + +@Composable +private fun ButtonItem( + onClick: () -> Unit, + enabled: Boolean = true, + @DrawableRes icon: Int, + text: String +) = Surface( + onClick = onClick, + enabled = enabled, + modifier = Modifier + .alpha(if (enabled) 1f else 0.5f) + .fillMaxWidth() +) { + Row( + modifier = Modifier.padding(vertical = 16.dp, horizontal = 18.dp), + horizontalArrangement = Arrangement.Start, + verticalAlignment = Alignment.CenterVertically + ) { + Icon( + modifier = Modifier.size(20.dp), + painter = painterResource(id = icon), + contentDescription = null + ) + Spacer(modifier = Modifier.width(16.dp)) + Text( + text = text, + style = MaterialTheme.typography.labelLarge + ) + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/com/dergoogler/mmrl/ui/component/scrollbar/FastScrollbar.kt b/app/src/main/kotlin/com/dergoogler/mmrl/ui/component/scrollbar/FastScrollbar.kt new file mode 100644 index 00000000..8a2b3649 --- /dev/null +++ b/app/src/main/kotlin/com/dergoogler/mmrl/ui/component/scrollbar/FastScrollbar.kt @@ -0,0 +1,236 @@ +package com.dergoogler.mmrl.ui.component.scrollbar + +import androidx.compose.animation.animateColorAsState +import androidx.compose.animation.core.Spring +import androidx.compose.animation.core.SpringSpec +import androidx.compose.foundation.background +import androidx.compose.foundation.gestures.Orientation +import androidx.compose.foundation.gestures.ScrollableState +import androidx.compose.foundation.interaction.InteractionSource +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.interaction.collectIsDraggedAsState +import androidx.compose.foundation.interaction.collectIsHoveredAsState +import androidx.compose.foundation.interaction.collectIsPressedAsState +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.surfaceColorAtElevation +import androidx.compose.runtime.Composable +import androidx.compose.runtime.Immutable +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.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import kotlinx.coroutines.delay + +@Composable +fun VerticalFastScrollbar( + state: LazyListState, + modifier: Modifier = Modifier, + contentPadding: PaddingValues = PaddingValues(horizontal = 2.dp), + interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, + colors: ScrollbarColors = ScrollbarDefaults.colors(), + thumb: @Composable () -> Unit = { + ScrollbarDefaults.Thumb( + color = colors.thumbColor( + canScrollForward = state.canScrollForward, + isScrollInProgress = state.isScrollInProgress, + interactionSource = interactionSource + ), + orientation = Orientation.Vertical, + ) + } +) { + val scrollbarState = state.scrollbarState() + val reverseLayout by remember(state) { + derivedStateOf { + state.layoutInfo.reverseLayout + } + } + + state.FastScrollbar( + modifier = Modifier + .fillMaxHeight() + .padding(contentPadding) + .then(modifier), + state = scrollbarState, + orientation = Orientation.Vertical, + onThumbMoved = state.rememberDraggableScroller(), + reverseLayout = reverseLayout, + colors = colors, + thumb = thumb, + interactionSource = interactionSource + ) +} + +@Composable +private fun ScrollableState.FastScrollbar( + state: ScrollbarState, + orientation: Orientation, + reverseLayout: Boolean, + onThumbMoved: (Float) -> Unit, + modifier: Modifier, + colors: ScrollbarColors, + thumb: @Composable () -> Unit, + interactionSource: MutableInteractionSource +) = Scrollbar( + modifier = modifier, + orientation = orientation, + interactionSource = interactionSource, + state = state, + thumb = thumb, + backgroundColor = colors.trackColor( + canScrollForward = canScrollForward, + isScrollInProgress = isScrollInProgress, + interactionSource = interactionSource + ), + onThumbMoved = onThumbMoved, + reverseLayout = reverseLayout +) + +@Immutable +class ScrollbarColors internal constructor( + private val contentColor: Color, + private val activeContentColor: Color, + private val containerColor: Color, + private val activeContainerColor: Color +) { + @Composable + private fun scrollbarColor( + color1: Color, + color2: Color, + canScrollForward: Boolean, + isScrollInProgress: Boolean, + interactionSource: InteractionSource, + ): Color { + var state by remember { mutableStateOf(ThumbState.Dormant) } + val pressed by interactionSource.collectIsPressedAsState() + val hovered by interactionSource.collectIsHoveredAsState() + val dragged by interactionSource.collectIsDraggedAsState() + val active = canScrollForward && (pressed || hovered || dragged || isScrollInProgress) + + val color by animateColorAsState( + targetValue = when (state) { + ThumbState.Active -> color1 + ThumbState.Inactive -> color2 + ThumbState.Dormant -> color2.copy(0f) + }, + animationSpec = SpringSpec(stiffness = Spring.StiffnessLow), + label = "scrollbarColor" + ) + LaunchedEffect(active) { + when (active) { + true -> state = ThumbState.Active + false -> if (state == ThumbState.Active) { + state = ThumbState.Inactive + delay(SCROLLBAR_INACTIVE_TO_DORMANT_TIME_IN_MS) + state = ThumbState.Dormant + } + } + } + + return color + } + + @Composable + fun thumbColor( + canScrollForward: Boolean, + isScrollInProgress: Boolean, + interactionSource: InteractionSource + ): Color = scrollbarColor( + color1 = activeContentColor, + color2 = contentColor, + canScrollForward = canScrollForward, + isScrollInProgress = isScrollInProgress, + interactionSource = interactionSource + ) + + @Composable + fun trackColor( + canScrollForward: Boolean, + isScrollInProgress: Boolean, + interactionSource: InteractionSource + ): Color = scrollbarColor( + color1 = activeContainerColor, + color2 = containerColor, + canScrollForward = canScrollForward, + isScrollInProgress = isScrollInProgress, + interactionSource = interactionSource + ) + + @Suppress("RedundantIf") + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other == null || other !is ScrollbarColors) return false + + if (contentColor != other.contentColor) return false + if (activeContentColor != other.activeContentColor) return false + if (containerColor != other.containerColor) return false + if (activeContainerColor != other.activeContainerColor) return false + + return true + } + + override fun hashCode(): Int { + var result = contentColor.hashCode() + result = 31 * result + activeContentColor.hashCode() + result = 31 * result + containerColor.hashCode() + result = 31 * result + activeContainerColor.hashCode() + + return result + } + + companion object { + private const val SCROLLBAR_INACTIVE_TO_DORMANT_TIME_IN_MS = 2_000L + private enum class ThumbState { + Active, Inactive, Dormant + } + } +} + +object ScrollbarDefaults { + @Composable + fun colors( + contentColor: Color = MaterialTheme.colorScheme.primary, + scrolledContentColor: Color = contentColor, + containerColor: Color = MaterialTheme.colorScheme.surfaceColorAtElevation(10.dp), + scrolledContainerColor: Color = containerColor + ) = ScrollbarColors( + contentColor, + scrolledContentColor, + containerColor, + scrolledContainerColor + ) + + @Composable + fun Thumb( + color: Color, + orientation: Orientation, + size: Dp = 8.dp + ) = Box( + modifier = Modifier + .run { + when (orientation) { + Orientation.Vertical -> width(size).fillMaxHeight() + Orientation.Horizontal -> height(size).fillMaxWidth() + } + } + .background( + color = color, + shape = CircleShape + ) + ) +} \ No newline at end of file diff --git a/app/src/main/kotlin/com/dergoogler/mmrl/ui/component/scrollbar/LazyScrollbarExt.kt b/app/src/main/kotlin/com/dergoogler/mmrl/ui/component/scrollbar/LazyScrollbarExt.kt new file mode 100644 index 00000000..0632971d --- /dev/null +++ b/app/src/main/kotlin/com/dergoogler/mmrl/ui/component/scrollbar/LazyScrollbarExt.kt @@ -0,0 +1,159 @@ +/* + * Copyright 2023 Sanmer + * + * 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 . + * + * Portions of this software are based on work by The Android Open Source Project, + * which is licensed under the Apache License, Version 2.0. You may obtain a copy + * of the Apache License, Version 2.0 at . + */ + +package com.dergoogler.mmrl.ui.component.scrollbar + +import androidx.compose.foundation.gestures.ScrollableState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.runtime.snapshotFlow +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.filterNotNull +import kotlin.math.abs +import kotlin.math.min + +/** + * Calculates the [ScrollbarState] for lazy layouts. + * @param itemsAvailable the total amount of items available to scroll in the layout. + * @param visibleItems a list of items currently visible in the layout. + * @param firstVisibleItemIndex a function for interpolating the first visible index in the lazy layout + * as scrolling progresses for smooth and linear scrollbar thumb progression. + * */ +@Composable +internal inline fun LazyState.scrollbarState( + itemsAvailable: Int, + crossinline visibleItems: LazyState.() -> List, + crossinline firstVisibleItemIndex: LazyState.(List) -> Float, + crossinline itemPercentVisible: LazyState.(LazyStateItem) -> Float +): ScrollbarState { + var state by remember { mutableStateOf(ScrollbarState.FULL) } + + LaunchedEffect( + key1 = this, + key2 = itemsAvailable, + ) { + snapshotFlow { + if (itemsAvailable == 0) return@snapshotFlow null + + val visibleItemsInfo = visibleItems(this@scrollbarState) + if (visibleItemsInfo.isEmpty()) return@snapshotFlow null + + val firstIndex = min( + a = firstVisibleItemIndex(visibleItemsInfo), + b = itemsAvailable.toFloat(), + ) + if (firstIndex.isNaN()) return@snapshotFlow null + + val itemsVisible = visibleItemsInfo.sumOf { + itemPercentVisible(it).toDouble() + }.toFloat() + + val thumbTravelPercent = min( + a = firstIndex / itemsAvailable, + b = 1f, + ) + val thumbSizePercent = min( + a = itemsVisible / itemsAvailable, + b = 1f, + ) + ScrollbarState( + thumbSizePercent = thumbSizePercent, + thumbMovedPercent = thumbTravelPercent + ) + } + .filterNotNull() + .distinctUntilChanged() + .collect { state = it } + } + return state +} + +/** + * Linearly interpolates the index for the first item in [visibleItems] for smooth scrollbar + * progression. + * @param visibleItems a list of items currently visible in the layout. + * @param itemSize a lookup function for the size of an item in the layout. + * @param offset a lookup function for the offset of an item relative to the start of the view port. + * @param nextItemOnMainAxis a lookup function for the next item on the main axis in the direction + * of the scroll. + * @param itemIndex a lookup function for index of an item in the layout relative to + * the total amount of items available. + * + * @return a [Float] in the range [firstItemPosition..nextItemPosition) where nextItemPosition + * is the index of the consecutive item along the major axis. + * */ +internal inline fun LazyState.interpolateFirstItemIndex( + visibleItems: List, + crossinline itemSize: LazyState.(LazyStateItem) -> Int, + crossinline offset: LazyState.(LazyStateItem) -> Int, + crossinline nextItemOnMainAxis: LazyState.(LazyStateItem) -> LazyStateItem?, + crossinline itemIndex: (LazyStateItem) -> Int, +): Float { + if (visibleItems.isEmpty()) return 0f + + val firstItem = visibleItems.first() + val firstItemIndex = itemIndex(firstItem) + + if (firstItemIndex < 0) return Float.NaN + + val firstItemSize = itemSize(firstItem) + if (firstItemSize == 0) return Float.NaN + + val itemOffset = offset(firstItem).toFloat() + val offsetPercentage = abs(itemOffset) / firstItemSize + + val nextItem = nextItemOnMainAxis(firstItem) ?: return firstItemIndex + offsetPercentage + + val nextItemIndex = itemIndex(nextItem) + + return firstItemIndex + ((nextItemIndex - firstItemIndex) * offsetPercentage) +} + +/** + * Returns the percentage of an item that is currently visible in the view port. + * @param itemSize the size of the item + * @param itemStartOffset the start offset of the item relative to the view port start + * @param viewportStartOffset the start offset of the view port + * @param viewportEndOffset the end offset of the view port + */ +internal fun itemVisibilityPercentage( + itemSize: Int, + itemStartOffset: Int, + viewportStartOffset: Int, + viewportEndOffset: Int, +): Float { + if (itemSize == 0) return 0f + val itemEnd = itemStartOffset + itemSize + val startOffset = when { + itemStartOffset > viewportStartOffset -> 0 + else -> abs(abs(viewportStartOffset) - abs(itemStartOffset)) + } + val endOffset = when { + itemEnd < viewportEndOffset -> 0 + else -> abs(abs(itemEnd) - abs(viewportEndOffset)) + } + val size = itemSize.toFloat() + return (size - startOffset - endOffset) / size +} \ No newline at end of file diff --git a/app/src/main/kotlin/com/dergoogler/mmrl/ui/component/scrollbar/Scrollbar.kt b/app/src/main/kotlin/com/dergoogler/mmrl/ui/component/scrollbar/Scrollbar.kt new file mode 100644 index 00000000..a97a2735 --- /dev/null +++ b/app/src/main/kotlin/com/dergoogler/mmrl/ui/component/scrollbar/Scrollbar.kt @@ -0,0 +1,451 @@ +/* + * Copyright 2023 Sanmer + * + * 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 . + * + * Portions of this software are based on work by The Android Open Source Project, + * which is licensed under the Apache License, Version 2.0. You may obtain a copy + * of the Apache License, Version 2.0 at . + */ + +package com.dergoogler.mmrl.ui.component.scrollbar + +import androidx.compose.animation.core.animateDpAsState +import androidx.compose.foundation.background +import androidx.compose.foundation.gestures.Orientation +import androidx.compose.foundation.gestures.detectHorizontalDragGestures +import androidx.compose.foundation.gestures.detectTapGestures +import androidx.compose.foundation.gestures.detectVerticalDragGestures +import androidx.compose.foundation.hoverable +import androidx.compose.foundation.interaction.DragInteraction +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.interaction.PressInteraction +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.offset +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.runtime.Composable +import androidx.compose.runtime.Immutable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableFloatStateOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberUpdatedState +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.input.pointer.PointerInputChange +import androidx.compose.ui.input.pointer.pointerInput +import androidx.compose.ui.layout.onGloballyPositioned +import androidx.compose.ui.layout.positionInRoot +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.IntOffset +import androidx.compose.ui.unit.IntSize +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.max +import androidx.compose.ui.util.packFloats +import androidx.compose.ui.util.unpackFloat1 +import androidx.compose.ui.util.unpackFloat2 +import kotlinx.coroutines.TimeoutCancellationException +import kotlinx.coroutines.delay +import kotlinx.coroutines.withTimeout +import kotlin.math.max +import kotlin.math.min + +/** + * The delay between scrolls when a user long presses on the scrollbar track to initiate a scroll + * instead of dragging the scrollbar thumb. + */ +private const val SCROLLBAR_PRESS_DELAY_MS = 10L + +/** + * The percentage displacement of the scrollbar when scrolled by long presses on the scrollbar + * track. + */ +private const val SCROLLBAR_PRESS_DELTA_PCT = 0.02f + +/** + * Class definition for the core properties of a scroll bar + */ +@Immutable +@JvmInline +value class ScrollbarState internal constructor( + internal val packedValue: Long +) { + companion object { + val FULL = ScrollbarState( + thumbSizePercent = 1f, + thumbMovedPercent = 0f + ) + } +} + +/** + * Class definition for the core properties of a scroll bar track + */ +@Immutable +@JvmInline +private value class ScrollbarTrack( + val packedValue: Long +) { + constructor( + max: Float, + min: Float + ) : this(packFloats(max, min)) +} + +/** + * Creates a [ScrollbarState] with the listed properties + * @param thumbSizePercent the thumb size of the scrollbar as a percentage of the total track size. + * Refers to either the thumb width (for horizontal scrollbars) + * or height (for vertical scrollbars). + * @param thumbMovedPercent the distance the thumb has traveled as a percentage of total + * track size. + */ +fun ScrollbarState( + thumbSizePercent: Float, + thumbMovedPercent: Float +) = ScrollbarState( + packedValue = packFloats( + val1 = thumbSizePercent, + val2 = thumbMovedPercent + ) +) + +/** + * Returns the thumb size of the scrollbar as a percentage of the total track size + */ +val ScrollbarState.thumbSizePercent + get() = unpackFloat1(packedValue) + +/** + * Returns the distance the thumb has traveled as a percentage of total track size + */ +val ScrollbarState.thumbMovedPercent + get() = unpackFloat2(packedValue) + +/** + * Returns the size of the scrollbar track in pixels + */ +private val ScrollbarTrack.size + get() = unpackFloat2(packedValue) - unpackFloat1(packedValue) + +/** + * Returns the position of the scrollbar thumb on the track as a percentage + */ +private fun ScrollbarTrack.thumbPosition( + dimension: Float +): Float = max( + a = min( + a = dimension / size, + b = 1f, + ), + b = 0f, +) + +/** + * Returns the value of [offset] along the axis specified by [this] + */ +internal fun Orientation.valueOf(offset: Offset) = when (this) { + Orientation.Horizontal -> offset.x + Orientation.Vertical -> offset.y +} + +/** + * Returns the value of [intSize] along the axis specified by [this] + */ +internal fun Orientation.valueOf(intSize: IntSize) = when (this) { + Orientation.Horizontal -> intSize.width + Orientation.Vertical -> intSize.height +} + +/** + * Returns the value of [intOffset] along the axis specified by [this] + */ +internal fun Orientation.valueOf(intOffset: IntOffset) = when (this) { + Orientation.Horizontal -> intOffset.x + Orientation.Vertical -> intOffset.y +} + +/** + * A Composable for drawing a scrollbar + * @param orientation the scroll direction of the scrollbar + * @param state the state describing the position of the scrollbar + * @param backgroundColor the color of the background for scrollbar + * @param thumbSize the size of the scrollbar thumb + * @param interactionSource allows for observing the state of the scroll bar + * @param thumb a composable for drawing the scrollbar thumb + * @param onThumbMoved an function for reacting to scroll bar displacements caused by direct + * interactions on the scrollbar thumb by the user, for example implementing a fast scroll + * @param reverseLayout reverse the direction of scrolling and layout + */ +@Composable +fun Scrollbar( + modifier: Modifier, + orientation: Orientation, + state: ScrollbarState, + backgroundColor: Color, + thumbSize: Dp = 50.dp, + interactionSource: MutableInteractionSource?, + thumb: @Composable () -> Unit, + onThumbMoved: ((Float) -> Unit)?, + reverseLayout: Boolean +) { + // Only displayed when thumb size > 0.50 + if (state.thumbSizePercent > 0.50) return + + // Using Offset.Unspecified and Float.NaN instead of null + // to prevent unnecessary boxing of primitives + var pressedOffset by remember { mutableStateOf(Offset.Unspecified) } + var draggedOffset by remember { mutableStateOf(Offset.Unspecified) } + + // Used to immediately show drag feedback in the UI while the scrolling implementation + // catches up + var interactionThumbTravelPercent by remember { mutableFloatStateOf(Float.NaN) } + + var track by remember { mutableStateOf(ScrollbarTrack(packedValue = 0)) } + + val thumbSizePx = with(LocalDensity.current) { thumbSize.toPx() } + val oldThumbSizePx = state.thumbSizePercent * track.size + + val thumbTravelPercent = when { + interactionThumbTravelPercent.isNaN() -> { + // Calculate the resize percentage + val p = (track.size - thumbSizePx) / (track.size - oldThumbSizePx) + val new = state.thumbMovedPercent * p + + val thumbMovedPercent = when { + new.isNaN() -> 0f + new.isInfinite() -> 1f + else -> new + } + + when { + reverseLayout -> 1f - thumbMovedPercent + else -> thumbMovedPercent + } + } + else -> interactionThumbTravelPercent + } + + val thumbSizeDp by animateDpAsState( + targetValue = with(LocalDensity.current) { thumbSizePx.toDp() }, + label = "thumbSizeDp", + ) + + val thumbMovedPx = min( + a = when { + reverseLayout -> track.size * thumbTravelPercent - thumbSizePx + else -> track.size * thumbTravelPercent + }, + b = track.size - thumbSizePx + ) + + // scrollbar track container + Box( + modifier = modifier + .run { + val withHover = interactionSource?.let(::hoverable) ?: this + when (orientation) { + Orientation.Vertical -> withHover.fillMaxHeight() + Orientation.Horizontal -> withHover.fillMaxWidth() + } + } + .onGloballyPositioned { coordinates -> + val scrollbarStartCoordinate = orientation.valueOf(coordinates.positionInRoot()) + track = ScrollbarTrack( + max = scrollbarStartCoordinate, + min = scrollbarStartCoordinate + orientation.valueOf(coordinates.size), + ) + } + // Process scrollbar presses + .pointerInput(Unit) { + detectTapGestures( + onPress = { offset -> + try { + // Wait for a long press before scrolling + withTimeout(viewConfiguration.longPressTimeoutMillis) { + tryAwaitRelease() + } + } catch (e: TimeoutCancellationException) { + // Start the press triggered scroll + val initialPress = PressInteraction.Press(offset) + interactionSource?.tryEmit(initialPress) + + pressedOffset = offset + interactionSource?.tryEmit( + when { + tryAwaitRelease() -> PressInteraction.Release(initialPress) + else -> PressInteraction.Cancel(initialPress) + }, + ) + + // End the press + pressedOffset = Offset.Unspecified + } + }, + ) + } + // Process scrollbar drags + .pointerInput(Unit) { + var dragInteraction: DragInteraction.Start? = null + val onDragStart: (Offset) -> Unit = { offset -> + val start = DragInteraction.Start() + dragInteraction = start + interactionSource?.tryEmit(start) + draggedOffset = offset + } + val onDragEnd: () -> Unit = { + dragInteraction?.let { interactionSource?.tryEmit(DragInteraction.Stop(it)) } + draggedOffset = Offset.Unspecified + } + val onDragCancel: () -> Unit = { + dragInteraction?.let { interactionSource?.tryEmit(DragInteraction.Cancel(it)) } + draggedOffset = Offset.Unspecified + } + val onDrag: (change: PointerInputChange, dragAmount: Float) -> Unit = + onDrag@{ _, delta -> + if (draggedOffset == Offset.Unspecified) return@onDrag + draggedOffset = when (orientation) { + Orientation.Vertical -> draggedOffset.copy( + y = draggedOffset.y + delta, + ) + + Orientation.Horizontal -> draggedOffset.copy( + x = draggedOffset.x + delta, + ) + } + } + + when (orientation) { + Orientation.Horizontal -> detectHorizontalDragGestures( + onDragStart = onDragStart, + onDragEnd = onDragEnd, + onDragCancel = onDragCancel, + onHorizontalDrag = onDrag, + ) + + Orientation.Vertical -> detectVerticalDragGestures( + onDragStart = onDragStart, + onDragEnd = onDragEnd, + onDragCancel = onDragCancel, + onVerticalDrag = onDrag, + ) + } + } + .background( + color = backgroundColor, + shape = CircleShape + ), + ) { + val scrollbarThumbMovedDp = max( + a = with(LocalDensity.current) { thumbMovedPx.toDp() }, + b = 0.dp, + ) + // scrollbar thumb container + Box( + modifier = Modifier + .align(Alignment.TopStart) + .run { + when (orientation) { + Orientation.Horizontal -> width(thumbSizeDp) + Orientation.Vertical -> height(thumbSizeDp) + } + } + .offset( + y = when (orientation) { + Orientation.Horizontal -> 0.dp + Orientation.Vertical -> scrollbarThumbMovedDp + }, + x = when (orientation) { + Orientation.Horizontal -> scrollbarThumbMovedDp + Orientation.Vertical -> 0.dp + }, + ), + ) { + thumb() + } + } + + if (onThumbMoved == null) return + val thumbMoved: (Float) -> Unit = { + onThumbMoved( + when { + reverseLayout -> 1f - it + else -> it + } + ) + } + + // State that will be read inside the effects that follow + // but will not cause re-triggering of them + val updatedState by rememberUpdatedState(state) + + // Process presses + LaunchedEffect(pressedOffset) { + // Press ended, reset interactionThumbTravelPercent + if (pressedOffset == Offset.Unspecified) { + interactionThumbTravelPercent = Float.NaN + return@LaunchedEffect + } + + var currentThumbMovedPercent = updatedState.thumbMovedPercent + val destinationThumbMovedPercent = track.thumbPosition( + dimension = orientation.valueOf(pressedOffset), + ) + val isPositive = currentThumbMovedPercent < destinationThumbMovedPercent + val delta = SCROLLBAR_PRESS_DELTA_PCT * if (isPositive) 1f else -1f + + while (currentThumbMovedPercent != destinationThumbMovedPercent) { + currentThumbMovedPercent = when { + isPositive -> min( + a = currentThumbMovedPercent + delta, + b = destinationThumbMovedPercent, + ) + + else -> max( + a = currentThumbMovedPercent + delta, + b = destinationThumbMovedPercent, + ) + } + + thumbMoved(currentThumbMovedPercent) + + interactionThumbTravelPercent = currentThumbMovedPercent + delay(SCROLLBAR_PRESS_DELAY_MS) + } + } + + // Process drags + LaunchedEffect(draggedOffset) { + if (draggedOffset == Offset.Unspecified) { + interactionThumbTravelPercent = Float.NaN + return@LaunchedEffect + } + + val currentTravel = track.thumbPosition( + dimension = orientation.valueOf(draggedOffset), + ) + + thumbMoved(currentTravel) + interactionThumbTravelPercent = currentTravel + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/com/dergoogler/mmrl/ui/component/scrollbar/ScrollbarExt.kt b/app/src/main/kotlin/com/dergoogler/mmrl/ui/component/scrollbar/ScrollbarExt.kt new file mode 100644 index 00000000..de1b1822 --- /dev/null +++ b/app/src/main/kotlin/com/dergoogler/mmrl/ui/component/scrollbar/ScrollbarExt.kt @@ -0,0 +1,105 @@ +/* + * Copyright 2023 Sanmer + * + * 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 . + * + * Portions of this software are based on work by The Android Open Source Project, + * which is licensed under the Apache License, Version 2.0. You may obtain a copy + * of the Apache License, Version 2.0 at . + */ + +package com.dergoogler.mmrl.ui.component.scrollbar + +import androidx.compose.foundation.gestures.Orientation +import androidx.compose.foundation.lazy.LazyListItemInfo +import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.foundation.lazy.grid.LazyGridItemInfo +import androidx.compose.foundation.lazy.grid.LazyGridState +import androidx.compose.runtime.Composable + +/** + * Calculates a [ScrollbarState] driven by the changes in a [LazyListState]. + * + * @param itemsAvailable the total amount of items available to scroll in the lazy list. + * @param itemIndex a lookup function for index of an item in the list relative to [itemsAvailable]. + */ +@Composable +fun LazyListState.scrollbarState( + itemsAvailable: Int = layoutInfo.totalItemsCount, + itemIndex: (LazyListItemInfo) -> Int = LazyListItemInfo::index +): ScrollbarState = scrollbarState( + itemsAvailable = itemsAvailable, + visibleItems = { layoutInfo.visibleItemsInfo }, + firstVisibleItemIndex = { visibleItems -> + interpolateFirstItemIndex( + visibleItems = visibleItems, + itemSize = { it.size }, + offset = { it.offset }, + nextItemOnMainAxis = { first -> visibleItems.find { it != first } }, + itemIndex = itemIndex, + ) + }, + itemPercentVisible = itemPercentVisible@{ itemInfo -> + itemVisibilityPercentage( + itemSize = itemInfo.size, + itemStartOffset = itemInfo.offset, + viewportStartOffset = layoutInfo.viewportStartOffset, + viewportEndOffset = layoutInfo.viewportEndOffset, + ) + } +) + +/** + * Calculates a [ScrollbarState] driven by the changes in a [LazyGridState] + * + * @param itemsAvailable the total amount of items available to scroll in the grid. + * @param itemIndex a lookup function for index of an item in the grid relative to [itemsAvailable]. + */ +@Composable +fun LazyGridState.scrollbarState( + itemsAvailable: Int, + itemIndex: (LazyGridItemInfo) -> Int = LazyGridItemInfo::index +): ScrollbarState = scrollbarState( + itemsAvailable = itemsAvailable, + visibleItems = { layoutInfo.visibleItemsInfo }, + firstVisibleItemIndex = { visibleItems -> + interpolateFirstItemIndex( + visibleItems = visibleItems, + itemSize = { + layoutInfo.orientation.valueOf(it.size) + }, + offset = { layoutInfo.orientation.valueOf(it.offset) }, + nextItemOnMainAxis = { first -> + when (layoutInfo.orientation) { + Orientation.Vertical -> visibleItems.find { + it != first && it.row != first.row + } + + Orientation.Horizontal -> visibleItems.find { + it != first && it.column != first.column + } + } + }, + itemIndex = itemIndex, + ) + }, + itemPercentVisible = itemPercentVisible@{ itemInfo -> + itemVisibilityPercentage( + itemSize = layoutInfo.orientation.valueOf(itemInfo.size), + itemStartOffset = layoutInfo.orientation.valueOf(itemInfo.offset), + viewportStartOffset = layoutInfo.viewportStartOffset, + viewportEndOffset = layoutInfo.viewportEndOffset, + ) + } +) \ No newline at end of file diff --git a/app/src/main/kotlin/com/dergoogler/mmrl/ui/component/scrollbar/ThumbExt.kt b/app/src/main/kotlin/com/dergoogler/mmrl/ui/component/scrollbar/ThumbExt.kt new file mode 100644 index 00000000..ed46d71e --- /dev/null +++ b/app/src/main/kotlin/com/dergoogler/mmrl/ui/component/scrollbar/ThumbExt.kt @@ -0,0 +1,79 @@ +/* + * Copyright 2023 Sanmer + * + * 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 . + * + * Portions of this software are based on work by The Android Open Source Project, + * which is licensed under the Apache License, Version 2.0. You may obtain a copy + * of the Apache License, Version 2.0 at . + */ + +package com.dergoogler.mmrl.ui.component.scrollbar + +import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.foundation.lazy.grid.LazyGridState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableFloatStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberUpdatedState +import androidx.compose.runtime.setValue + +/** + * Remembers a function to react to [Scrollbar] thumb position displacements for a [LazyListState] + * @param itemsAvailable the amount of items in the list. + */ +@Composable +fun LazyListState.rememberDraggableScroller( + itemsAvailable: Int = layoutInfo.totalItemsCount +): (Float) -> Unit = rememberDraggableScroller( + itemsAvailable = itemsAvailable, + scroll = ::scrollToItem +) + +/** + * Remembers a function to react to [Scrollbar] thumb position displacements for a [LazyGridState] + * @param itemsAvailable the amount of items in the grid. + */ +@Composable +fun LazyGridState.rememberDraggableScroller( + itemsAvailable: Int = layoutInfo.totalItemsCount +): (Float) -> Unit = rememberDraggableScroller( + itemsAvailable = itemsAvailable, + scroll = ::scrollToItem +) + +/** + * Generic function to react to [Scrollbar] thumb displacements in a lazy layout. + * @param itemsAvailable the total amount of items available to scroll in the layout. + * @param scroll a function to be invoked when an index has been identified to scroll to. + */ +@Composable +private inline fun rememberDraggableScroller( + itemsAvailable: Int, + crossinline scroll: suspend (index: Int) -> Unit +): (Float) -> Unit { + var percentage by remember { mutableFloatStateOf(Float.NaN) } + val itemCount by rememberUpdatedState(itemsAvailable) + + LaunchedEffect(percentage) { + if (percentage.isNaN()) return@LaunchedEffect + val indexToFind = (itemCount * percentage).toInt() + scroll(indexToFind) + } + return remember { + { newPercentage -> percentage = newPercentage } + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/com/dergoogler/mmrl/ui/navigation/Main.kt b/app/src/main/kotlin/com/dergoogler/mmrl/ui/navigation/Main.kt new file mode 100644 index 00000000..f8b33a55 --- /dev/null +++ b/app/src/main/kotlin/com/dergoogler/mmrl/ui/navigation/Main.kt @@ -0,0 +1,33 @@ +package com.dergoogler.mmrl.ui.navigation + +import androidx.annotation.DrawableRes +import androidx.annotation.StringRes +import com.dergoogler.mmrl.R + +enum class MainScreen( + val route: String, + @StringRes val label: Int, + @DrawableRes val icon: Int, + @DrawableRes val iconFilled: Int +) { + Repository( + route = "RepositoryScreen", + label = R.string.page_repository, + icon = R.drawable.cloud, + iconFilled = R.drawable.cloud_filled + ), + + Modules( + route = "ModulesScreen", + label = R.string.page_modules, + icon = R.drawable.keyframes, + iconFilled = R.drawable.keyframes_filled + ), + + Settings( + route = "SettingsScreen", + label = R.string.page_settings, + icon = R.drawable.settings, + iconFilled = R.drawable.settings_filled + ) +} \ No newline at end of file diff --git a/app/src/main/kotlin/com/dergoogler/mmrl/ui/navigation/graphs/Modules.kt b/app/src/main/kotlin/com/dergoogler/mmrl/ui/navigation/graphs/Modules.kt new file mode 100644 index 00000000..4094675c --- /dev/null +++ b/app/src/main/kotlin/com/dergoogler/mmrl/ui/navigation/graphs/Modules.kt @@ -0,0 +1,31 @@ +package com.dergoogler.mmrl.ui.navigation.graphs + +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.navigation.NavController +import androidx.navigation.NavGraphBuilder +import androidx.navigation.compose.composable +import androidx.navigation.navigation +import com.dergoogler.mmrl.ui.navigation.MainScreen +import com.dergoogler.mmrl.ui.screens.modules.ModulesScreen + +enum class ModulesScreen(val route: String) { + Home("Modules"), +} + +fun NavGraphBuilder.modulesScreen( + navController: NavController +) = navigation( + startDestination = ModulesScreen.Home.route, + route = MainScreen.Modules.route +) { + composable( + route = ModulesScreen.Home.route, + enterTransition = { fadeIn() }, + exitTransition = { fadeOut() } + ) { + ModulesScreen( + navController = navController + ) + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/com/dergoogler/mmrl/ui/navigation/graphs/Repository.kt b/app/src/main/kotlin/com/dergoogler/mmrl/ui/navigation/graphs/Repository.kt new file mode 100644 index 00000000..0a9d23dc --- /dev/null +++ b/app/src/main/kotlin/com/dergoogler/mmrl/ui/navigation/graphs/Repository.kt @@ -0,0 +1,47 @@ +package com.dergoogler.mmrl.ui.navigation.graphs + +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.animation.scaleIn +import androidx.navigation.NavController +import androidx.navigation.NavGraphBuilder +import androidx.navigation.NavType +import androidx.navigation.compose.composable +import androidx.navigation.navArgument +import androidx.navigation.navigation +import com.dergoogler.mmrl.ui.navigation.MainScreen +import com.dergoogler.mmrl.ui.screens.repository.RepositoryScreen +import com.dergoogler.mmrl.ui.screens.repository.view.ViewScreen + +enum class RepositoryScreen(val route: String) { + Home("Repository"), + View("View/{moduleId}") +} + +fun NavGraphBuilder.repositoryScreen( + navController: NavController +) = navigation( + startDestination = RepositoryScreen.Home.route, + route = MainScreen.Repository.route +) { + composable( + route = RepositoryScreen.Home.route, + enterTransition = { fadeIn() }, + exitTransition = { fadeOut() } + ) { + RepositoryScreen( + navController = navController + ) + } + + composable( + route = RepositoryScreen.View.route, + arguments = listOf(navArgument("moduleId") { type = NavType.StringType }), + enterTransition = { scaleIn() + fadeIn() }, + exitTransition = { fadeOut() } + ) { + ViewScreen( + navController = navController + ) + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/com/dergoogler/mmrl/ui/navigation/graphs/Settings.kt b/app/src/main/kotlin/com/dergoogler/mmrl/ui/navigation/graphs/Settings.kt new file mode 100644 index 00000000..2a633753 --- /dev/null +++ b/app/src/main/kotlin/com/dergoogler/mmrl/ui/navigation/graphs/Settings.kt @@ -0,0 +1,80 @@ +package com.dergoogler.mmrl.ui.navigation.graphs + +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.animation.scaleIn +import androidx.navigation.NavController +import androidx.navigation.NavGraphBuilder +import androidx.navigation.compose.composable +import androidx.navigation.navigation +import com.dergoogler.mmrl.ui.navigation.MainScreen +import com.dergoogler.mmrl.ui.screens.settings.SettingsScreen +import com.dergoogler.mmrl.ui.screens.settings.about.AboutScreen +import com.dergoogler.mmrl.ui.screens.settings.app.AppScreen +import com.dergoogler.mmrl.ui.screens.settings.repositories.RepositoriesScreen +import com.dergoogler.mmrl.ui.screens.settings.workingmode.WorkingModeScreen + +enum class SettingsScreen(val route: String) { + Home("Settings"), + Repositories("Repositories"), + App("App"), + WorkingMode("WorkingMode"), + About("About") +} + +fun NavGraphBuilder.settingsScreen( + navController: NavController +) = navigation( + startDestination = SettingsScreen.Home.route, + route = MainScreen.Settings.route +) { + composable( + route = SettingsScreen.Home.route, + enterTransition = { fadeIn() }, + exitTransition = { fadeOut() } + ) { + SettingsScreen( + navController = navController + ) + } + + composable( + route = SettingsScreen.Repositories.route, + enterTransition = { scaleIn() + fadeIn() }, + exitTransition = { fadeOut() } + ) { + RepositoriesScreen( + navController = navController + ) + } + + composable( + route = SettingsScreen.App.route, + enterTransition = { scaleIn() + fadeIn() }, + exitTransition = { fadeOut() } + ) { + AppScreen( + navController = navController + ) + } + + composable( + route = SettingsScreen.WorkingMode.route, + enterTransition = { scaleIn() + fadeIn() }, + exitTransition = { fadeOut() } + ) { + WorkingModeScreen( + navController = navController + ) + } + + composable( + route = SettingsScreen.About.route, + enterTransition = { scaleIn() + fadeIn() }, + exitTransition = { fadeOut() } + ) { + AboutScreen( + navController = navController + ) + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/com/dergoogler/mmrl/ui/providable/LocalUserPreferences.kt b/app/src/main/kotlin/com/dergoogler/mmrl/ui/providable/LocalUserPreferences.kt new file mode 100644 index 00000000..43250d68 --- /dev/null +++ b/app/src/main/kotlin/com/dergoogler/mmrl/ui/providable/LocalUserPreferences.kt @@ -0,0 +1,6 @@ +package com.dergoogler.mmrl.ui.providable + +import androidx.compose.runtime.staticCompositionLocalOf +import com.dergoogler.mmrl.datastore.UserPreferencesCompat + +val LocalUserPreferences = staticCompositionLocalOf { UserPreferencesCompat.default() } diff --git a/app/src/main/kotlin/com/dergoogler/mmrl/ui/screens/modules/ModuleItem.kt b/app/src/main/kotlin/com/dergoogler/mmrl/ui/screens/modules/ModuleItem.kt new file mode 100644 index 00000000..ff523514 --- /dev/null +++ b/app/src/main/kotlin/com/dergoogler/mmrl/ui/screens/modules/ModuleItem.kt @@ -0,0 +1,165 @@ +package com.dergoogler.mmrl.ui.screens.modules + +import androidx.annotation.DrawableRes +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.BoxScope +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.RowScope +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.requiredSize +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.LinearProgressIndicator +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +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.alpha +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.ColorFilter +import androidx.compose.ui.graphics.StrokeCap +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextDecoration +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import com.dergoogler.mmrl.R +import com.dergoogler.mmrl.model.local.LocalModule +import com.dergoogler.mmrl.model.local.versionDisplay +import com.dergoogler.mmrl.ui.providable.LocalUserPreferences +import com.dergoogler.mmrl.utils.extensions.toDate + +@Composable +fun ModuleItem( + module: LocalModule, + progress: Float, + indeterminate: Boolean = false, + alpha: Float = 1f, + decoration: TextDecoration = TextDecoration.None, + switch: @Composable (() -> Unit?)? = null, + indicator: @Composable (BoxScope.() -> Unit?)? = null, + trailingButton: @Composable RowScope.() -> Unit, +) = Surface( + color = MaterialTheme.colorScheme.surface, + tonalElevation = 1.dp, + shape = RoundedCornerShape(20.dp) +) { + val userPreferences = LocalUserPreferences.current + val menu = userPreferences.modulesMenu + + Box( + modifier = Modifier.fillMaxWidth(), + contentAlignment = Alignment.Center + ) { + Column( + modifier = Modifier.fillMaxWidth() + ) { + Row( + modifier = Modifier.padding(all = 16.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Column( + modifier = Modifier + .alpha(alpha = alpha) + .weight(1f), + verticalArrangement = Arrangement.spacedBy(2.dp) + ) { + Text( + text = module.name, + style = MaterialTheme.typography.titleSmall + .copy(fontWeight = FontWeight.Bold), + maxLines = 2, + textDecoration = decoration, + overflow = TextOverflow.Ellipsis + ) + + Text( + text = stringResource(id = R.string.module_version_author, + module.versionDisplay, module.author), + style = MaterialTheme.typography.bodySmall, + textDecoration = decoration, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + + if (module.lastUpdated != 0L && menu.showUpdatedTime) { + Text( + text = stringResource(id = R.string.module_update_at, + module.lastUpdated.toDate()), + style = MaterialTheme.typography.bodySmall, + textDecoration = decoration, + color = MaterialTheme.colorScheme.outline + ) + } + } + + switch?.invoke() + } + + Text( + modifier = Modifier + .alpha(alpha = alpha) + .padding(horizontal = 16.dp), + text = module.description, + style = MaterialTheme.typography.bodySmall, + textDecoration = decoration, + color = MaterialTheme.colorScheme.outline + ) + + when { + indeterminate -> LinearProgressIndicator( + strokeCap = StrokeCap.Round, + modifier = Modifier + .padding(top = 8.dp) + .height(2.dp) + .fillMaxWidth() + ) + progress != 0f -> LinearProgressIndicator( + progress = { progress }, + strokeCap = StrokeCap.Round, + modifier = Modifier + .padding(top = 8.dp) + .height(2.dp) + .fillMaxWidth() + ) + else -> HorizontalDivider( + thickness = 1.5.dp, + color = MaterialTheme.colorScheme.surface, + modifier = Modifier.padding(top = 8.dp) + ) + } + + Row( + modifier = Modifier + .padding(horizontal = 16.dp, vertical = 8.dp) + .fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically + ) { + Spacer(modifier = Modifier.weight(1f)) + trailingButton() + } + } + + indicator?.invoke(this) + } +} + +@Composable +fun StateIndicator( + @DrawableRes icon: Int, + color: Color = MaterialTheme.colorScheme.outline +) = Image( + modifier = Modifier.requiredSize(150.dp), + painter = painterResource(id = icon), + contentDescription = null, + alpha = 0.1f, + colorFilter = ColorFilter.tint(color) +) diff --git a/app/src/main/kotlin/com/dergoogler/mmrl/ui/screens/modules/ModulesList.kt b/app/src/main/kotlin/com/dergoogler/mmrl/ui/screens/modules/ModulesList.kt new file mode 100644 index 00000000..2207f328 --- /dev/null +++ b/app/src/main/kotlin/com/dergoogler/mmrl/ui/screens/modules/ModulesList.kt @@ -0,0 +1,234 @@ +package com.dergoogler.mmrl.ui.screens.modules + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.foundation.lazy.items +import androidx.compose.material3.FilledTonalButton +import androidx.compose.material3.Icon +import androidx.compose.material3.Switch +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.derivedStateOf +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.platform.LocalContext +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextDecoration +import androidx.compose.ui.unit.dp +import com.dergoogler.mmrl.R +import com.dergoogler.mmrl.model.local.LocalModule +import com.dergoogler.mmrl.model.local.State +import com.dergoogler.mmrl.model.online.VersionItem +import com.dergoogler.mmrl.ui.activity.ModConfActivity +import com.dergoogler.mmrl.ui.component.VersionItemBottomSheet +import com.dergoogler.mmrl.ui.component.scrollbar.VerticalFastScrollbar +import com.dergoogler.mmrl.viewmodel.ModulesViewModel + +@Composable +fun ModulesList( + list: List, + state: LazyListState, + isProviderAlive: Boolean, + getModuleOps: (LocalModule) -> ModulesViewModel.ModuleOps, + getVersionItem: @Composable (LocalModule) -> VersionItem?, + getProgress: @Composable (VersionItem?) -> Float, + onDownload: (LocalModule, VersionItem, Boolean) -> Unit +) = Box( + modifier = Modifier.fillMaxSize() +) { + LazyColumn( + state = state, + modifier = Modifier.fillMaxSize(), + contentPadding = PaddingValues(16.dp), + verticalArrangement = Arrangement.spacedBy(16.dp) + ) { + items( + items = list, + key = { it.id } + ) { module -> + ModuleItem( + module = module, + isProviderAlive = isProviderAlive, + getModuleOps = getModuleOps, + getVersionItem = getVersionItem, + getProgress = getProgress, + onDownload = onDownload + ) + } + } + + VerticalFastScrollbar( + state = state, + modifier = Modifier.align(Alignment.CenterEnd) + ) +} + +@Composable +fun ModuleItem( + module: LocalModule, + isProviderAlive: Boolean, + getModuleOps: (LocalModule) -> ModulesViewModel.ModuleOps, + getVersionItem: @Composable (LocalModule) -> VersionItem?, + getProgress: @Composable (VersionItem?) -> Float, + onDownload: (LocalModule, VersionItem, Boolean) -> Unit +) { + val ops by remember(module.state) { + derivedStateOf { getModuleOps(module) } + } + + val context = LocalContext.current + + val item = getVersionItem(module) + val progress = getProgress(item) + + var open by remember { mutableStateOf(false) } + if (open && item != null) { + VersionItemBottomSheet( + isUpdate = true, + item = item, + isProviderAlive = isProviderAlive, + onClose = { open = false }, + onDownload = { onDownload(module, item, it) } + ) + } + + ModuleItem( + module = module, + progress = progress, + indeterminate = ops.isOpsRunning, + alpha = when (module.state) { + State.DISABLE, State.REMOVE -> 0.5f + else -> 1f + }, + decoration = when (module.state) { + State.REMOVE -> TextDecoration.LineThrough + else -> TextDecoration.None + }, + switch = { + Switch( + checked = module.state == State.ENABLE, + onCheckedChange = ops.toggle, + enabled = isProviderAlive + ) + }, + indicator = { + when (module.state) { + State.REMOVE -> StateIndicator(R.drawable.trash) + State.UPDATE -> StateIndicator(R.drawable.device_mobile_down) + else -> {} + } + }, + trailingButton = { + if (item != null) { + UpdateButton( + enabled = item.versionCode > module.versionCode, + onClick = { open = true } + ) + + Spacer(modifier = Modifier.width(12.dp)) + } + + RemoveOrRestore( + module = module, + enabled = isProviderAlive, + onClick = ops.change + ) + +// Spacer(modifier = Modifier.width(12.dp)) +// +// ModConf( +// enabled = isProviderAlive, +// onClick = { +// ModConfActivity.start(context = context, modId = module.id) +// } +// ) + } + ) +} + +@Composable +private fun UpdateButton( + enabled: Boolean, + onClick: () -> Unit +) = FilledTonalButton( + onClick = onClick, + enabled = enabled, + contentPadding = PaddingValues(horizontal = 12.dp) +) { + Icon( + modifier = Modifier.size(20.dp), + painter = painterResource(id = R.drawable.device_mobile_down), + contentDescription = null + ) + + Spacer(modifier = Modifier.width(6.dp)) + Text( + text = stringResource(id = R.string.module_update) + ) +} + +@Composable +private fun RemoveOrRestore( + module: LocalModule, + enabled: Boolean, + onClick: () -> Unit +) = FilledTonalButton( + onClick = onClick, + enabled = enabled, + contentPadding = PaddingValues(horizontal = 12.dp) +) { + Icon( + modifier = Modifier.size(20.dp), + painter = painterResource( + id = if (module.state == State.REMOVE) { + R.drawable.rotate + } else { + R.drawable.trash + } + ), + contentDescription = null + ) + + Spacer(modifier = Modifier.width(6.dp)) + Text( + text = stringResource( + id = if (module.state == State.REMOVE) { + R.string.module_restore + } else { + R.string.module_remove + } + ) + ) +} + +@Composable +private fun ModConf( + enabled: Boolean, + onClick: () -> Unit +) = FilledTonalButton( + onClick = onClick, + enabled = enabled, + contentPadding = PaddingValues(horizontal = 12.dp) +) { + Icon( + modifier = Modifier.size(20.dp), + painter = painterResource(id = R.drawable.settings), + contentDescription = null + ) + Spacer(modifier = Modifier.width(6.dp)) + Text( + text = stringResource(id = R.string.module_config) + ) +} \ No newline at end of file diff --git a/app/src/main/kotlin/com/dergoogler/mmrl/ui/screens/modules/ModulesMenu.kt b/app/src/main/kotlin/com/dergoogler/mmrl/ui/screens/modules/ModulesMenu.kt new file mode 100644 index 00000000..52701bfa --- /dev/null +++ b/app/src/main/kotlin/com/dergoogler/mmrl/ui/screens/modules/ModulesMenu.kt @@ -0,0 +1,142 @@ +package com.dergoogler.mmrl.ui.screens.modules + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.FlowRow +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.wrapContentHeight +import androidx.compose.material3.BottomSheetDefaults +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.ModalBottomSheet +import androidx.compose.material3.Text +import androidx.compose.material3.rememberModalBottomSheetState +import androidx.compose.runtime.Composable +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.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import com.dergoogler.mmrl.R +import com.dergoogler.mmrl.datastore.modules.ModulesMenuCompat +import com.dergoogler.mmrl.datastore.repository.Option +import com.dergoogler.mmrl.ui.component.MenuChip +import com.dergoogler.mmrl.ui.component.NavigationBarsSpacer +import com.dergoogler.mmrl.ui.component.Segment +import com.dergoogler.mmrl.ui.component.SegmentedButtons +import com.dergoogler.mmrl.ui.component.SegmentedButtonsDefaults +import com.dergoogler.mmrl.ui.providable.LocalUserPreferences +import com.dergoogler.mmrl.ui.utils.expandedShape + +@Composable +fun ModulesMenu( + setMenu: (ModulesMenuCompat) -> Unit +) { + val userPreferences = LocalUserPreferences.current + var open by rememberSaveable { mutableStateOf(false) } + + IconButton( + onClick = { open = true } + ) { + Icon( + painter = painterResource(id = R.drawable.menu_2), + contentDescription = null + ) + + if (open) { + BottomSheet( + onClose = { open = false }, + menu = userPreferences.modulesMenu, + setMenu = setMenu + ) + } + } +} + +@Composable +private fun BottomSheet( + onClose: () -> Unit, + menu: ModulesMenuCompat, + setMenu: (ModulesMenuCompat) -> Unit +) = ModalBottomSheet( + onDismissRequest = onClose, + sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true), + shape = BottomSheetDefaults.expandedShape(15.dp), + windowInsets = WindowInsets(0) +) { + val options = listOf( + Option.NAME to R.string.menu_sort_option_name, + Option.UPDATED_TIME to R.string.menu_sort_option_updated + ) + + Text( + text = stringResource(id = R.string.menu_advanced_menu), + style = MaterialTheme.typography.headlineSmall, + modifier = Modifier.align(Alignment.CenterHorizontally) + ) + + Column( + modifier = Modifier.padding(all = 18.dp), + verticalArrangement = Arrangement.spacedBy(8.dp) + ) { + Text( + text = stringResource(id = R.string.menu_sort_mode), + style = MaterialTheme.typography.titleSmall + ) + + SegmentedButtons( + border = SegmentedButtonsDefaults.border( + color = MaterialTheme.colorScheme.secondary + ) + ) { + options.forEach { (option, label) -> + Segment( + selected = option == menu.option, + onClick = { setMenu(menu.copy(option = option)) }, + colors = SegmentedButtonsDefaults.buttonColor( + selectedContainerColor = MaterialTheme.colorScheme.secondary, + selectedContentColor = MaterialTheme.colorScheme.onSecondary + ), + icon = null + ) { + Text(text = stringResource(id = label)) + } + } + } + + FlowRow( + modifier = Modifier + .fillMaxWidth(1f) + .wrapContentHeight(align = Alignment.Top), + horizontalArrangement = Arrangement.spacedBy(8.dp), + verticalArrangement = Arrangement.spacedBy(8.dp) + ) { + MenuChip( + selected = menu.descending, + onClick = { setMenu(menu.copy(descending = !menu.descending)) }, + label = { Text(text = stringResource(id = R.string.menu_descending)) } + ) + + MenuChip( + selected = menu.pinEnabled, + onClick = { setMenu(menu.copy(pinEnabled = !menu.pinEnabled)) }, + label = { Text(text = stringResource(id = R.string.menu_pin_enabled)) } + ) + + MenuChip( + selected = menu.showUpdatedTime, + onClick = { setMenu(menu.copy(showUpdatedTime = !menu.showUpdatedTime)) }, + label = { Text(text = stringResource(id = R.string.menu_show_updated)) } + ) + } + } + + NavigationBarsSpacer() +} \ No newline at end of file diff --git a/app/src/main/kotlin/com/dergoogler/mmrl/ui/screens/modules/ModulesScreen.kt b/app/src/main/kotlin/com/dergoogler/mmrl/ui/screens/modules/ModulesScreen.kt new file mode 100644 index 00000000..d3672e00 --- /dev/null +++ b/app/src/main/kotlin/com/dergoogler/mmrl/ui/screens/modules/ModulesScreen.kt @@ -0,0 +1,227 @@ +package com.dergoogler.mmrl.ui.screens.modules + +import androidx.activity.compose.BackHandler +import androidx.activity.compose.rememberLauncherForActivityResult +import androidx.activity.result.contract.ActivityResultContracts +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.core.tween +import androidx.compose.animation.scaleIn +import androidx.compose.animation.scaleOut +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.interaction.PressInteraction +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.material3.FloatingActionButton +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.TopAppBarDefaults +import androidx.compose.material3.TopAppBarScrollBehavior +import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect +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.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.input.nestedscroll.nestedScroll +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.core.net.toUri +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import androidx.navigation.NavController +import com.dergoogler.mmrl.R +import com.dergoogler.mmrl.datastore.modules.ModulesMenuCompat +import com.dergoogler.mmrl.model.local.LocalModule +import com.dergoogler.mmrl.model.online.VersionItem +import com.dergoogler.mmrl.ui.activity.InstallActivity +import com.dergoogler.mmrl.ui.component.Loading +import com.dergoogler.mmrl.ui.component.PageIndicator +import com.dergoogler.mmrl.ui.component.SearchTopBar +import com.dergoogler.mmrl.ui.component.TopAppBarTitle +import com.dergoogler.mmrl.ui.utils.isScrollingUp +import com.dergoogler.mmrl.ui.utils.none +import com.dergoogler.mmrl.viewmodel.ModulesViewModel + +@Composable +fun ModulesScreen( + @Suppress("UNUSED_PARAMETER") + navController: NavController, + viewModel: ModulesViewModel = hiltViewModel() +) { + val context = LocalContext.current + + val list by viewModel.local.collectAsStateWithLifecycle() + + val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior() + val listState = rememberLazyListState() + + val isScrollingUp by listState.isScrollingUp() + val showFab by remember { + derivedStateOf { + isScrollingUp && !viewModel.isSearch && viewModel.isProviderAlive + } + } + + val download: (LocalModule, VersionItem, Boolean) -> Unit = { module, item, install -> + viewModel.downloader(context, module, item) { + if (install) { + InstallActivity.start( + context = context, + uri = it.toUri() + ) + } + } + } + + BackHandler( + enabled = viewModel.isSearch, + onBack = viewModel::closeSearch + ) + + DisposableEffect(true) { + onDispose(viewModel::closeSearch) + } + + Scaffold( + modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection), + topBar = { + TopBar( + isSearch = viewModel.isSearch, + onQueryChange = viewModel::search, + onOpenSearch = viewModel::openSearch, + onCloseSearch = viewModel::closeSearch, + setMenu = viewModel::setModulesMenu, + scrollBehavior = scrollBehavior + ) + }, + floatingActionButton = { + AnimatedVisibility( + visible = showFab, + enter = scaleIn( + animationSpec = tween(100), + initialScale = 0.8f + ), + exit = scaleOut( + animationSpec = tween(100), + targetScale = 0.8f + ) + ) { + FloatingButton() + } + }, + contentWindowInsets = WindowInsets.none + ) { innerPadding -> + Box( + modifier = Modifier.padding(innerPadding) + ) { + if (viewModel.isLoading) { + Loading() + } + + if (list.isEmpty() && !viewModel.isLoading) { + PageIndicator( + icon = R.drawable.keyframes, + text = if (viewModel.isSearch) R.string.search_empty else R.string.modules_empty, + ) + } + + ModulesList( + list = list, + state = listState, + isProviderAlive = viewModel.isProviderAlive, + getModuleOps = viewModel::createModuleOps, + getVersionItem = { viewModel.getVersionItem(it) }, + getProgress = { viewModel.getProgress(it) }, + onDownload = download + ) + } + } +} + +@Composable +private fun TopBar( + isSearch: Boolean, + onQueryChange: (String) -> Unit, + onOpenSearch: () -> Unit, + onCloseSearch: () -> Unit, + setMenu: (ModulesMenuCompat) -> Unit, + scrollBehavior: TopAppBarScrollBehavior, +) { + var query by remember { mutableStateOf("") } + DisposableEffect(isSearch) { + onDispose { query = "" } + } + + SearchTopBar( + isSearch = isSearch, + query = query, + onQueryChange = { + onQueryChange(it) + query = it + }, + onClose = { + onCloseSearch() + query = "" + }, + title = { TopAppBarTitle(text = stringResource(id = R.string.page_modules)) }, + scrollBehavior = scrollBehavior, + actions = { + if (!isSearch) { + IconButton( + onClick = onOpenSearch + ) { + Icon( + painter = painterResource(id = R.drawable.search), + contentDescription = null + ) + } + } + + ModulesMenu( + setMenu = setMenu + ) + } + ) +} + +@Composable +private fun FloatingButton() { + val context = LocalContext.current + val interactionSource = remember { MutableInteractionSource() } + val launcher = rememberLauncherForActivityResult(ActivityResultContracts.GetContent()) { uri -> + if (uri == null) return@rememberLauncherForActivityResult + + InstallActivity.start( + context = context, + uri = uri + ) + } + + LaunchedEffect(interactionSource) { + interactionSource.interactions.collect { interaction -> + if (interaction is PressInteraction.Release) { + launcher.launch("application/zip") + } + } + } + + FloatingActionButton( + interactionSource = interactionSource, + onClick = {}, + contentColor = MaterialTheme.colorScheme.onPrimary, + containerColor = MaterialTheme.colorScheme.primary + ) { + Icon( + painter = painterResource(id = R.drawable.package_import), + contentDescription = null + ) + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/com/dergoogler/mmrl/ui/screens/repository/ModuleItem.kt b/app/src/main/kotlin/com/dergoogler/mmrl/ui/screens/repository/ModuleItem.kt new file mode 100644 index 00000000..174e83ba --- /dev/null +++ b/app/src/main/kotlin/com/dergoogler/mmrl/ui/screens/repository/ModuleItem.kt @@ -0,0 +1,217 @@ +package com.dergoogler.mmrl.ui.screens.repository + +import androidx.compose.foundation.Image +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.Spacer +import androidx.compose.foundation.layout.aspectRatio +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +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.BlurredEdgeTreatment.Companion.Rectangle +import androidx.compose.ui.draw.alpha +import androidx.compose.ui.graphics.Outline +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextDecoration +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import coil.compose.AsyncImage +import coil.compose.AsyncImagePainter +import coil.compose.rememberAsyncImagePainter +import coil.request.CachePolicy +import coil.request.ImageRequest +import com.dergoogler.mmrl.R +import com.dergoogler.mmrl.model.online.OnlineModule +import com.dergoogler.mmrl.model.state.OnlineState +import com.dergoogler.mmrl.ui.component.LabelItem +import com.dergoogler.mmrl.ui.component.Logo +import com.dergoogler.mmrl.ui.providable.LocalUserPreferences +import com.dergoogler.mmrl.utils.extensions.toDate +import com.dergoogler.mmrl.utils.extensions.toFormattedDate + +@Composable +fun ModuleItem( + module: OnlineModule, + state: OnlineState, + alpha: Float = 1f, + onClick: () -> Unit = {}, decoration: TextDecoration = TextDecoration.None, + enabled: Boolean = true +) = Surface( + onClick = onClick, + color = MaterialTheme.colorScheme.surface, + tonalElevation = 1.dp, + shape = RoundedCornerShape(20.dp) +) { + val context = LocalContext.current + val userPreferences = LocalUserPreferences.current + val menu = userPreferences.repositoryMenu + val hasLabel = + (state.hasLicense && menu.showLicense) || state.installed || (module.track.hasAntifeatures && menu.showAntiFeatures) + val isVerified = module.isVerified && menu.showVerified + + Box( + modifier = Modifier.fillMaxWidth(), contentAlignment = Alignment.Center + ) { + Column( + modifier = Modifier.fillMaxWidth() + ) { + if (menu.showCover) { + module.cover?.let { + if (it.isNotEmpty()) { + val painter = rememberAsyncImagePainter( + model = ImageRequest.Builder(context).data(it).memoryCacheKey(it) + .diskCacheKey(it).diskCachePolicy(CachePolicy.ENABLED) + .memoryCachePolicy(CachePolicy.ENABLED).build(), + ) + + if (painter.state !is AsyncImagePainter.State.Error) { + Image( + painter = painter, + contentDescription = null, + contentScale = ContentScale.Crop, + modifier = Modifier + .fillMaxWidth() + .aspectRatio(2.048f) + ) + } else { + Logo( + icon = R.drawable.alert_triangle, + shape = RoundedCornerShape(0.dp), + modifier = Modifier + .fillMaxWidth() + .aspectRatio(2.048f) + + ) + } + } + } + } + + Row( + modifier = Modifier.padding(all = 16.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Column( + modifier = Modifier + .alpha(alpha = alpha) + .weight(1f), + verticalArrangement = Arrangement.spacedBy(2.dp) + ) { + Row( + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = module.name, + style = MaterialTheme.typography.titleSmall.copy(fontWeight = FontWeight.Bold), + maxLines = 2, + textDecoration = decoration, + overflow = TextOverflow.Ellipsis + ) + if (isVerified) { + Spacer(modifier = Modifier.width(4.dp)) + + val iconSize = + with(LocalDensity.current) { MaterialTheme.typography.titleSmall.fontSize.toDp() * 1.0f } + + Icon( + modifier = Modifier.size(iconSize), + painter = painterResource(id = R.drawable.rosette_discount_check), + contentDescription = null, + tint = MaterialTheme.colorScheme.onPrimaryContainer + ) + } + } + + Text( + text = stringResource( + id = R.string.module_version_author, + module.versionDisplay, + module.author + ), + style = MaterialTheme.typography.bodySmall, + textDecoration = decoration, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + + if (menu.showUpdatedTime) { + Text( + text = stringResource( + id = R.string.module_update_at, state.lastUpdated.toFormattedDate() + ), + style = MaterialTheme.typography.bodySmall, + textDecoration = decoration, + color = MaterialTheme.colorScheme.outline + ) + } + } + } + + Text( + modifier = Modifier + .alpha(alpha = alpha) + .padding(end = 16.dp, bottom = 16.dp, start = 16.dp), + text = module.description, + style = MaterialTheme.typography.bodySmall, + textDecoration = decoration, + color = MaterialTheme.colorScheme.outline + ) + + Spacer(modifier = Modifier.weight(1f)) + + if (hasLabel) { + Row( + modifier = Modifier + .padding(end = 16.dp, bottom = 16.dp, start = 16.dp) + .fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(4.dp) + ) { + if (menu.showLicense && module.hasLicense) { + module.license?.let { LabelItem(text = it) } + } + + if (menu.showAntiFeatures) { + module.track.antifeatures?.let { + if (it.isNotEmpty()) { + LabelItem( + containerColor = MaterialTheme.colorScheme.onTertiary, + contentColor = MaterialTheme.colorScheme.onTertiaryContainer, + text = stringResource(id = R.string.view_module_antifeatures) + ) + } + } + } + + when { + state.updatable -> + LabelItem( + text = stringResource(id = R.string.module_new), + containerColor = MaterialTheme.colorScheme.error, + contentColor = MaterialTheme.colorScheme.onError + ) + + state.installed -> + LabelItem(text = stringResource(id = R.string.module_installed)) + } + } + Spacer(modifier = Modifier.weight(1f)) + } + } + } +} diff --git a/app/src/main/kotlin/com/dergoogler/mmrl/ui/screens/repository/ModulesList.kt b/app/src/main/kotlin/com/dergoogler/mmrl/ui/screens/repository/ModulesList.kt new file mode 100644 index 00000000..a0b3ea72 --- /dev/null +++ b/app/src/main/kotlin/com/dergoogler/mmrl/ui/screens/repository/ModulesList.kt @@ -0,0 +1,55 @@ +package com.dergoogler.mmrl.ui.screens.repository + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.foundation.lazy.items +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import androidx.navigation.NavController +import com.dergoogler.mmrl.model.online.OnlineModule +import com.dergoogler.mmrl.model.state.OnlineState +import com.dergoogler.mmrl.ui.component.scrollbar.VerticalFastScrollbar +import com.dergoogler.mmrl.ui.utils.navigateSingleTopTo +import com.dergoogler.mmrl.viewmodel.ModuleViewModel + +@Composable +fun ModulesList( + list: List>, + state: LazyListState, + navController: NavController +) = Box( + modifier = Modifier.fillMaxSize() +) { + LazyColumn( + state = state, + modifier = Modifier.fillMaxSize(), + contentPadding = PaddingValues(16.dp), + verticalArrangement = Arrangement.spacedBy(16.dp) + ) { + items( + items = list, + key = { it.second.id } + ) { (state, module) -> + ModuleItem( + module = module, + state = state, + onClick = { + navController.navigateSingleTopTo( + ModuleViewModel.putModuleId(module) + ) + } + ) + } + } + + VerticalFastScrollbar( + state = state, + modifier = Modifier.align(Alignment.CenterEnd) + ) +} \ No newline at end of file diff --git a/app/src/main/kotlin/com/dergoogler/mmrl/ui/screens/repository/RepositoryMenu.kt b/app/src/main/kotlin/com/dergoogler/mmrl/ui/screens/repository/RepositoryMenu.kt new file mode 100644 index 00000000..bb3f6685 --- /dev/null +++ b/app/src/main/kotlin/com/dergoogler/mmrl/ui/screens/repository/RepositoryMenu.kt @@ -0,0 +1,194 @@ +package com.dergoogler.mmrl.ui.screens.repository + +import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.FlowRow +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.wrapContentHeight +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.BottomSheetDefaults +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.ModalBottomSheet +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.material3.rememberModalBottomSheetState +import androidx.compose.runtime.Composable +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.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import com.dergoogler.mmrl.R +import com.dergoogler.mmrl.datastore.repository.Option +import com.dergoogler.mmrl.datastore.repository.RepositoryMenuCompat +import com.dergoogler.mmrl.model.online.OnlineModule +import com.dergoogler.mmrl.model.state.OnlineState +import com.dergoogler.mmrl.ui.component.MenuChip +import com.dergoogler.mmrl.ui.component.NavigationBarsSpacer +import com.dergoogler.mmrl.ui.component.Segment +import com.dergoogler.mmrl.ui.component.SegmentedButtons +import com.dergoogler.mmrl.ui.component.SegmentedButtonsDefaults +import com.dergoogler.mmrl.ui.providable.LocalUserPreferences +import com.dergoogler.mmrl.ui.utils.expandedShape + +@Composable +fun RepositoryMenu( + setMenu: (RepositoryMenuCompat) -> Unit +) { + val userPreferences = LocalUserPreferences.current + var open by rememberSaveable { mutableStateOf(false) } + + IconButton( + onClick = { open = true } + ) { + Icon( + painter = painterResource(id = R.drawable.menu_2), + contentDescription = null + ) + + if (open) { + BottomSheet( + onClose = { open = false }, + menu = userPreferences.repositoryMenu, + setMenu = setMenu + ) + } + } +} + +@Composable +private fun BottomSheet( + onClose: () -> Unit, + menu: RepositoryMenuCompat, + setMenu: (RepositoryMenuCompat) -> Unit +) = ModalBottomSheet( + onDismissRequest = onClose, + sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true), + shape = BottomSheetDefaults.expandedShape(15.dp), + windowInsets = WindowInsets(0) +) { + val options = listOf( + Option.NAME to R.string.menu_sort_option_name, + Option.UPDATED_TIME to R.string.menu_sort_option_updated + ) + + Text( + text = stringResource(id = R.string.menu_advanced_menu), + style = MaterialTheme.typography.headlineSmall, + modifier = Modifier.align(Alignment.CenterHorizontally) + ) + + Column( + modifier = Modifier.padding(all = 18.dp), + verticalArrangement = Arrangement.spacedBy(8.dp) + ) { +// Surface( +// modifier = Modifier.padding(bottom = 8.dp), +// shape = RoundedCornerShape(10.dp), +// tonalElevation = 6.dp, +// border = BorderStroke(1.dp, color = MaterialTheme.colorScheme.outline) +// ) { +// ModuleItem( +// module = OnlineModule.example(), +// state = OnlineState.example(), +// enabled = false +// ) +// } + + Text( + text = stringResource(id = R.string.menu_sort_mode), + style = MaterialTheme.typography.titleSmall + ) + + SegmentedButtons( + border = SegmentedButtonsDefaults.border( + color = MaterialTheme.colorScheme.secondary + ) + ) { + options.forEach { (option, label) -> + Segment( + selected = option == menu.option, + onClick = { setMenu(menu.copy(option = option)) }, + colors = SegmentedButtonsDefaults.buttonColor( + selectedContainerColor = MaterialTheme.colorScheme.secondary, + selectedContentColor = MaterialTheme.colorScheme.onSecondary + ), + icon = null + ) { + Text(text = stringResource(id = label)) + } + } + } + + FlowRow( + modifier = Modifier + .fillMaxWidth(1f) + .wrapContentHeight(align = Alignment.Top), + horizontalArrangement = Arrangement.spacedBy(8.dp), + verticalArrangement = Arrangement.spacedBy(8.dp) + ) { + MenuChip( + selected = menu.descending, + onClick = { setMenu(menu.copy(descending = !menu.descending)) }, + label = { Text(text = stringResource(id = R.string.menu_descending)) } + ) + + MenuChip( + selected = menu.pinInstalled, + onClick = { setMenu(menu.copy(pinInstalled = !menu.pinInstalled)) }, + label = { Text(text = stringResource(id = R.string.menu_pin_installed)) } + ) + + MenuChip( + selected = menu.pinUpdatable, + onClick = { setMenu(menu.copy(pinUpdatable = !menu.pinUpdatable)) }, + label = { Text(text = stringResource(id = R.string.menu_pin_updatable)) } + ) + + MenuChip( + selected = menu.showIcon, + onClick = { setMenu(menu.copy(showIcon = !menu.showIcon)) }, + label = { Text(text = stringResource(id = R.string.menu_show_icon)) } + ) + + MenuChip( + selected = menu.showCover, + onClick = { setMenu(menu.copy(showCover = !menu.showCover)) }, + label = { Text(text = stringResource(id = R.string.menu_show_cover)) } + ) + + MenuChip(selected = menu.showVerified, + onClick = { setMenu(menu.copy(showVerified = !menu.showVerified)) }, + label = { Text(text = stringResource(id = R.string.menu_show_verified)) }) + + MenuChip( + selected = menu.showLicense, + onClick = { setMenu(menu.copy(showLicense = !menu.showLicense)) }, + label = { Text(text = stringResource(id = R.string.menu_show_license)) } + ) + + MenuChip( + selected = menu.showAntiFeatures, + onClick = { setMenu(menu.copy(showAntiFeatures = !menu.showAntiFeatures)) }, + label = { Text(text = stringResource(id = R.string.menu_show_antifeatures)) } + ) + + MenuChip( + selected = menu.showUpdatedTime, + onClick = { setMenu(menu.copy(showUpdatedTime = !menu.showUpdatedTime)) }, + label = { Text(text = stringResource(id = R.string.menu_show_updated)) } + ) + } + + NavigationBarsSpacer() + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/com/dergoogler/mmrl/ui/screens/repository/RepositoryScreen.kt b/app/src/main/kotlin/com/dergoogler/mmrl/ui/screens/repository/RepositoryScreen.kt new file mode 100644 index 00000000..5babc027 --- /dev/null +++ b/app/src/main/kotlin/com/dergoogler/mmrl/ui/screens/repository/RepositoryScreen.kt @@ -0,0 +1,135 @@ +package com.dergoogler.mmrl.ui.screens.repository + +import androidx.activity.compose.BackHandler +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.Scaffold +import androidx.compose.material3.TopAppBarDefaults +import androidx.compose.material3.TopAppBarScrollBehavior +import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.input.nestedscroll.nestedScroll +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import androidx.navigation.NavController +import com.dergoogler.mmrl.R +import com.dergoogler.mmrl.datastore.repository.RepositoryMenuCompat +import com.dergoogler.mmrl.ui.component.Loading +import com.dergoogler.mmrl.ui.component.PageIndicator +import com.dergoogler.mmrl.ui.component.SearchTopBar +import com.dergoogler.mmrl.ui.component.TopAppBarTitle +import com.dergoogler.mmrl.ui.utils.none +import com.dergoogler.mmrl.viewmodel.RepositoryViewModel + +@Composable +fun RepositoryScreen( + navController: NavController, + viewModel: RepositoryViewModel = hiltViewModel() +) { + val list by viewModel.online.collectAsStateWithLifecycle() + + val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior() + val listState = rememberLazyListState() + + BackHandler( + enabled = viewModel.isSearch, + onBack = viewModel::closeSearch + ) + + DisposableEffect(true) { + onDispose(viewModel::closeSearch) + } + + Scaffold( + modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection), + topBar = { + TopBar( + isSearch = viewModel.isSearch, + onQueryChange = viewModel::search, + onOpenSearch = viewModel::openSearch, + onCloseSearch = viewModel::closeSearch, + setMenu = viewModel::setRepositoryMenu, + scrollBehavior = scrollBehavior + ) + }, + contentWindowInsets = WindowInsets.none + ) { innerPadding -> + Box( + modifier = Modifier.padding(innerPadding) + ) { + if (viewModel.isLoading) { + Loading() + } + + if (list.isEmpty() && !viewModel.isLoading) { + PageIndicator( + icon = R.drawable.cloud, + text = if (viewModel.isSearch) R.string.search_empty else R.string.repository_empty, + ) + } + + ModulesList( + list = list, + state = listState, + navController = navController + ) + } + } +} + +@Composable +private fun TopBar( + isSearch: Boolean, + onQueryChange: (String) -> Unit, + onOpenSearch: () -> Unit, + onCloseSearch: () -> Unit, + setMenu: (RepositoryMenuCompat) -> Unit, + scrollBehavior: TopAppBarScrollBehavior, +) { + var query by remember { mutableStateOf("") } + DisposableEffect(isSearch) { + onDispose { query = "" } + } + + SearchTopBar( + isSearch = isSearch, + query = query, + onQueryChange = { + onQueryChange(it) + query = it + }, + onClose = { + onCloseSearch() + query = "" + }, + title = { TopAppBarTitle(text = stringResource(id = R.string.page_repository)) }, + scrollBehavior = scrollBehavior, + actions = { + if (!isSearch) { + IconButton( + onClick = onOpenSearch + ) { + Icon( + painter = painterResource(id = R.drawable.search), + contentDescription = null + ) + } + } + + RepositoryMenu( + setMenu = setMenu + ) + } + ) +} \ No newline at end of file diff --git a/app/src/main/kotlin/com/dergoogler/mmrl/ui/screens/repository/view/ViewScreen.kt b/app/src/main/kotlin/com/dergoogler/mmrl/ui/screens/repository/view/ViewScreen.kt new file mode 100644 index 00000000..d36bf731 --- /dev/null +++ b/app/src/main/kotlin/com/dergoogler/mmrl/ui/screens/repository/view/ViewScreen.kt @@ -0,0 +1,126 @@ +package com.dergoogler.mmrl.ui.screens.repository.view + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.pager.HorizontalPager +import androidx.compose.foundation.pager.rememberPagerState +import androidx.compose.material3.Scaffold +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.input.nestedscroll.nestedScroll +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import androidx.core.net.toUri +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.navigation.NavController +import com.dergoogler.mmrl.R +import com.dergoogler.mmrl.model.online.VersionItem +import com.dergoogler.mmrl.ui.activity.InstallActivity +import com.dergoogler.mmrl.ui.component.CollapsingTopAppBarDefaults +import com.dergoogler.mmrl.ui.screens.repository.view.pages.AboutPage +import com.dergoogler.mmrl.ui.screens.repository.view.pages.OverviewPage +import com.dergoogler.mmrl.ui.screens.repository.view.pages.ReadmePage +import com.dergoogler.mmrl.ui.screens.repository.view.pages.VersionsPage +import com.dergoogler.mmrl.ui.utils.none +import com.dergoogler.mmrl.viewmodel.ModuleViewModel + +@Composable +fun ViewScreen( + navController: NavController, + viewModel: ModuleViewModel = hiltViewModel() +) { + val context = LocalContext.current + + val scrollBehavior = CollapsingTopAppBarDefaults.scrollBehavior() + + val hasAbout = !viewModel.isEmptyAbout + val hasReadme = !viewModel.isEmptyReadme + + val pages = mutableListOf().apply { + add(R.string.view_module_page_overview) + + if (hasReadme) { + add(R.string.view_module_page_readme) + } + + add(R.string.view_module_page_versions) + + if (hasAbout) { + add(R.string.view_module_page_about) + } + } + + val pagerState = rememberPagerState(initialPage = 0, pageCount = { pages.size }) + + val download: (VersionItem, Boolean) -> Unit = { item, install -> + viewModel.downloader(context, item) { + if (install) { + InstallActivity.start( + context = context, + uri = it.toUri() + ) + } + } + } + + Scaffold( + modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection), + topBar = { + ViewTopBar( + online = viewModel.online, + tracks = viewModel.tracks, + scrollBehavior = scrollBehavior, + navController = navController + ) + }, + contentWindowInsets = WindowInsets.none + ) { innerPadding -> + Column( + modifier = Modifier.padding(innerPadding) + ) { + ViewTab( + state = pagerState, + updatableSize = viewModel.updatableSize, + pages = pages + ) + + HorizontalPager( + state = pagerState, + modifier = Modifier.fillMaxSize() + ) { index -> + pages.getOrNull( + index % (pages.size) + )?.let { page -> + when (page) { + R.string.view_module_page_overview -> OverviewPage( + online = viewModel.online, + item = viewModel.lastVersionItem, + local = viewModel.local, + notifyUpdates = viewModel.notifyUpdates, + isProviderAlive = viewModel.isProviderAlive, + rootVersionName = viewModel.version, + setUpdatesTag = viewModel::setUpdatesTag, + onInstall = { download(it, true) }, + ) + + R.string.view_module_page_readme -> ReadmePage(url = viewModel.readme) + + R.string.view_module_page_versions -> VersionsPage( + versions = viewModel.versions, + localVersionCode = viewModel.localVersionCode, + isProviderAlive = viewModel.isProviderAlive, + getProgress = { viewModel.getProgress(it) }, + onDownload = download + ) + + R.string.view_module_page_about -> AboutPage( + online = viewModel.online + ) + } + } + } + } + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/com/dergoogler/mmrl/ui/screens/repository/view/ViewTab.kt b/app/src/main/kotlin/com/dergoogler/mmrl/ui/screens/repository/view/ViewTab.kt new file mode 100644 index 00000000..3b7b1bd3 --- /dev/null +++ b/app/src/main/kotlin/com/dergoogler/mmrl/ui/screens/repository/view/ViewTab.kt @@ -0,0 +1,160 @@ +package com.dergoogler.mmrl.ui.screens.repository.view + +import androidx.compose.animation.core.animateDp +import androidx.compose.animation.core.spring +import androidx.compose.animation.core.updateTransition +import androidx.compose.foundation.Canvas +import androidx.compose.foundation.layout.defaultMinSize +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.offset +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.wrapContentSize +import androidx.compose.foundation.pager.PagerState +import androidx.compose.material3.Badge +import androidx.compose.material3.BadgedBox +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.TabPosition +import androidx.compose.material3.TabRow +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.shadow +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.StrokeCap +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import com.dergoogler.mmrl.R +import com.dergoogler.mmrl.ui.component.Tab +import kotlinx.coroutines.launch + +@Composable +fun ViewTab( + state: PagerState, + updatableSize: Int, + pages: List, + modifier: Modifier = Modifier +) { + val scope = rememberCoroutineScope() + + TabRow( + modifier = modifier, + selectedTabIndex = state.currentPage, + indicator = { tabPositions: List -> + AnimatedIndicator( + tabPositions = tabPositions, + selectedTabIndex = state.currentPage + ) + }, + divider = { + HorizontalDivider( + thickness = 0.3.dp, + modifier = Modifier.shadow(6.dp) + ) + } + ) { + pages.forEach { text -> + val index = pages.indexOfFirst { it == text } + + Tab( + modifier = Modifier.padding(vertical = 12.dp), + selected = state.currentPage == index, + onClick = { + scope.launch { + state.animateScrollToPage(index) + } + }, + selectedContentColor = MaterialTheme.colorScheme.primary, + unselectedContentColor = MaterialTheme.colorScheme.onSurfaceVariant + ) { + BadgedBox( + badge = { + if (text == R.string.view_module_page_versions && updatableSize != 0) { + Badge( + containerColor = MaterialTheme.colorScheme.error + ) { + Text( + text = updatableSize.toString(), + style = MaterialTheme.typography.labelSmall, + color = MaterialTheme.colorScheme.onError + ) + } + } + } + ) { + Text( + text = stringResource(id = text), + style = MaterialTheme.typography.labelLarge, + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + } + } + } + } +} + +@Composable +private fun Indicator( + color: Color, + modifier: Modifier = Modifier +) { + Canvas( + modifier = modifier.defaultMinSize(minHeight = 3.dp), + ) { + val width = size.width / 4 + + drawLine( + color = color, + start = Offset(width, size.height), + end = Offset(width * 3, size.height), + strokeWidth = size.height * 2, + cap = StrokeCap.Round + ) + } +} + +@Composable +private fun AnimatedIndicator(tabPositions: List, selectedTabIndex: Int) { + val transition = updateTransition(selectedTabIndex, label = "Indicator") + val indicatorStart by transition.animateDp( + transitionSpec = { + if (initialState < targetState) { + spring(dampingRatio = 1f, stiffness = 50f) + } else { + spring(dampingRatio = 1f, stiffness = 1000f) + } + }, + label = "Indicator" + ) { + tabPositions[it].left + } + + val indicatorEnd by transition.animateDp( + transitionSpec = { + if (initialState < targetState) { + spring(dampingRatio = 1f, stiffness = 1000f) + } else { + spring(dampingRatio = 1f, stiffness = 50f) + } + }, + label = "Indicator" + ) { + tabPositions[it].right + } + + Indicator( + color = MaterialTheme.colorScheme.primary, + modifier = Modifier + .wrapContentSize(align = Alignment.BottomStart) + .offset(x = indicatorStart) + .width(indicatorEnd - indicatorStart) + .height(3.dp) + ) +} \ No newline at end of file diff --git a/app/src/main/kotlin/com/dergoogler/mmrl/ui/screens/repository/view/ViewTopBar.kt b/app/src/main/kotlin/com/dergoogler/mmrl/ui/screens/repository/view/ViewTopBar.kt new file mode 100644 index 00000000..f39a7396 --- /dev/null +++ b/app/src/main/kotlin/com/dergoogler/mmrl/ui/screens/repository/view/ViewTopBar.kt @@ -0,0 +1,194 @@ +package com.dergoogler.mmrl.ui.screens.repository.view + +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.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBarScrollBehavior +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.platform.LocalContext +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.buildAnnotatedString +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import androidx.navigation.NavController +import coil.compose.AsyncImage +import com.dergoogler.mmrl.R +import com.dergoogler.mmrl.database.entity.Repo +import com.dergoogler.mmrl.model.online.OnlineModule +import com.dergoogler.mmrl.model.online.TrackJson +import com.dergoogler.mmrl.ui.component.CollapsingTopAppBar +import com.dergoogler.mmrl.ui.component.CollapsingTopAppBarDefaults +import com.dergoogler.mmrl.ui.component.Logo +import com.dergoogler.mmrl.ui.providable.LocalUserPreferences +import com.dergoogler.mmrl.ui.screens.repository.view.items.LicenseItem +import com.dergoogler.mmrl.ui.screens.repository.view.items.TagItem +import com.dergoogler.mmrl.ui.screens.repository.view.items.TrackItem +import com.dergoogler.mmrl.utils.extensions.openUrl + +@Composable +fun ViewTopBar( + online: OnlineModule, + tracks: List>, + scrollBehavior: TopAppBarScrollBehavior, + navController: NavController +) = CollapsingTopAppBar( + title = { + Text( + text = online.name, + style = MaterialTheme.typography.titleLarge, + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + }, + content = topBarContent( + module = online, + tracks = tracks + ), + navigationIcon = { + IconButton( + onClick = { navController.popBackStack() } + ) { + Icon( + painter = painterResource(id = R.drawable.arrow_left), + contentDescription = null + ) + } + }, + scrollBehavior = scrollBehavior, + colors = CollapsingTopAppBarDefaults.topAppBarColors( + scrolledContainerColor = MaterialTheme.colorScheme.surface + ) +) + +@Composable +private fun topBarContent( + module: OnlineModule, + tracks: List>, +): @Composable ColumnScope.() -> Unit = { + val userPreferences = LocalUserPreferences.current + val repositoryMenu = userPreferences.repositoryMenu + + val context = LocalContext.current + val hasLicense = module.hasLicense + val hasDonate = module.donate.orEmpty().isNotBlank() + + Row( + modifier = Modifier.padding(horizontal = 16.dp), + verticalAlignment = Alignment.Top + ) { + if (repositoryMenu.showIcon) { + if (module.icon.orEmpty().isNotEmpty()) { + AsyncImage( + model = module.icon, + modifier = Modifier + .size(55.dp) + .clip(CircleShape), + contentDescription = null + ) + } else { + Logo( + icon = R.drawable.box, + modifier = Modifier.size(55.dp), + contentColor = MaterialTheme.colorScheme.onSecondaryContainer, + containerColor = MaterialTheme.colorScheme.secondaryContainer + ) + } + + Spacer(modifier = Modifier.width(16.dp)) + } + + Column( + modifier = Modifier.weight(1f) + ) { + + + Row( + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = module.name, + style = MaterialTheme.typography.titleMedium, + maxLines = 2, + overflow = TextOverflow.Ellipsis + ) + if (module.isVerified) { + Spacer(modifier = Modifier.width(4.dp)) + + val iconSize = + with(LocalDensity.current) { MaterialTheme.typography.titleMedium.fontSize.toDp() * 1.0f } + + Icon( + modifier = Modifier.size(iconSize), + painter = painterResource(id = R.drawable.rosette_discount_check), + contentDescription = null, + tint = MaterialTheme.colorScheme.onPrimaryContainer + ) + } + } + + + Spacer(modifier = Modifier.height(4.dp)) + Text( + text = module.author, + style = MaterialTheme.typography.bodyMedium, + maxLines = 2, + overflow = TextOverflow.Ellipsis + ) + + Text( + text = buildAnnotatedString { + append("ID = ${module.id}") + if (hasLicense) { + append(", ") + append("License = ${module.license}") + } + }, + style = MaterialTheme.typography.labelSmall, + color = MaterialTheme.colorScheme.outline + ) + } + } + + Row( + modifier = Modifier + .padding(top = 10.dp) + .padding(horizontal = 12.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(8.dp) + ) { + TrackItem( + tracks = tracks + ) + + + module.license?.let { + LicenseItem( + licenseId = it + ) + } + + + module.donate?.let { + TagItem( + icon = R.drawable.currency_dollar, + onClick = { context.openUrl(it) } + ) + } + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/com/dergoogler/mmrl/ui/screens/repository/view/items/LicenseItem.kt b/app/src/main/kotlin/com/dergoogler/mmrl/ui/screens/repository/view/items/LicenseItem.kt new file mode 100644 index 00000000..110880cc --- /dev/null +++ b/app/src/main/kotlin/com/dergoogler/mmrl/ui/screens/repository/view/items/LicenseItem.kt @@ -0,0 +1,57 @@ +package com.dergoogler.mmrl.ui.screens.repository.view.items + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.BottomSheetDefaults +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.ModalBottomSheet +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +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.res.stringResource +import androidx.compose.ui.unit.dp +import com.dergoogler.mmrl.R +import com.dergoogler.mmrl.ui.component.LicenseContent +import com.dergoogler.mmrl.ui.component.NavigationBarsSpacer +import com.dergoogler.mmrl.ui.utils.expandedShape + +@Composable +fun LicenseItem( + licenseId: String +) = Box { + var open by rememberSaveable { mutableStateOf(false) } + + TagItem( + icon = R.drawable.file_certificate, + onClick = { open = true } + ) + + if (open) { + ModalBottomSheet( + onDismissRequest = { open = false }, + shape = BottomSheetDefaults.expandedShape(15.dp), + windowInsets = WindowInsets(0) + ) { + Text( + text = stringResource(id = R.string.license_title), + style = MaterialTheme.typography.headlineSmall, + modifier = Modifier.align(Alignment.CenterHorizontally) + ) + + LicenseContent( + licenseId = licenseId, + modifier = Modifier + .padding(top = 16.dp) + .padding(horizontal = 16.dp) + ) + + NavigationBarsSpacer() + } + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/com/dergoogler/mmrl/ui/screens/repository/view/items/TagItem.kt b/app/src/main/kotlin/com/dergoogler/mmrl/ui/screens/repository/view/items/TagItem.kt new file mode 100644 index 00000000..fbe60e23 --- /dev/null +++ b/app/src/main/kotlin/com/dergoogler/mmrl/ui/screens/repository/view/items/TagItem.kt @@ -0,0 +1,29 @@ +package com.dergoogler.mmrl.ui.screens.repository.view.items + +import androidx.annotation.DrawableRes +import androidx.compose.foundation.layout.size +import androidx.compose.material3.FilledTonalIconButton +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButtonDefaults +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.unit.dp + +@Composable +fun TagItem( + @DrawableRes icon: Int, + onClick: () -> Unit +) = FilledTonalIconButton( + onClick = onClick, + colors = IconButtonDefaults.filledTonalIconButtonColors( + containerColor = MaterialTheme.colorScheme.surfaceVariant + ), + modifier = Modifier.size(35.dp), +) { + Icon( + painter = painterResource(id = icon), + contentDescription = null + ) +} \ No newline at end of file diff --git a/app/src/main/kotlin/com/dergoogler/mmrl/ui/screens/repository/view/items/TrackItem.kt b/app/src/main/kotlin/com/dergoogler/mmrl/ui/screens/repository/view/items/TrackItem.kt new file mode 100644 index 00000000..cd1bd9e7 --- /dev/null +++ b/app/src/main/kotlin/com/dergoogler/mmrl/ui/screens/repository/view/items/TrackItem.kt @@ -0,0 +1,140 @@ +package com.dergoogler.mmrl.ui.screens.repository.view.items + +import androidx.compose.foundation.BorderStroke +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.Spacer +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.BottomSheetDefaults +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.ModalBottomSheet +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +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.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import com.dergoogler.mmrl.R +import com.dergoogler.mmrl.database.entity.Repo +import com.dergoogler.mmrl.model.online.TrackJson +import com.dergoogler.mmrl.ui.component.NavigationBarsSpacer +import com.dergoogler.mmrl.ui.utils.expandedShape +import com.dergoogler.mmrl.utils.extensions.toDateTime + +@Composable +fun TrackItem( + tracks: List> +) = Box { + var open by rememberSaveable { mutableStateOf(false) } + + TagItem( + icon = R.drawable.tag, + onClick = { open = true } + ) + + if (open) { + ModalBottomSheet( + onDismissRequest = { open = false }, + shape = BottomSheetDefaults.expandedShape(15.dp), + windowInsets = WindowInsets(0) + ) { + Text( + text = stringResource(id = R.string.view_module_view_track), + style = MaterialTheme.typography.headlineSmall, + modifier = Modifier.align(Alignment.CenterHorizontally) + ) + + LazyColumn( + modifier = Modifier + .padding(top = 16.dp) + .padding(horizontal = 16.dp), + verticalArrangement = Arrangement.spacedBy(16.dp) + ) { + items( + items = tracks, + key = { it.first.url } + ) { (repo, track) -> + ValueItem( + repo = repo, + track = track + ) + } + + item { + NavigationBarsSpacer() + } + } + } + } +} + +@Composable +private fun ValueItem( + repo: Repo, + track: TrackJson +) = Surface( + modifier = Modifier.fillMaxWidth(), + tonalElevation = 6.dp, + border = BorderStroke(1.dp, MaterialTheme.colorScheme.outline), + shape = RoundedCornerShape(15.dp) +) { + Row( + modifier = Modifier.padding(all = 16.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Icon( + modifier = Modifier.size(30.dp), + painter = painterResource(id = R.drawable.code_asterix), + contentDescription = null, + tint = MaterialTheme.colorScheme.onSurfaceVariant + ) + + Column( + modifier = Modifier + .padding(start = 16.dp) + .fillMaxWidth() + ) { + Text( + text = repo.name, + style = MaterialTheme.typography.titleMedium, + color = MaterialTheme.colorScheme.primary + ) + + Spacer(modifier = Modifier.height(8.dp)) + + Text( + text = stringResource(id = R.string.view_module_type, + track.type.name.replace("_", " ")), + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.outline + ) + + stringResource(id = R.string.view_module_added, + track.added!!.toDateTime() + ) + + Text( + text = stringResource(id = R.string.view_module_added, + track.added.toDateTime()), + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.outline + ) + } + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/com/dergoogler/mmrl/ui/screens/repository/view/pages/AboutPage.kt b/app/src/main/kotlin/com/dergoogler/mmrl/ui/screens/repository/view/pages/AboutPage.kt new file mode 100644 index 00000000..b6d24d60 --- /dev/null +++ b/app/src/main/kotlin/com/dergoogler/mmrl/ui/screens/repository/view/pages/AboutPage.kt @@ -0,0 +1,247 @@ +package com.dergoogler.mmrl.ui.screens.repository.view.pages + +import androidx.annotation.DrawableRes +import androidx.annotation.StringRes +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.material3.AssistChipDefaults +import androidx.compose.material3.ElevatedAssistChip +import androidx.compose.material3.HorizontalDivider +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.platform.LocalContext +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import com.dergoogler.mmrl.R +import com.dergoogler.mmrl.model.online.ModuleFeatures +import com.dergoogler.mmrl.model.online.OnlineModule +import com.dergoogler.mmrl.ui.component.LabelItem +import com.dergoogler.mmrl.utils.extensions.isObjectEmpty +import com.dergoogler.mmrl.utils.extensions.openUrl + +@Composable +fun AboutPage( + online: OnlineModule +) = Column( + modifier = Modifier + .fillMaxSize() + .verticalScroll(rememberScrollState()) +) { + HelpItem(online = online) + + HorizontalDivider(thickness = 0.9.dp) + + online.features?.let { + // TODO: find other way to check if is empty. build.gradle at exclude : "kotlin/**" to reduce size + if (!it.isObjectEmpty()) { + FeaturesItem(features = it) + + HorizontalDivider(thickness = 0.9.dp) + } + } +} + + +@Composable +private fun HelpItem( + online: OnlineModule +) = Column( + modifier = Modifier + .padding(all = 16.dp) + .fillMaxWidth(), + verticalArrangement = Arrangement.spacedBy(16.dp) +) { + Text( + text = stringResource(id = R.string.view_module_help), + style = MaterialTheme.typography.titleSmall, + color = MaterialTheme.colorScheme.primary + ) + + online.homepage?.let { + ValueItem( + key = stringResource(id = R.string.view_module_homepage), + value = it, + icon = R.drawable.world_www + ) + } + + ValueItem( + key = stringResource(id = R.string.view_module_source), + value = online.track.source, + icon = R.drawable.brand_git + ) + + online.support?.let { + ValueItem( + key = stringResource(id = R.string.view_module_support), + value = it, + icon = R.drawable.heart_handshake + ) + } +} + + +@Composable +private fun FeaturesItem( + features: ModuleFeatures +) = Column( + modifier = Modifier + .padding(all = 16.dp) + .fillMaxWidth(), + verticalArrangement = Arrangement.spacedBy(16.dp) +) { + Text( + text = stringResource(id = R.string.view_module_features), + style = MaterialTheme.typography.titleSmall, + color = MaterialTheme.colorScheme.primary + ) + + FeatureValueItem( + feature = features.service, + key = R.string.view_module_features_service, + value = R.string.view_module_features_service_sub + ) + FeatureValueItem( + feature = features.postFsData, + key = R.string.view_module_features_post_fs_data, + value = R.string.view_module_features_post_fs_data_sub + ) + FeatureValueItem( + feature = features.resetprop, + key = R.string.view_module_features_system_properties, + value = R.string.view_module_features_resetprop_sub + ) + FeatureValueItem( + feature = features.sepolicy, + key = R.string.view_module_features_selinux_policy, + value = R.string.view_module_features_sepolicy_sub + ) + FeatureValueItem( + feature = features.zygisk, + key = R.string.view_module_features_zygisk, + value = R.string.view_module_features_zygisk_sub + ) + FeatureValueItem( + feature = features.apks, + key = R.string.view_module_features_apks, + value = R.string.view_module_features_apks_sub + ) + FeatureValueItem( + feature = features.webroot, + key = R.string.view_module_features_webui, + value = R.string.view_module_features_webui_sub, + rootSolutions = listOf("KernelSU", "APatch") + ) + FeatureValueItem( + feature = features.postMount, + key = R.string.view_module_features_post_mount, + value = R.string.view_module_features_postmount_sub, + rootSolutions = listOf("KernelSU", "APatch") + ) + FeatureValueItem( + feature = features.bootCompleted, + key = R.string.view_module_features_boot_completed, + value = R.string.view_module_features_bootcompleted_sub, + rootSolutions = listOf("KernelSU", "APatch") + ) +} + + +@Composable +private fun ValueItem( + key: String, + value: String, + @DrawableRes icon: Int = R.drawable.world_www +) { + if (value.isBlank()) return + val context = LocalContext.current + + Row( + modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(16.dp) + ) { + Column( + modifier = Modifier + .fillMaxWidth() + .weight(1f), + verticalArrangement = Arrangement.spacedBy(4.dp) + ) { + Text( + text = key, + style = MaterialTheme.typography.titleSmall, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + + Text( + text = value, + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.outline + ) + } + + ElevatedAssistChip( + onClick = { context.openUrl(value) }, + label = { Text(text = stringResource(id = R.string.open)) }, + leadingIcon = { + Icon( + painter = painterResource(id = icon), + contentDescription = null, + modifier = Modifier.size(AssistChipDefaults.IconSize) + ) + } + ) + } +} + +@Composable +private fun FeatureValueItem( + feature: Boolean?, + @StringRes key: Int, + @StringRes value: Int, + modifier: Modifier = Modifier, + rootSolutions: List? = null, +) { + if (feature == null) return + + Column( + modifier = modifier.fillMaxWidth(), + verticalArrangement = Arrangement.spacedBy(4.dp) + ) { + Text( + text = stringResource(id = key), + style = MaterialTheme.typography.titleSmall, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + + Text( + text = stringResource(id = value), + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.outline + ) + rootSolutions?.let { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(2.dp) + ) { + rootSolutions.forEach { root -> + LabelItem( + text = root, + ) + } + } + } + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/com/dergoogler/mmrl/ui/screens/repository/view/pages/OverviewPage.kt b/app/src/main/kotlin/com/dergoogler/mmrl/ui/screens/repository/view/pages/OverviewPage.kt new file mode 100644 index 00000000..0760409b --- /dev/null +++ b/app/src/main/kotlin/com/dergoogler/mmrl/ui/screens/repository/view/pages/OverviewPage.kt @@ -0,0 +1,402 @@ +package com.dergoogler.mmrl.ui.screens.repository.view.pages + +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.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.lazy.LazyRow +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.AssistChipDefaults +import androidx.compose.material3.ElevatedAssistChip +import androidx.compose.material3.ElevatedFilterChip +import androidx.compose.material3.FilterChipDefaults +import androidx.compose.material3.HorizontalDivider +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.draw.clip +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import coil.compose.AsyncImage +import com.dergoogler.mmrl.R +import com.dergoogler.mmrl.model.local.LocalModule +import com.dergoogler.mmrl.model.local.versionDisplay +import com.dergoogler.mmrl.model.online.OnlineModule +import com.dergoogler.mmrl.model.online.VersionItem +import com.dergoogler.mmrl.ui.component.Alert +import com.dergoogler.mmrl.ui.component.LabelItem +import com.dergoogler.mmrl.utils.extensions.toDateTime +import com.dergoogler.mmrl.utils.extensions.toFormattedDate +import java.util.Locale + +@Composable +fun OverviewPage( + online: OnlineModule, + item: VersionItem?, + local: LocalModule?, + isProviderAlive: Boolean, + rootVersionName: String, + notifyUpdates: Boolean, + setUpdatesTag: (Boolean) -> Unit, + onInstall: (VersionItem) -> Unit +) = Column( + modifier = Modifier + .fillMaxSize() + .verticalScroll(rememberScrollState()) +) { + + online.root?.let { + if (it.isNotSupported(rootVersionName)) { + Alert( + title = stringResource(id = R.string.view_module_unsupported), + backgroundColor = MaterialTheme.colorScheme.errorContainer, + textColor = MaterialTheme.colorScheme.onErrorContainer, + message = stringResource(id = R.string.view_module_unsupported_desc), + modifier = Modifier.padding(top = 8.dp, end = 8.dp, start = 8.dp, bottom = 4.dp) + ) + } + } + + online.note?.let { + it.message?.let { it1 -> + Alert( + title = it.title, + message = it1, + modifier = Modifier.padding(top = 8.dp, end = 8.dp, start = 8.dp, bottom = 4.dp) + ) + } + } + + Column( + modifier = Modifier + .padding(all = 16.dp) + .fillMaxWidth() + ) { + Text( + text = stringResource(id = R.string.view_module_description), + style = MaterialTheme.typography.titleSmall, + color = MaterialTheme.colorScheme.primary + ) + + Spacer(modifier = Modifier.height(8.dp)) + Text( + text = online.description, + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + + online.categories?.let { + if (it.isNotEmpty()) { + Spacer(modifier = Modifier.height(8.dp)) + + Row( + modifier = Modifier + .fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(4.dp) + ) { + if (it.isNotEmpty()) { + it.forEach { category -> + LabelItem(text = category) + } + } + } + } + } + } + + HorizontalDivider(thickness = 0.9.dp) + + if (item != null) { + CloudItem( + item = item, + size = online.size, + isProviderAlive = isProviderAlive, + onInstall = onInstall + ) + + HorizontalDivider(thickness = 0.9.dp) + } + + if (local != null) { + LocalItem( + local = local, + notifyUpdates = notifyUpdates, + setUpdatesTag = setUpdatesTag + ) + + HorizontalDivider(thickness = 0.9.dp) + } + + online.track.antifeatures?.let { + if (it.isNotEmpty()) { + AntiFeaturesItem(antifeatures = it) + + HorizontalDivider(thickness = 0.9.dp) + } + } + + online.screenshots?.let { + if (it.isNotEmpty()) { + ScreenshotsItem(images = it) + + HorizontalDivider(thickness = 0.9.dp) + } + } +} + +@Composable +private fun CloudItem( + item: VersionItem, + size: Int?, + isProviderAlive: Boolean, + onInstall: (VersionItem) -> Unit +) = Column( + modifier = Modifier + .padding(all = 16.dp) + .fillMaxWidth(), + verticalArrangement = Arrangement.spacedBy(16.dp) +) { + Text( + text = stringResource(id = R.string.view_module_cloud), + style = MaterialTheme.typography.titleSmall, + color = MaterialTheme.colorScheme.primary + ) + + Row( + modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(16.dp) + ) { + ValueItem( + key = stringResource(id = R.string.view_module_version), + value = item.versionDisplay, + modifier = Modifier.weight(1f) + ) + + ElevatedAssistChip( + enabled = isProviderAlive, + onClick = { onInstall(item) }, + label = { Text(text = stringResource(id = R.string.module_install)) }, + leadingIcon = { + Icon( + painter = painterResource(id = R.drawable.device_mobile_down), + contentDescription = null, + modifier = Modifier.size(AssistChipDefaults.IconSize) + ) + } + ) + } + + ValueItem( + key = stringResource(id = R.string.view_module_last_updated), + value = item.timestamp.toFormattedDate() + ) + + size?.let { + ValueItem( + key = stringResource(id = R.string.view_module_file_size), + value = formatFileSize(it) + ) + } +} + +@Composable +private fun LocalItem( + local: LocalModule, + notifyUpdates: Boolean, + setUpdatesTag: (Boolean) -> Unit +) = Column( + modifier = Modifier + .padding(all = 16.dp) + .fillMaxWidth(), + verticalArrangement = Arrangement.spacedBy(16.dp) +) { + Text( + text = stringResource(id = R.string.view_module_local), + style = MaterialTheme.typography.titleSmall, + color = MaterialTheme.colorScheme.primary + ) + + Row( + modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(16.dp) + ) { + ValueItem( + key = stringResource(id = R.string.view_module_version), + value = local.versionDisplay, + modifier = Modifier.weight(1f) + ) + + ElevatedFilterChip( + selected = notifyUpdates, + onClick = { setUpdatesTag(!notifyUpdates) }, + label = { + Text( + text = stringResource( + id = if (notifyUpdates) { + R.string.view_module_update_ignore + } else { + R.string.view_module_update_notify + } + ) + ) + }, + leadingIcon = { + Icon( + painter = painterResource( + id = if (notifyUpdates) { + R.drawable.target_off + } else { + R.drawable.target + } + ), + contentDescription = null, + modifier = Modifier.size(FilterChipDefaults.IconSize) + ) + } + ) + } + + if (local.lastUpdated != 0L) { + ValueItem( + key = stringResource(id = R.string.view_module_last_updated), + value = local.lastUpdated.toDateTime() + ) + } +} + + +@Composable +private fun AntiFeaturesItem( + antifeatures: List, +) = Column( + modifier = Modifier + .padding(all = 16.dp) + .fillMaxWidth(), + verticalArrangement = Arrangement.spacedBy(16.dp) +) { + Text( + text = stringResource(id = R.string.view_module_antifeatures), + style = MaterialTheme.typography.titleSmall, + color = MaterialTheme.colorScheme.primary + ) + + antifeatures.forEach { antifeature -> + val result = getAntifeatureDetails(antifeature) + result?.let { + val (nameResId, descResId) = result + ValueItem(key = stringResource(id = nameResId), value = stringResource(id = descResId)) + } + } +} + +fun getAntifeatureDetails(id: String?): Pair? { + // A map to store the resource IDs for names and descriptions + val antifeatureMap = mapOf( + "ads" to (R.string.ads_name to R.string.ads_desc), + "knownvuln" to (R.string.knownvuln_name to R.string.knownvuln_desc), + "nsfw" to (R.string.nsfw_name to R.string.nsfw_desc), + "nosourcesince" to (R.string.nosourcesince_name to R.string.nosourcesince_desc), + "nonfreeadd" to (R.string.nonfreeadd_name to R.string.nonfreeadd_desc), + "nonfreeassets" to (R.string.nonfreeassets_name to R.string.nonfreeassets_desc), + "nonfreedep" to (R.string.nonfreedep_name to R.string.nonfreedep_desc), + "nonfreenet" to (R.string.nonfreenet_name to R.string.nonfreenet_desc), + "tracking" to (R.string.tracking_name to R.string.tracking_desc), + "upstreamnonfree" to (R.string.upstreamnonfree_name to R.string.upstreamnonfree_desc), + "obfuscation" to (R.string.obfuscation_name to R.string.obfuscation_desc), + "unaskedremoval" to (R.string.unaskedremoval_name to R.string.unaskedremoval_desc) + ) + + // Check if ID is not null and if it exists in the map + return id?.lowercase()?.let { antifeatureMap[it] } +} + +@Composable +private fun ValueItem( + key: String, + value: String?, + modifier: Modifier = Modifier +) { + if (value.isNullOrBlank()) return + + Column( + modifier = modifier.fillMaxWidth(), + verticalArrangement = Arrangement.spacedBy(4.dp) + ) { + Text( + text = key, + style = MaterialTheme.typography.titleSmall, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + + Text( + text = value, + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.outline + ) + } +} + +@Composable +private fun ScreenshotsItem( + images: List, +) = Column( + modifier = Modifier + .padding(all = 16.dp) + .fillMaxWidth(), + verticalArrangement = Arrangement.spacedBy(16.dp) +) { + Text( + text = stringResource(id = R.string.view_module_screenshots), + style = MaterialTheme.typography.titleSmall, + color = MaterialTheme.colorScheme.primary + ) + + LazyRow( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(4.dp) + ) { + items(images.size) { imageUrl -> + AsyncImage( + model = images[imageUrl], + contentDescription = null, + modifier = Modifier + .width(200.dp) + .clip(RoundedCornerShape(8.dp)), + contentScale = ContentScale.Crop + ) + } + } +} + + +fun formatFileSize(sizeInBytes: Int): String { + if (sizeInBytes < 1024) return "$sizeInBytes B" + + val units = arrayOf("B", "KB", "MB", "GB", "TB", "PB") + var size = sizeInBytes.toDouble() + var unitIndex = 0 + + while (size >= 1024 && unitIndex < units.size - 1) { + size /= 1024 + unitIndex++ + } + + // Use the system's current default locale + return String.format(Locale.getDefault(), "%.2f %s", size, units[unitIndex]) +} \ No newline at end of file diff --git a/app/src/main/kotlin/com/dergoogler/mmrl/ui/screens/repository/view/pages/ReadmePage.kt b/app/src/main/kotlin/com/dergoogler/mmrl/ui/screens/repository/view/pages/ReadmePage.kt new file mode 100644 index 00000000..6be23306 --- /dev/null +++ b/app/src/main/kotlin/com/dergoogler/mmrl/ui/screens/repository/view/pages/ReadmePage.kt @@ -0,0 +1,70 @@ +package com.dergoogler.mmrl.ui.screens.repository.view.pages + +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.animateContentSize +import androidx.compose.animation.core.Spring +import androidx.compose.animation.core.spring +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +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.AlertDialogDefaults +import androidx.compose.material3.MaterialTheme +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.Modifier +import androidx.compose.ui.unit.dp +import com.dergoogler.mmrl.app.Event.Companion.isLoading +import com.dergoogler.mmrl.app.Event.Companion.isSucceeded +import com.dergoogler.mmrl.network.compose.requestString +import com.dergoogler.mmrl.ui.component.Loading +import com.dergoogler.mmrl.ui.component.MarkdownText + +@Composable +fun ReadmePage( + url: String, +) { + var readme by remember { mutableStateOf("") } + val event = requestString( + url = url, + onSuccess = { readme = it } + ) + + Column( + modifier = Modifier + .fillMaxWidth(), + ) { + AnimatedVisibility( + visible = event.isLoading, + enter = fadeIn(), + exit = fadeOut() + ) { + Loading(minHeight = 200.dp) + } + + AnimatedVisibility( + visible = event.isSucceeded, + enter = fadeIn(), + exit = fadeOut() + ) { + MarkdownText( + text = readme, + color = AlertDialogDefaults.textContentColor, + style = MaterialTheme.typography.bodyMedium, + modifier = Modifier + .verticalScroll(rememberScrollState()) + .padding(horizontal = 18.dp) + .padding(top = 8.dp) + .padding(bottom = 18.dp) + ) + } + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/com/dergoogler/mmrl/ui/screens/repository/view/pages/VersionsPage.kt b/app/src/main/kotlin/com/dergoogler/mmrl/ui/screens/repository/view/pages/VersionsPage.kt new file mode 100644 index 00000000..d3065b6a --- /dev/null +++ b/app/src/main/kotlin/com/dergoogler/mmrl/ui/screens/repository/view/pages/VersionsPage.kt @@ -0,0 +1,131 @@ +package com.dergoogler.mmrl.ui.screens.repository.view.pages + +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.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.LinearProgressIndicator +import androidx.compose.material3.MaterialTheme +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.graphics.StrokeCap +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import com.dergoogler.mmrl.R +import com.dergoogler.mmrl.database.entity.Repo +import com.dergoogler.mmrl.model.online.VersionItem +import com.dergoogler.mmrl.ui.component.LabelItem +import com.dergoogler.mmrl.ui.component.VersionItemBottomSheet +import com.dergoogler.mmrl.utils.extensions.toDate +import com.dergoogler.mmrl.utils.extensions.toFormattedDate + +@Composable +fun VersionsPage( + versions: List>, + localVersionCode: Int, + isProviderAlive: Boolean, + getProgress: @Composable (VersionItem) -> Float, + onDownload: (VersionItem, Boolean) -> Unit +) = LazyColumn( + modifier = Modifier.fillMaxSize() +) { + items( + items = versions, + key = { it.first.url + it.second.versionCode } + ) { (repo, item) -> + VersionItem( + item = item, + repo = repo, + localVersionCode = localVersionCode, + isProviderAlive = isProviderAlive, + onDownload = { onDownload(item, it) } + ) + + val progress = getProgress(item) + if (progress != 0f) { + LinearProgressIndicator( + progress = { progress }, + strokeCap = StrokeCap.Round, + modifier = Modifier + .height(2.dp) + .fillMaxWidth() + ) + } else { + HorizontalDivider(thickness = 0.9.dp) + } + } +} + +@Composable +private fun VersionItem( + item: VersionItem, + repo: Repo, + localVersionCode: Int, + isProviderAlive: Boolean, + onDownload: (Boolean) -> Unit +) { + var open by remember { mutableStateOf(false) } + if (open) VersionItemBottomSheet( + isUpdate = false, + item = item, + isProviderAlive = isProviderAlive, + onClose = { open = false }, + onDownload = onDownload + ) + + Row( + modifier = Modifier + .clickable(onClick = { open = true }) + .padding(all = 16.dp) + .fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically + ) { + Column( + modifier = Modifier.weight(1f), + verticalArrangement = Arrangement.spacedBy(4.dp) + ) { + Row( + horizontalArrangement = Arrangement.spacedBy(4.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = item.versionDisplay, + style = MaterialTheme.typography.bodyMedium + ) + + if (localVersionCode < item.versionCode) { + LabelItem( + text = stringResource(id = R.string.module_new), + containerColor = MaterialTheme.colorScheme.error, + contentColor = MaterialTheme.colorScheme.onError + ) + } + } + + Text( + text = stringResource(id = R.string.view_module_provided, repo.name), + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.primary + ) + } + + Text( + text = item.timestamp.toFormattedDate(), + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.outline + ) + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/com/dergoogler/mmrl/ui/screens/settings/SettingsScreen.kt b/app/src/main/kotlin/com/dergoogler/mmrl/ui/screens/settings/SettingsScreen.kt new file mode 100644 index 00000000..f7648ffe --- /dev/null +++ b/app/src/main/kotlin/com/dergoogler/mmrl/ui/screens/settings/SettingsScreen.kt @@ -0,0 +1,134 @@ +package com.dergoogler.mmrl.ui.screens.settings + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.WindowInsets +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.Scaffold +import androidx.compose.material3.TopAppBar +import androidx.compose.material3.TopAppBarDefaults +import androidx.compose.material3.TopAppBarScrollBehavior +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.input.nestedscroll.nestedScroll +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.navigation.NavController +import com.dergoogler.mmrl.BuildConfig +import com.dergoogler.mmrl.R +import com.dergoogler.mmrl.app.Const +import com.dergoogler.mmrl.datastore.UserPreferencesCompat.Companion.isNonRoot +import com.dergoogler.mmrl.datastore.UserPreferencesCompat.Companion.isRoot +import com.dergoogler.mmrl.datastore.WorkingMode +import com.dergoogler.mmrl.ui.component.SettingNormalItem +import com.dergoogler.mmrl.ui.component.TopAppBarTitle +import com.dergoogler.mmrl.ui.navigation.graphs.SettingsScreen +import com.dergoogler.mmrl.ui.providable.LocalUserPreferences +import com.dergoogler.mmrl.ui.screens.settings.items.NonRootItem +import com.dergoogler.mmrl.ui.screens.settings.items.RootItem +import com.dergoogler.mmrl.ui.utils.navigateSingleTopTo +import com.dergoogler.mmrl.ui.utils.none +import com.dergoogler.mmrl.utils.extensions.openUrl +import com.dergoogler.mmrl.viewmodel.SettingsViewModel + +@Composable +fun SettingsScreen( + navController: NavController, + viewModel: SettingsViewModel = hiltViewModel() +) { + val userPreferences = LocalUserPreferences.current + val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior() + + val context = LocalContext.current + + Scaffold( + modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection), + topBar = { + TopBar( + scrollBehavior = scrollBehavior + ) + }, + contentWindowInsets = WindowInsets.none + ) { innerPadding -> + Column( + modifier = Modifier + .padding(innerPadding) + .fillMaxWidth() + .verticalScroll(rememberScrollState()) + ) { + when { + userPreferences.workingMode.isRoot -> RootItem( + isAlive = viewModel.isProviderAlive, + version = viewModel.version + ) + + userPreferences.workingMode.isNonRoot -> NonRootItem() + } + + SettingNormalItem( + icon = R.drawable.launcher_outline, + title = stringResource(id = R.string.settings_app), + desc = stringResource(id = R.string.settings_app_desc), + onClick = { + navController.navigateSingleTopTo(SettingsScreen.App.route) + } + ) + + SettingNormalItem( + icon = R.drawable.git_pull_request, + title = stringResource(id = R.string.settings_repo), + desc = stringResource(id = R.string.settings_repo_desc), + onClick = { + navController.navigateSingleTopTo(SettingsScreen.Repositories.route) + } + ) + + SettingNormalItem( + icon = R.drawable.file_3d, + title = stringResource(id = R.string.settings_resources), + desc = stringResource(id = R.string.settings_resources_desc), + onClick = { + context.openUrl(Const.RESOURCES_URL) + } + ) + + SettingNormalItem( + icon = R.drawable.components, + title = stringResource(id = R.string.setup_mode), + desc = stringResource( + id = when (userPreferences.workingMode) { + WorkingMode.MODE_ROOT -> R.string.setup_root_title + WorkingMode.MODE_SHIZUKU -> R.string.setup_shizuku_title + WorkingMode.MODE_NON_ROOT -> R.string.setup_non_root_title + else -> R.string.settings_root_none + } + ), + onClick = { + navController.navigateSingleTopTo(SettingsScreen.WorkingMode.route) + } + ) + + SettingNormalItem( + icon = R.drawable.award, + title = stringResource(id = R.string.settings_about), + desc = "${BuildConfig.VERSION_NAME} (${BuildConfig.VERSION_CODE})", + onClick = { + navController.navigateSingleTopTo(SettingsScreen.About.route) + } + ) + } + } +} + +@Composable +private fun TopBar( + scrollBehavior: TopAppBarScrollBehavior +) = TopAppBar( + title = { + TopAppBarTitle(text = stringResource(id = R.string.page_settings)) + }, + scrollBehavior = scrollBehavior +) \ No newline at end of file diff --git a/app/src/main/kotlin/com/dergoogler/mmrl/ui/screens/settings/about/AboutScreen.kt b/app/src/main/kotlin/com/dergoogler/mmrl/ui/screens/settings/about/AboutScreen.kt new file mode 100644 index 00000000..d1f626dc --- /dev/null +++ b/app/src/main/kotlin/com/dergoogler/mmrl/ui/screens/settings/about/AboutScreen.kt @@ -0,0 +1,165 @@ +package com.dergoogler.mmrl.ui.screens.settings.about + +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.WindowInsets +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.FilledTonalButton +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedCard +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBarDefaults +import androidx.compose.material3.TopAppBarScrollBehavior +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.input.nestedscroll.nestedScroll +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 androidx.navigation.NavController +import com.dergoogler.mmrl.BuildConfig +import com.dergoogler.mmrl.R +import com.dergoogler.mmrl.app.Const +import com.dergoogler.mmrl.ui.component.HtmlText +import com.dergoogler.mmrl.ui.component.Logo +import com.dergoogler.mmrl.ui.component.NavigateUpTopBar +import com.dergoogler.mmrl.ui.utils.none +import com.dergoogler.mmrl.utils.extensions.openUrl + +@Composable +fun AboutScreen( + navController: NavController +) { + val context = LocalContext.current + val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior() + + Scaffold( + modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection), + topBar = { + TopBar( + scrollBehavior = scrollBehavior, + navController = navController + ) + }, + contentWindowInsets = WindowInsets.none + ) { innerPadding -> + Column( + modifier = Modifier + .padding(innerPadding) + .fillMaxWidth() + .verticalScroll(rememberScrollState()), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Spacer(modifier = Modifier.height(20.dp)) + Logo( + icon = R.drawable.launcher_outline, + modifier = Modifier.size(65.dp), + contentColor = MaterialTheme.colorScheme.onPrimary, + containerColor = MaterialTheme.colorScheme.primary, + fraction = 0.7f + ) + + Spacer(modifier = Modifier.height(20.dp)) + Text( + text = stringResource(id = R.string.app_name), + style = MaterialTheme.typography.titleLarge + ) + Text( + text = stringResource(id = R.string.about_app_version, + BuildConfig.VERSION_NAME, BuildConfig.VERSION_CODE), + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.padding(top = 5.dp) + ) + + Spacer(modifier = Modifier.height(20.dp)) + FilledTonalButton( + onClick = { context.openUrl(Const.GITHUB_URL) } + ) { + Icon( + painter = painterResource(id = R.drawable.github), + contentDescription = null + ) + Spacer(modifier = Modifier.width(ButtonDefaults.IconSpacing)) + Text(text = stringResource(id = R.string.about_github)) + } + + Row( + modifier = Modifier.padding(top = 8.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(8.dp) + ) { +// FilledTonalButton( +// onClick = { context.openUrl(Const.TRANSLATE_URL) } +// ) { +// Icon( +// painter = painterResource(id = R.drawable.weblate), +// contentDescription = null, +// modifier = Modifier.size(ButtonDefaults.IconSize) +// ) +// Spacer(modifier = Modifier.width(ButtonDefaults.IconSpacing)) +// Text(text = stringResource(id = R.string.about_weblate)) +// } + + FilledTonalButton( + onClick = { context.openUrl(Const.TELEGRAM_URL) } + ) { + Icon( + painter = painterResource(id = R.drawable.telegram), + contentDescription = null + ) + Spacer(modifier = Modifier.width(ButtonDefaults.IconSpacing)) + Text(text = stringResource(id = R.string.about_telegram)) + } + } + + OutlinedCard( + modifier = Modifier.padding(vertical = 30.dp, horizontal = 20.dp), + shape = RoundedCornerShape(15.dp) + ) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(all = 15.dp) + ) { + Text( + text = stringResource(id = R.string.about_desc1), + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + Spacer(modifier = Modifier.height(15.dp)) + HtmlText( + text = stringResource(id = R.string.about_desc2, + "Sanmer & DerGoogler"), + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + } + } + } +} + +@Composable +private fun TopBar( + scrollBehavior: TopAppBarScrollBehavior, + navController: NavController +) = NavigateUpTopBar( + title = stringResource(id = R.string.settings_about), + scrollBehavior = scrollBehavior, + navController = navController +) \ No newline at end of file diff --git a/app/src/main/kotlin/com/dergoogler/mmrl/ui/screens/settings/app/AppScreen.kt b/app/src/main/kotlin/com/dergoogler/mmrl/ui/screens/settings/app/AppScreen.kt new file mode 100644 index 00000000..eca276bd --- /dev/null +++ b/app/src/main/kotlin/com/dergoogler/mmrl/ui/screens/settings/app/AppScreen.kt @@ -0,0 +1,93 @@ +package com.dergoogler.mmrl.ui.screens.settings.app + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.Scaffold +import androidx.compose.material3.TopAppBarDefaults +import androidx.compose.material3.TopAppBarScrollBehavior +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.input.nestedscroll.nestedScroll +import androidx.compose.ui.res.stringResource +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.navigation.NavController +import com.dergoogler.mmrl.R +import com.dergoogler.mmrl.datastore.UserPreferencesCompat.Companion.isRoot +import com.dergoogler.mmrl.ui.component.NavigateUpTopBar +import com.dergoogler.mmrl.ui.component.SettingSwitchItem +import com.dergoogler.mmrl.ui.providable.LocalUserPreferences +import com.dergoogler.mmrl.ui.screens.settings.app.items.AppThemeItem +import com.dergoogler.mmrl.ui.screens.settings.app.items.DownloadPathItem +import com.dergoogler.mmrl.ui.utils.none +import com.dergoogler.mmrl.viewmodel.SettingsViewModel + +@Composable +fun AppScreen( + navController: NavController, + viewModel: SettingsViewModel = hiltViewModel() +) { + val userPreferences = LocalUserPreferences.current + val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior() + + Scaffold( + modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection), + topBar = { + TopBar( + scrollBehavior = scrollBehavior, + navController = navController + ) + }, + contentWindowInsets = WindowInsets.none + ) { innerPadding -> + Column( + modifier = Modifier + .padding(innerPadding) + .fillMaxSize() + .verticalScroll(rememberScrollState()) + ) { + AppThemeItem( + themeColor = userPreferences.themeColor, + darkMode = userPreferences.darkMode, + isDarkMode = userPreferences.isDarkMode(), + onThemeColorChange = viewModel::setThemeColor, + onDarkModeChange = viewModel::setDarkTheme + ) + + DownloadPathItem( + downloadPath = userPreferences.downloadPath, + onChange = viewModel::setDownloadPath + ) + + SettingSwitchItem( + icon = R.drawable.file_type_zip, + title = stringResource(id = R.string.settings_delete_zip), + desc = stringResource(id = R.string.settings_delete_zip_desc), + checked = userPreferences.deleteZipFile, + onChange = viewModel::setDeleteZipFile, + enabled = userPreferences.workingMode.isRoot + ) + + SettingSwitchItem( + icon = R.drawable.brand_cloudflare, + title = stringResource(id = R.string.settings_doh), + desc = stringResource(id = R.string.settings_doh_desc), + checked = userPreferences.useDoh, + onChange = viewModel::setUseDoh + ) + } + } +} + +@Composable +private fun TopBar( + scrollBehavior: TopAppBarScrollBehavior, + navController: NavController +) = NavigateUpTopBar( + title = stringResource(id = R.string.settings_app), + scrollBehavior = scrollBehavior, + navController = navController +) \ No newline at end of file diff --git a/app/src/main/kotlin/com/dergoogler/mmrl/ui/screens/settings/app/items/AppThemeItem.kt b/app/src/main/kotlin/com/dergoogler/mmrl/ui/screens/settings/app/items/AppThemeItem.kt new file mode 100644 index 00000000..15c552cc --- /dev/null +++ b/app/src/main/kotlin/com/dergoogler/mmrl/ui/screens/settings/app/items/AppThemeItem.kt @@ -0,0 +1,97 @@ +package com.dergoogler.mmrl.ui.screens.settings.app.items + +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.BottomSheetDefaults +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.ModalBottomSheet +import androidx.compose.material3.Text +import androidx.compose.material3.rememberModalBottomSheetState +import androidx.compose.runtime.Composable +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.res.stringResource +import androidx.compose.ui.unit.dp +import com.dergoogler.mmrl.R +import com.dergoogler.mmrl.datastore.DarkMode +import com.dergoogler.mmrl.ui.component.NavigationBarsSpacer +import com.dergoogler.mmrl.ui.component.SettingNormalItem +import com.dergoogler.mmrl.ui.utils.expandedShape + +@Composable +fun AppThemeItem( + themeColor: Int, + darkMode: DarkMode, + isDarkMode: Boolean, + onThemeColorChange: (Int) -> Unit, + onDarkModeChange: (DarkMode) -> Unit +) { + var open by rememberSaveable { mutableStateOf(false) } + + SettingNormalItem( + icon = R.drawable.color_swatch, + title = stringResource(id = R.string.settings_app_theme), + desc = stringResource(id = R.string.settings_app_theme_desc), + onClick = { open = true } + ) + + if (open) { + BottomSheet( + onClose = { open = false }, + themeColor = themeColor, + darkMode = darkMode, + isDarkMode = isDarkMode, + onThemeColorChange =onThemeColorChange, + onDarkModeChange = onDarkModeChange + ) + } +} + +@Composable +private fun BottomSheet( + onClose: () -> Unit, + themeColor: Int, + darkMode: DarkMode, + isDarkMode: Boolean, + onThemeColorChange: (Int) -> Unit, + onDarkModeChange: (DarkMode) -> Unit, +) = ModalBottomSheet( + onDismissRequest = onClose, + sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true), + shape = BottomSheetDefaults.expandedShape(15.dp), + windowInsets = WindowInsets(0) +) { + Text( + text = stringResource(id = R.string.settings_app_theme), + style = MaterialTheme.typography.headlineSmall, + modifier = Modifier.align(Alignment.CenterHorizontally) + ) + + TitleItem(text = stringResource(id = R.string.app_theme_palette)) + ThemePaletteItem( + themeColor = themeColor, + isDarkMode = isDarkMode, + onChange = onThemeColorChange + ) + + TitleItem(text = stringResource(id = R.string.app_theme_dark_theme)) + DarkModeItem( + darkMode = darkMode, + onChange = onDarkModeChange + ) + + NavigationBarsSpacer() +} + +@Composable +private fun TitleItem( + text: String +) = Text( + text = text, + style = MaterialTheme.typography.titleSmall, + modifier = Modifier.padding(start = 18.dp, top = 18.dp) +) \ No newline at end of file diff --git a/app/src/main/kotlin/com/dergoogler/mmrl/ui/screens/settings/app/items/DarkModeItem.kt b/app/src/main/kotlin/com/dergoogler/mmrl/ui/screens/settings/app/items/DarkModeItem.kt new file mode 100644 index 00000000..d56fcefa --- /dev/null +++ b/app/src/main/kotlin/com/dergoogler/mmrl/ui/screens/settings/app/items/DarkModeItem.kt @@ -0,0 +1,147 @@ +package com.dergoogler.mmrl.ui.screens.settings.app.items + +import androidx.compose.animation.core.FastOutSlowInEasing +import androidx.compose.animation.core.animateFloatAsState +import androidx.compose.animation.core.tween +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.lazy.LazyRow +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Icon +import androidx.compose.material3.LocalContentColor +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.material3.surfaceColorAtElevation +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +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.graphicsLayer +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import com.dergoogler.mmrl.R +import com.dergoogler.mmrl.datastore.DarkMode + +private enum class DarkModeItem( + val value: DarkMode, + val icon: Int, + val text: Int +) { + Auto( + value = DarkMode.FOLLOW_SYSTEM, + icon = R.drawable.brightness_2, + text = R.string.app_theme_dark_theme_auto + ), + + Light( + value = DarkMode.ALWAYS_OFF, + icon = R.drawable.sun, + text = R.string.app_theme_dark_theme_light + ), + + Dark( + value = DarkMode.ALWAYS_ON, + icon = R.drawable.moon_stars, + text = R.string.app_theme_dark_theme_dark + ) +} + +private val modes = listOf( + DarkModeItem.Auto, + DarkModeItem.Light, + DarkModeItem.Dark +) + +@Composable +fun DarkModeItem( + darkMode: DarkMode, + onChange: (DarkMode) -> Unit +) = LazyRow( + contentPadding = PaddingValues(horizontal = 18.dp, vertical = 16.dp), + horizontalArrangement = Arrangement.spacedBy(15.dp) +) { + items( + items = modes, + key = { it.value } + ) { + DarkModeItem( + item = it, + darkMode = darkMode + ) { value -> + onChange(value) + } + } +} + +@Composable +private fun DarkModeItem( + item: DarkModeItem, + darkMode: DarkMode, + onClick: (DarkMode) -> Unit +) { + val selected = item.value == darkMode + + Box( + modifier = Modifier + .clip(RoundedCornerShape(15.dp)) + .clickable( + onClick = { onClick(item.value) }, + indication = null, + interactionSource = remember { MutableInteractionSource() } + ) + .background(color = MaterialTheme.colorScheme.surfaceColorAtElevation(6.dp)), + contentAlignment = Alignment.Center + ){ + Row( + modifier = Modifier.padding(all = 12.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(8.dp) + ) { + val animateZ by animateFloatAsState( + targetValue = if (selected) 0f else 360f, + animationSpec = tween( + durationMillis = 350, + easing = FastOutSlowInEasing + ), + label = "animateZ" + ) + + Icon( + modifier = Modifier + .size(20.dp) + .graphicsLayer { + rotationZ = if (selected) animateZ else 0f + }, + painter = painterResource(id = item.icon), + contentDescription = null, + tint = if (selected) { + MaterialTheme.colorScheme.primary + } else { + LocalContentColor.current + } + ) + + Text( + text = stringResource(id = item.text), + style = MaterialTheme.typography.labelLarge, + color = if (selected) { + MaterialTheme.colorScheme.primary + } else { + Color.Unspecified + } + ) + } + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/com/dergoogler/mmrl/ui/screens/settings/app/items/DownloadPathItem.kt b/app/src/main/kotlin/com/dergoogler/mmrl/ui/screens/settings/app/items/DownloadPathItem.kt new file mode 100644 index 00000000..5384cdb8 --- /dev/null +++ b/app/src/main/kotlin/com/dergoogler/mmrl/ui/screens/settings/app/items/DownloadPathItem.kt @@ -0,0 +1,84 @@ +package com.dergoogler.mmrl.ui.screens.settings.app.items + +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +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.res.stringResource +import androidx.compose.ui.unit.dp +import com.dergoogler.mmrl.R +import com.dergoogler.mmrl.app.Const +import com.dergoogler.mmrl.ui.component.SettingNormalItem +import com.dergoogler.mmrl.ui.component.TextFieldDialog +import java.io.File + +@Composable +fun DownloadPathItem( + downloadPath: File, + onChange: (File) -> Unit +) { + var edit by remember { mutableStateOf(false) } + if (edit) OpenDocumentTreeDialog( + path = downloadPath, + onClose = { edit = false }, + onConfirm = { if (it != downloadPath) onChange(it) } + ) + + SettingNormalItem( + icon = R.drawable.files, + title = stringResource(id = R.string.settings_download_path), + desc = downloadPath.absolutePath, + onClick = { edit = true } + ) +} + +@Composable +private fun OpenDocumentTreeDialog( + path : File, + onClose: () -> Unit, + onConfirm: (File) -> Unit +) { + var name by remember { + mutableStateOf(path.toRelativeString(Const.PUBLIC_DOWNLOADS)) + } + + TextFieldDialog( + shape = RoundedCornerShape(20.dp), + onDismissRequest = onClose, + title = { Text(text = stringResource(id = R.string.settings_download_path)) }, + confirmButton = { + TextButton( + onClick = { + val new = Const.PUBLIC_DOWNLOADS.resolve(name) + onConfirm(new) + onClose() + }, + ) { + Text(text = stringResource(id = R.string.dialog_ok)) + } + }, + dismissButton = { + TextButton( + onClick = onClose + ) { + Text(text = stringResource(id = R.string.dialog_cancel)) + } + }, + launchKeyboard = false + ) { + OutlinedTextField( + textStyle = MaterialTheme.typography.bodyLarge, + value = name, + onValueChange = { name = it }, + shape = RoundedCornerShape(15.dp), + label = { Text(text = Const.PUBLIC_DOWNLOADS.absolutePath) }, + singleLine = true + ) + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/com/dergoogler/mmrl/ui/screens/settings/app/items/ThemePaletteItem.kt b/app/src/main/kotlin/com/dergoogler/mmrl/ui/screens/settings/app/items/ThemePaletteItem.kt new file mode 100644 index 00000000..73c639c0 --- /dev/null +++ b/app/src/main/kotlin/com/dergoogler/mmrl/ui/screens/settings/app/items/ThemePaletteItem.kt @@ -0,0 +1,132 @@ +package com.dergoogler.mmrl.ui.screens.settings.app.items + +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.FlowRow +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxHeight +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.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Icon +import androidx.compose.material3.surfaceColorAtElevation +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.unit.dp +import com.dergoogler.mmrl.R +import dev.dergoogler.mmrl.compat.BuildCompat +import com.dergoogler.mmrl.ui.theme.Colors + +@Composable +fun ThemePaletteItem( + themeColor: Int, + isDarkMode: Boolean, + onChange: (Int) -> Unit +) { + FlowRow( + modifier = Modifier.padding(horizontal = 18.dp, vertical = 16.dp), + verticalArrangement = Arrangement.spacedBy(8.dp), + horizontalArrangement = Arrangement.spacedBy(8.dp) + ) { + if (BuildCompat.atLeastS) { + ThemeColorItem( + id = Colors.Dynamic.id, + themeColor = themeColor, + isDarkMode = isDarkMode + ) { + onChange(it) + } + } + + Colors.getColorIds().forEach { + ThemeColorItem( + id = it, + themeColor = themeColor, + isDarkMode = isDarkMode + ) { value -> + onChange(value) + } + } + } +} + +@Composable +private fun ThemeColorItem( + id: Int, + themeColor: Int, + isDarkMode: Boolean, + onClick: (Int) -> Unit +) { + val color = Colors.getColor(id) + val colorScheme = if (isDarkMode) color.darkColorScheme else color.lightColorScheme + val selected = id == themeColor + + Box( + modifier = Modifier + .clip(RoundedCornerShape(15.dp)) + .clickable( + onClick = { onClick(id) }, + indication = null, + interactionSource = remember { MutableInteractionSource() } + ) + .background( + color = colorScheme.surfaceColorAtElevation(3.dp) + ) + .size(60.dp), + contentAlignment = Alignment.Center + ) { + Box( + modifier = Modifier + .clip(CircleShape) + .fillMaxSize(0.75f), + contentAlignment = Alignment.Center + ) { + Column( + modifier = Modifier.fillMaxSize() + ) { + Spacer(modifier = Modifier + .fillMaxWidth() + .fillMaxHeight(0.5f) + .background(color = colorScheme.primaryContainer) + ) + + Spacer(modifier = Modifier + .fillMaxSize() + .background(color = colorScheme.tertiaryContainer) + ) + } + + Spacer(modifier = Modifier + .fillMaxSize(0.5f) + .clip(CircleShape) + .background( + color = if (selected) { + colorScheme.onPrimary + } else { + colorScheme.primary + } + ) + ) + + if (selected) { + Icon( + modifier = Modifier.size(36.dp), + painter = painterResource(id = R.drawable.circle_check_filled), + contentDescription = null, + tint = colorScheme.primary + ) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/com/dergoogler/mmrl/ui/screens/settings/items/NonRootItem.kt b/app/src/main/kotlin/com/dergoogler/mmrl/ui/screens/settings/items/NonRootItem.kt new file mode 100644 index 00000000..3970a42b --- /dev/null +++ b/app/src/main/kotlin/com/dergoogler/mmrl/ui/screens/settings/items/NonRootItem.kt @@ -0,0 +1,61 @@ +package com.dergoogler.mmrl.ui.screens.settings.items + +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.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +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.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import com.dergoogler.mmrl.R + +@Composable +fun NonRootItem() = Surface( + modifier = Modifier.padding(all = 18.dp), + shape = RoundedCornerShape(15.dp), + color = MaterialTheme.colorScheme.secondaryContainer +) { + Row( + modifier = Modifier + .padding(all = 20.dp) + .fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically + ) { + Icon( + modifier = Modifier.size(30.dp), + painter = painterResource(id = R.drawable.info_circle_filled), + contentDescription = null, + tint = MaterialTheme.colorScheme.primary + ) + + Spacer(modifier = Modifier.width(16.dp)) + Column( + modifier = Modifier.fillMaxWidth(), + verticalArrangement = Arrangement.spacedBy(4.dp) + ) { + Text( + text = stringResource(id = R.string.settings_non_root), + style = MaterialTheme.typography.titleMedium, + color = MaterialTheme.colorScheme.onSecondaryContainer + ) + + Text( + text = stringResource(id = R.string.settings_non_root_desc), + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSecondaryContainer + ) + } + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/com/dergoogler/mmrl/ui/screens/settings/items/RootItem.kt b/app/src/main/kotlin/com/dergoogler/mmrl/ui/screens/settings/items/RootItem.kt new file mode 100644 index 00000000..49877ab6 --- /dev/null +++ b/app/src/main/kotlin/com/dergoogler/mmrl/ui/screens/settings/items/RootItem.kt @@ -0,0 +1,79 @@ +package com.dergoogler.mmrl.ui.screens.settings.items + +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.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +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.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import com.dergoogler.mmrl.R + +@Composable +fun RootItem( + isAlive: Boolean, + version: String +) = Surface( + modifier = Modifier.padding(all = 18.dp), + shape = RoundedCornerShape(15.dp), + color = MaterialTheme.colorScheme.secondaryContainer +) { + Row( + modifier = Modifier + .padding(all = 20.dp) + .fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically + ) { + Icon( + modifier = Modifier.size(30.dp), + painter = painterResource(id = if (isAlive) { + R.drawable.circle_check_filled + } else { + R.drawable.alert_circle_filled + }), + contentDescription = null, + tint = MaterialTheme.colorScheme.primary + ) + + Spacer(modifier = Modifier.width(16.dp)) + Column( + modifier = Modifier.fillMaxWidth(), + verticalArrangement = Arrangement.spacedBy(4.dp) + ) { + Text( + text = if (isAlive) { + stringResource(id = R.string.settings_root_access, + stringResource(id = R.string.settings_root_granted)) + } else { + stringResource(id = R.string.settings_root_access, + stringResource(id = R.string.settings_root_none)) + }, + style = MaterialTheme.typography.titleMedium, + color = MaterialTheme.colorScheme.onSecondaryContainer + ) + + Text( + text = if (isAlive) { + stringResource(id = R.string.settings_root_provider, version) + } else { + stringResource(id = R.string.settings_root_provider, + stringResource(id = R.string.settings_root_not_available)) + }, + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSecondaryContainer + ) + } + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/com/dergoogler/mmrl/ui/screens/settings/repositories/RepositoriesList.kt b/app/src/main/kotlin/com/dergoogler/mmrl/ui/screens/settings/repositories/RepositoriesList.kt new file mode 100644 index 00000000..f2c2cb33 --- /dev/null +++ b/app/src/main/kotlin/com/dergoogler/mmrl/ui/screens/settings/repositories/RepositoriesList.kt @@ -0,0 +1,148 @@ +package com.dergoogler.mmrl.ui.screens.settings.repositories + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.requiredHeightIn +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.AlertDialog +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +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.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import com.dergoogler.mmrl.R +import com.dergoogler.mmrl.model.state.RepoState + +@Composable +fun RepositoriesList( + list: List, + state: LazyListState, + update: (RepoState) -> Unit, + delete: (RepoState) -> Unit, + getUpdate: (RepoState, (Throwable) -> Unit) -> Unit +) = LazyColumn( + state = state, + modifier = Modifier.fillMaxSize(), + contentPadding = PaddingValues(10.dp), + verticalArrangement = Arrangement.spacedBy(10.dp), +) { + items( + items = list, + key = { it.url } + ) { repo -> + RepositoryItem( + repo = repo, + toggle = { update(it) }, + onUpdate = getUpdate, + onDelete = delete, + ) + } +} + +@Composable +private fun RepositoryItem( + repo: RepoState, + toggle: (RepoState) -> Unit, + onUpdate: (RepoState, (Throwable) -> Unit) -> Unit, + onDelete: (RepoState) -> Unit, +) { + var delete by remember { mutableStateOf(false) } + if (delete) DeleteDialog( + repo = repo, + onClose = { delete = false }, + onConfirm = { onDelete(repo) } + ) + + var failure by remember { mutableStateOf(false) } + var message: String by remember { mutableStateOf("") } + if (failure) FailureDialog( + name = repo.name, + message = message, + onClose = { + failure = false + message = "" + } + ) + + RepositoryItem( + repo = repo, + toggle = { + toggle(repo.copy(enable = it)) + }, + update = { + onUpdate(repo) { + failure = true + message = it.stackTraceToString() + } + }, + delete = { delete = true } + ) +} + +@Composable +private fun DeleteDialog( + repo: RepoState, + onClose: () -> Unit, + onConfirm: () -> Unit +) = AlertDialog( + shape = RoundedCornerShape(20.dp), + onDismissRequest = onClose, + title = { Text(text = stringResource(id = R.string.dialog_attention)) }, + text = { + Text(text = stringResource(id = R.string.repo_delete_dialog_desc, repo.name)) + }, + confirmButton = { + TextButton( + onClick = { + onConfirm() + onClose() + } + ) { + Text(text = stringResource(id = R.string.repo_options_delete)) + } + }, + dismissButton = { + TextButton( + onClick = onClose + ) { + Text(text = stringResource(id = R.string.dialog_cancel)) + } + } +) + +@Composable +fun FailureDialog( + name: String, + message: String, + onClose: () -> Unit +) = AlertDialog( + shape = RoundedCornerShape(20.dp), + onDismissRequest = onClose, + title = { Text(text = name) }, + text = { + Text( + text = message, + modifier = Modifier + .requiredHeightIn(max = 280.dp) + .verticalScroll(rememberScrollState()) + ) + }, + confirmButton = { + TextButton( + onClick = onClose + ) { + Text(text = stringResource(id = R.string.dialog_ok)) + } + } +) \ No newline at end of file diff --git a/app/src/main/kotlin/com/dergoogler/mmrl/ui/screens/settings/repositories/RepositoriesScreen.kt b/app/src/main/kotlin/com/dergoogler/mmrl/ui/screens/settings/repositories/RepositoriesScreen.kt new file mode 100644 index 00000000..a11cec3e --- /dev/null +++ b/app/src/main/kotlin/com/dergoogler/mmrl/ui/screens/settings/repositories/RepositoriesScreen.kt @@ -0,0 +1,236 @@ +package com.dergoogler.mmrl.ui.screens.settings.repositories + +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.core.tween +import androidx.compose.animation.scaleIn +import androidx.compose.animation.scaleOut +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.text.KeyboardActions +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material3.FloatingActionButton +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.LinearProgressIndicator +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.material3.TopAppBarDefaults +import androidx.compose.material3.TopAppBarScrollBehavior +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.Modifier +import androidx.compose.ui.focus.focusRequester +import androidx.compose.ui.graphics.StrokeCap +import androidx.compose.ui.input.nestedscroll.nestedScroll +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.input.ImeAction +import androidx.compose.ui.text.input.KeyboardType +import androidx.compose.ui.unit.dp +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import androidx.navigation.NavController +import com.dergoogler.mmrl.R +import com.dergoogler.mmrl.ui.animate.slideInTopToBottom +import com.dergoogler.mmrl.ui.animate.slideOutBottomToTop +import com.dergoogler.mmrl.ui.component.Loading +import com.dergoogler.mmrl.ui.component.NavigateUpTopBar +import com.dergoogler.mmrl.ui.component.PageIndicator +import com.dergoogler.mmrl.ui.component.TextFieldDialog +import com.dergoogler.mmrl.ui.utils.isScrollingUp +import com.dergoogler.mmrl.ui.utils.none +import com.dergoogler.mmrl.viewmodel.RepositoriesViewModel + +@Composable +fun RepositoriesScreen( + navController: NavController, + viewModel: RepositoriesViewModel = hiltViewModel() +) { + val list by viewModel.repos.collectAsStateWithLifecycle() + + val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior() + val listSate = rememberLazyListState() + val showFab by listSate.isScrollingUp() + + var repoUrl by remember { mutableStateOf("") } + var message: String by remember { mutableStateOf("") } + + var failure by remember { mutableStateOf(false) } + if (failure) FailureDialog( + name = repoUrl, + message = message, + onClose = { + failure = false + message = "" + } + ) + + var add by remember { mutableStateOf(false) } + if (add) AddDialog( + onClose = { add = false }, + onAdd = { + repoUrl = it + viewModel.insert(it) { e -> + failure = true + message = e.stackTraceToString() + } + } + ) + + Scaffold( + modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection), + topBar = { + TopBar( + scrollBehavior = scrollBehavior, + navController = navController, + onRefresh = viewModel::getRepoAll + ) + }, + floatingActionButton = { + AnimatedVisibility( + visible = showFab, + enter = scaleIn( + animationSpec = tween(100), + initialScale = 0.8f + ), + exit = scaleOut( + animationSpec = tween(100), + targetScale = 0.8f + ) + ) { + FloatingButton { add = true } + } + }, + contentWindowInsets = WindowInsets.none + ) { innerPadding -> + Box( + modifier = Modifier.padding(innerPadding) + ) { + if (viewModel.isLoading) { + Loading() + } + + if (list.isEmpty() && !viewModel.isLoading) { + PageIndicator( + icon = R.drawable.git_pull_request, + text = R.string.repo_empty + ) + } + + RepositoriesList( + list = list, + state = listSate, + update = viewModel::update, + delete = viewModel::delete, + getUpdate = viewModel::getUpdate + ) + + AnimatedVisibility( + visible = viewModel.progress, + enter = slideInTopToBottom(), + exit = slideOutBottomToTop() + ) { + LinearProgressIndicator( + modifier = Modifier.fillMaxWidth(), + strokeCap = StrokeCap.Round + ) + } + } + } +} + +@Composable +private fun AddDialog( + onClose: () -> Unit, + onAdd: (String) -> Unit +) { + var domain by remember { mutableStateOf("") } + + val onDone: () -> Unit = { + onAdd("https://${domain}/") + onClose() + } + + TextFieldDialog( + shape = RoundedCornerShape(20.dp), + onDismissRequest = onClose, + title = { Text(text = stringResource(id = R.string.repo_add_dialog_title)) }, + confirmButton = { + TextButton( + onClick = onDone, + enabled = domain.isNotBlank() + ) { + Text(text = stringResource(id = R.string.repo_add_dialog_add)) + } + }, + dismissButton = { + TextButton( + onClick = onClose + ) { + Text(text = stringResource(id = R.string.dialog_cancel)) + } + } + ) { focusRequester -> + OutlinedTextField( + modifier = Modifier.focusRequester(focusRequester), + textStyle = MaterialTheme.typography.bodyLarge, + value = domain, + onValueChange = { domain = it }, + prefix = { Text(text = "https://") }, + singleLine = false, + keyboardOptions = KeyboardOptions( + keyboardType = KeyboardType.Text, + imeAction = ImeAction.Done + ), + keyboardActions = KeyboardActions { + if (domain.isNotBlank()) onDone() + }, + shape = RoundedCornerShape(15.dp) + ) + } +} + +@Composable +private fun TopBar( + scrollBehavior: TopAppBarScrollBehavior, + navController: NavController, + onRefresh: () -> Unit +) = NavigateUpTopBar( + title = stringResource(id = R.string.settings_repo), + actions = { + IconButton( + onClick = onRefresh + ) { + Icon( + painter = painterResource(id = R.drawable.refresh), + contentDescription = null + ) + } + }, + scrollBehavior = scrollBehavior, + navController = navController +) + +@Composable +private fun FloatingButton( + onClick: () -> Unit +) = FloatingActionButton( + onClick = onClick, + contentColor = MaterialTheme.colorScheme.onPrimary, + containerColor = MaterialTheme.colorScheme.primary +) { + Icon( + painter = painterResource(id = R.drawable.pencil_plus), + contentDescription = null + ) +} \ No newline at end of file diff --git a/app/src/main/kotlin/com/dergoogler/mmrl/ui/screens/settings/repositories/RepositoryItem.kt b/app/src/main/kotlin/com/dergoogler/mmrl/ui/screens/settings/repositories/RepositoryItem.kt new file mode 100644 index 00000000..9df8e75e --- /dev/null +++ b/app/src/main/kotlin/com/dergoogler/mmrl/ui/screens/settings/repositories/RepositoryItem.kt @@ -0,0 +1,344 @@ +package com.dergoogler.mmrl.ui.screens.settings.repositories + +import androidx.annotation.DrawableRes +import androidx.annotation.StringRes +import androidx.compose.animation.Crossfade +import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.ripple.rememberRipple +import androidx.compose.material3.BottomSheetDefaults +import androidx.compose.material3.FilledTonalButton +import androidx.compose.material3.FilledTonalIconButton +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.ModalBottomSheet +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.material3.rememberModalBottomSheetState +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.alpha +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextDecoration +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import com.dergoogler.mmrl.R +import com.dergoogler.mmrl.model.state.RepoState +import com.dergoogler.mmrl.ui.component.LabelItem +import com.dergoogler.mmrl.ui.component.NavigationBarsSpacer +import com.dergoogler.mmrl.ui.utils.expandedShape +import com.dergoogler.mmrl.utils.extensions.openUrl +import com.dergoogler.mmrl.utils.extensions.shareText +import com.dergoogler.mmrl.utils.extensions.toDateTime +import com.dergoogler.mmrl.utils.extensions.toFormattedDate + +@Composable +fun RepositoryItem( + repo: RepoState, + toggle: (Boolean) -> Unit, + update: () -> Unit, + delete: () -> Unit, +) = Surface( + shape = RoundedCornerShape(15.dp), + color = MaterialTheme.colorScheme.surface, + tonalElevation = 1.dp, + onClick = { toggle(!repo.enable) }, +) { + val context = LocalContext.current + val (alpha, textDecoration) = when { + !repo.compatible -> 0.5f to TextDecoration.LineThrough + !repo.enable -> 0.5f to TextDecoration.None + else -> 1f to TextDecoration.None + } + + Column( + modifier = Modifier + .padding(all = 15.dp) + .fillMaxWidth() + ) { + Row( + verticalAlignment = Alignment.Top + ) { + Crossfade( + targetState = repo.enable, + label = "RepositoryItem" + ) { + if (it) { + Icon( + modifier = Modifier.size(32.dp), + painter = painterResource(id = R.drawable.circle_check_filled), + contentDescription = null, + tint = MaterialTheme.colorScheme.secondary + ) + } else { + Icon( + modifier = Modifier.size(32.dp), + painter = painterResource(id = R.drawable.circle_x_filled), + contentDescription = null, + tint = MaterialTheme.colorScheme.secondary + ) + } + } + + Spacer(modifier = Modifier.width(12.dp)) + Column( + modifier = Modifier + .weight(1f) + .alpha(alpha), + verticalArrangement = Arrangement.spacedBy(2.dp) + ) { + Text( + text = repo.name, + style = MaterialTheme.typography.titleSmall + .copy(fontWeight = FontWeight.Bold), + maxLines = 2, + overflow = TextOverflow.Ellipsis, + textDecoration = textDecoration + ) + + Text( + text = stringResource( + id = R.string.module_update_at, repo.timestamp.toFormattedDate() + ), + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.outline, + textDecoration = textDecoration + ) + } + + if (repo.compatible) { + LabelItem( + text = stringResource(id = R.string.repo_modules, repo.size), + upperCase = false + ) + } else { + LabelItem( + text = stringResource(id = R.string.repo_incompatible), + containerColor = MaterialTheme.colorScheme.error, + contentColor = MaterialTheme.colorScheme.onError + ) + } + } + + Row( + modifier = Modifier.padding(top = 12.dp), + horizontalArrangement = Arrangement.spacedBy(8.dp), + verticalAlignment = Alignment.CenterVertically + ) { + var open by remember { mutableStateOf(false) } + if (open) { + BottomSheet( + repo = repo, + onDelete = delete, + onClose = { open = false } + ) + } + + ButtonItem( + icon = R.drawable.share, + onClick = { context.shareText(repo.url) } + ) + + Spacer(Modifier.weight(1f)) + + ButtonItem( + icon = R.drawable.at, + onClick = { open = true } + ) + + ButtonItem( + icon = R.drawable.cloud_download, + label = R.string.repo_options_update, + onClick = update + ) + } + } +} + +@Composable +private fun BottomSheet( + repo: RepoState, onDelete: () -> Unit, onClose: () -> Unit +) = ModalBottomSheet( + onDismissRequest = onClose, + sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true), + shape = BottomSheetDefaults.expandedShape(15.dp), + windowInsets = WindowInsets(0) +) { + val context = LocalContext.current + + Column( + modifier = Modifier + .padding(bottom = 18.dp) + .padding(horizontal = 18.dp), + verticalArrangement = Arrangement.spacedBy(18.dp) + ) { + Row( + verticalAlignment = Alignment.Top + ) { + Column( + modifier = Modifier.weight(1f), + verticalArrangement = Arrangement.spacedBy(2.dp) + ) { + Text( + text = repo.name, + style = MaterialTheme.typography.titleSmall + .copy(fontWeight = FontWeight.Bold), + maxLines = 2, + overflow = TextOverflow.Ellipsis + ) + + Text( + text = stringResource( + id = R.string.module_update_at, repo.timestamp.toFormattedDate() + ), + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.outline + ) + } + + LabelItem( + text = stringResource(id = R.string.repo_modules, repo.size), + upperCase = false + ) + } + + OutlinedTextField( + modifier = Modifier.fillMaxWidth(), + textStyle = MaterialTheme.typography.bodyMedium, + shape = RoundedCornerShape(15.dp), + value = repo.url, + onValueChange = {}, + readOnly = true, + singleLine = true + ) + } + + Column( + modifier = Modifier + .padding(bottom = 18.dp), + ) { + repo.support?.let { + ValueItem( + icon = R.drawable.brand_git, + label = R.string.repo_options_support, + onClick = { context.openUrl(it) }) + } + + repo.donate?.let { + ValueItem( + icon = R.drawable.heart_handshake, + label = R.string.repo_options_donate, + onClick = { context.openUrl(it) } + ) + } + + repo.website?.let { + ValueItem( + icon = R.drawable.world_www, + label = R.string.repo_options_website, + onClick = { context.openUrl(it) } + ) + } + + repo.submission?.let { + ValueItem( + icon = R.drawable.cloud_upload, + label = R.string.repo_options_submission, + onClick = { context.openUrl(it) } + ) + } + + ValueItem( + icon = R.drawable.trash, + label = R.string.repo_options_delete, + onClick = onDelete + ) + } + + NavigationBarsSpacer() +} + +@Composable +private fun ValueItem( + modifier: Modifier = Modifier, + @DrawableRes icon: Int? = null, + @StringRes label: Int, + onClick: () -> Unit +) = Row( + modifier = modifier + .clickable( + interactionSource = remember { MutableInteractionSource() }, + indication = rememberRipple(bounded = true), + onClick = onClick + ) + .height(56.dp) + .fillMaxWidth() + .padding(horizontal = 16.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(4.dp) +) { + val titleStyle = MaterialTheme.typography.titleMedium + val iconSize = + with(LocalDensity.current) { titleStyle.fontSize.toDp() * 1.0f } + + icon?.let { + Icon( + modifier = Modifier.size(iconSize * 1.5f), + painter = painterResource(id = icon), + contentDescription = null + ) + + Spacer(modifier = Modifier.width(6.dp)) + } + Text( + text = stringResource(id = label), + style = titleStyle, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) +} + + +@Composable +private fun ButtonItem( + @DrawableRes icon: Int, + @StringRes label: Int? = null, + onClick: () -> Unit +) = FilledTonalButton( + onClick = onClick, + contentPadding = PaddingValues(horizontal = 12.dp) +) { + Icon( + modifier = Modifier.size(20.dp), + painter = painterResource(id = icon), + contentDescription = null + ) + + label?.let { + Spacer(modifier = Modifier.width(6.dp)) + Text( + text = stringResource(id = label) + ) + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/com/dergoogler/mmrl/ui/screens/settings/workingmode/WorkingModeItem.kt b/app/src/main/kotlin/com/dergoogler/mmrl/ui/screens/settings/workingmode/WorkingModeItem.kt new file mode 100644 index 00000000..b533dfbd --- /dev/null +++ b/app/src/main/kotlin/com/dergoogler/mmrl/ui/screens/settings/workingmode/WorkingModeItem.kt @@ -0,0 +1,79 @@ +package com.dergoogler.mmrl.ui.screens.settings.workingmode + +import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.requiredHeightIn +import androidx.compose.foundation.layout.requiredWidth +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +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.painterResource +import androidx.compose.ui.unit.dp +import com.dergoogler.mmrl.R + +@Composable +fun WorkingModeItem( + title: String, + desc: String, + modifier: Modifier = Modifier, + selected: Boolean = false, + onClick: () -> Unit +) = Box( + modifier = Modifier + .requiredWidth(240.dp) + .then(modifier) +) { + Surface( + onClick = onClick, + tonalElevation = if (selected) 4.dp else 0.dp, + border = BorderStroke(1.dp, color = MaterialTheme.colorScheme.outline), + shape = RoundedCornerShape(15.dp) + ) { + Column( + modifier = Modifier + .padding(all = 16.dp) + .requiredHeightIn(min = 120.dp) + .fillMaxWidth() + ) { + Text( + text = title, + style = MaterialTheme.typography.titleMedium, + color = MaterialTheme.colorScheme.primary + ) + + Spacer(modifier = Modifier.height(10.dp)) + + Text( + text = desc, + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + } + + if (selected) { + Box( + modifier = Modifier + .padding(top = 8.dp, end = 8.dp) + .align(Alignment.TopEnd) + ) { + Icon( + painter = painterResource(id = R.drawable.circle_check_filled), + contentDescription = null, + tint = MaterialTheme.colorScheme.primary, + modifier = Modifier.size(30.dp) + ) + } + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/com/dergoogler/mmrl/ui/screens/settings/workingmode/WorkingModeScreen.kt b/app/src/main/kotlin/com/dergoogler/mmrl/ui/screens/settings/workingmode/WorkingModeScreen.kt new file mode 100644 index 00000000..4467c6f1 --- /dev/null +++ b/app/src/main/kotlin/com/dergoogler/mmrl/ui/screens/settings/workingmode/WorkingModeScreen.kt @@ -0,0 +1,90 @@ +package com.dergoogler.mmrl.ui.screens.settings.workingmode + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.Scaffold +import androidx.compose.material3.TopAppBarDefaults +import androidx.compose.material3.TopAppBarScrollBehavior +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.input.nestedscroll.nestedScroll +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.navigation.NavController +import com.dergoogler.mmrl.R +import com.dergoogler.mmrl.datastore.WorkingMode +import com.dergoogler.mmrl.ui.component.NavigateUpTopBar +import com.dergoogler.mmrl.ui.providable.LocalUserPreferences +import com.dergoogler.mmrl.ui.utils.none +import com.dergoogler.mmrl.viewmodel.SettingsViewModel + +@Composable +fun WorkingModeScreen( + navController: NavController, + viewModel: SettingsViewModel = hiltViewModel() +) { + val userPreferences = LocalUserPreferences.current + val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior() + + Scaffold( + modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection), + topBar = { + TopBar( + scrollBehavior = scrollBehavior, + navController = navController + ) + }, + contentWindowInsets = WindowInsets.none + ) { innerPadding -> + Column( + modifier = Modifier + .padding(innerPadding) + .fillMaxSize() + .verticalScroll(rememberScrollState()), + verticalArrangement = Arrangement.spacedBy(20.dp), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Spacer(modifier = Modifier.height(20.dp)) + + WorkingModeItem( + title = stringResource(id = R.string.setup_root_title), + desc = stringResource(id = R.string.setup_root_desc), + selected = userPreferences.workingMode == WorkingMode.MODE_ROOT, + onClick = { viewModel.setWorkingMode(WorkingMode.MODE_ROOT) } + ) + + WorkingModeItem( + title = stringResource(id = R.string.setup_shizuku_title), + desc = stringResource(id = R.string.setup_shizuku_desc), + selected = userPreferences.workingMode == WorkingMode.MODE_SHIZUKU, + onClick = { viewModel.setWorkingMode(WorkingMode.MODE_SHIZUKU) } + ) + + WorkingModeItem( + title = stringResource(id = R.string.setup_non_root_title), + desc = stringResource(id = R.string.setup_non_root_desc), + selected = userPreferences.workingMode == WorkingMode.MODE_NON_ROOT, + onClick = { viewModel.setWorkingMode(WorkingMode.MODE_NON_ROOT) } + ) + } + } +} + +@Composable +private fun TopBar( + scrollBehavior: TopAppBarScrollBehavior, + navController: NavController +) = NavigateUpTopBar( + title = stringResource(id = R.string.setup_mode), + scrollBehavior = scrollBehavior, + navController = navController +) \ No newline at end of file diff --git a/app/src/main/kotlin/com/dergoogler/mmrl/ui/theme/Colors.kt b/app/src/main/kotlin/com/dergoogler/mmrl/ui/theme/Colors.kt new file mode 100644 index 00000000..0d5a559f --- /dev/null +++ b/app/src/main/kotlin/com/dergoogler/mmrl/ui/theme/Colors.kt @@ -0,0 +1,105 @@ +package com.dergoogler.mmrl.ui.theme + +import android.content.Context +import android.os.Build +import androidx.annotation.RequiresApi +import androidx.compose.material3.ColorScheme +import androidx.compose.material3.dynamicDarkColorScheme +import androidx.compose.material3.dynamicLightColorScheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.platform.LocalContext +import dev.dergoogler.mmrl.compat.BuildCompat +import com.dergoogler.mmrl.ui.theme.color.AlmondBlossomDarkScheme +import com.dergoogler.mmrl.ui.theme.color.AlmondBlossomLightScheme +import com.dergoogler.mmrl.ui.theme.color.JeufosseDarkScheme +import com.dergoogler.mmrl.ui.theme.color.JeufosseLightScheme +import com.dergoogler.mmrl.ui.theme.color.PlainAuversDarkScheme +import com.dergoogler.mmrl.ui.theme.color.PlainAuversLightScheme +import com.dergoogler.mmrl.ui.theme.color.PoppyFieldDarkScheme +import com.dergoogler.mmrl.ui.theme.color.PoppyFieldLightScheme +import com.dergoogler.mmrl.ui.theme.color.PourvilleDarkScheme +import com.dergoogler.mmrl.ui.theme.color.PourvilleLightScheme +import com.dergoogler.mmrl.ui.theme.color.SoleilLevantDarkScheme +import com.dergoogler.mmrl.ui.theme.color.SoleilLevantLightScheme +import com.dergoogler.mmrl.ui.theme.color.WildRosesDarkScheme +import com.dergoogler.mmrl.ui.theme.color.WildRosesLightScheme + +sealed class Colors( + val id: Int, + val lightColorScheme: ColorScheme, + val darkColorScheme: ColorScheme +) { + @RequiresApi(Build.VERSION_CODES.S) + class Dynamic(context: Context) : Colors( + id = id, + lightColorScheme = dynamicLightColorScheme(context), + darkColorScheme = dynamicDarkColorScheme(context) + ) { + companion object { + @Suppress("ConstPropertyName") + const val id = -1 + } + } + data object Pourville : Colors( + id = 0, + lightColorScheme = PourvilleLightScheme, + darkColorScheme = PourvilleDarkScheme + ) + data object SoleilLevant : Colors( + id = 1, + lightColorScheme = SoleilLevantLightScheme, + darkColorScheme = SoleilLevantDarkScheme + ) + data object Jeufosse: Colors( + id = 2, + lightColorScheme = JeufosseLightScheme, + darkColorScheme = JeufosseDarkScheme + ) + data object PoppyField: Colors( + id = 3, + lightColorScheme = PoppyFieldLightScheme, + darkColorScheme = PoppyFieldDarkScheme + ) + data object AlmondBlossom: Colors( + id = 4, + lightColorScheme = AlmondBlossomLightScheme, + darkColorScheme = AlmondBlossomDarkScheme + ) + data object PlainAuvers: Colors( + id = 5, + lightColorScheme = PlainAuversLightScheme, + darkColorScheme = PlainAuversDarkScheme + ) + data object WildRoses: Colors( + id = 6, + lightColorScheme = WildRosesLightScheme, + darkColorScheme = WildRosesDarkScheme + ) + + companion object { + private val mColors get() = listOf( + Pourville, + SoleilLevant, + Jeufosse, + PoppyField, + AlmondBlossom, + PlainAuvers, + WildRoses + ) + + fun getColorIds(): List { + return mColors.map { it.id } + } + + @Composable + fun getColor(id: Int): Colors { + val context = LocalContext.current + + return if (BuildCompat.atLeastS && id == Dynamic.id) { + Dynamic(context) + } else { + mColors[id] + } + } + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/com/dergoogler/mmrl/ui/theme/Shape.kt b/app/src/main/kotlin/com/dergoogler/mmrl/ui/theme/Shape.kt new file mode 100644 index 00000000..856ffdb6 --- /dev/null +++ b/app/src/main/kotlin/com/dergoogler/mmrl/ui/theme/Shape.kt @@ -0,0 +1,5 @@ +package com.dergoogler.mmrl.ui.theme + +import androidx.compose.material3.Shapes + +val Shapes = Shapes() \ No newline at end of file diff --git a/app/src/main/kotlin/com/dergoogler/mmrl/ui/theme/Theme.kt b/app/src/main/kotlin/com/dergoogler/mmrl/ui/theme/Theme.kt new file mode 100644 index 00000000..a6aaf54e --- /dev/null +++ b/app/src/main/kotlin/com/dergoogler/mmrl/ui/theme/Theme.kt @@ -0,0 +1,64 @@ +package com.dergoogler.mmrl.ui.theme + +import androidx.activity.ComponentActivity +import androidx.activity.SystemBarStyle +import androidx.activity.enableEdgeToEdge +import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.runtime.SideEffect +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.toArgb +import androidx.compose.ui.platform.LocalContext + +@Composable +fun AppTheme( + themeColor: Int, + darkMode: Boolean = isSystemInDarkTheme(), + content: @Composable () -> Unit +) { + val color = Colors.getColor(id = themeColor) + val colorScheme = when { + darkMode -> color.darkColorScheme + else -> color.lightColorScheme + } + + SystemBarStyle( + darkMode = darkMode + ) + + MaterialTheme( + colorScheme = colorScheme, + shapes = Shapes, + typography = Typography, + content = content + ) +} + +@Composable +private fun SystemBarStyle( + darkMode: Boolean, + statusBarScrim: Color = Color.Transparent, + navigationBarScrim: Color = Color.Transparent +) { + val context = LocalContext.current + val activity = context as ComponentActivity + + SideEffect { + activity.enableEdgeToEdge( + statusBarStyle = SystemBarStyle.auto( + statusBarScrim.toArgb(), + statusBarScrim.toArgb(), + ) { darkMode }, + navigationBarStyle = when { + darkMode -> SystemBarStyle.dark( + navigationBarScrim.toArgb() + ) + else -> SystemBarStyle.light( + navigationBarScrim.toArgb(), + navigationBarScrim.toArgb(), + ) + } + ) + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/com/dergoogler/mmrl/ui/theme/Type.kt b/app/src/main/kotlin/com/dergoogler/mmrl/ui/theme/Type.kt new file mode 100644 index 00000000..4d5d07d2 --- /dev/null +++ b/app/src/main/kotlin/com/dergoogler/mmrl/ui/theme/Type.kt @@ -0,0 +1,17 @@ +package com.dergoogler.mmrl.ui.theme + +import androidx.compose.material3.Typography +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.sp + +val Typography = Typography( + titleLarge = TextStyle( + fontFamily = FontFamily.SansSerif, + fontWeight = FontWeight.Medium, + fontSize = 20.sp, + lineHeight = 28.sp, + letterSpacing = 0.sp, + ) +) \ No newline at end of file diff --git a/app/src/main/kotlin/com/dergoogler/mmrl/ui/theme/color/AlmondBlossom.kt b/app/src/main/kotlin/com/dergoogler/mmrl/ui/theme/color/AlmondBlossom.kt new file mode 100644 index 00000000..b40ee7e3 --- /dev/null +++ b/app/src/main/kotlin/com/dergoogler/mmrl/ui/theme/color/AlmondBlossom.kt @@ -0,0 +1,157 @@ +/* + * Gogh, Vincent van; Almond Blossom; February 1890 - 1890 + */ + +package com.dergoogler.mmrl.ui.theme.color + +import androidx.compose.material3.darkColorScheme +import androidx.compose.material3.lightColorScheme +import androidx.compose.ui.graphics.Color + +private val primaryLight = Color(0xFF006970) +private val onPrimaryLight = Color(0xFFFFFFFF) +private val primaryContainerLight = Color(0xFF9DF0F8) +private val onPrimaryContainerLight = Color(0xFF002022) +private val secondaryLight = Color(0xFF4A6365) +private val onSecondaryLight = Color(0xFFFFFFFF) +private val secondaryContainerLight = Color(0xFFCCE8EA) +private val onSecondaryContainerLight = Color(0xFF051F21) +private val tertiaryLight = Color(0xFF505E7D) +private val onTertiaryLight = Color(0xFFFFFFFF) +private val tertiaryContainerLight = Color(0xFFD7E2FF) +private val onTertiaryContainerLight = Color(0xFF0A1B36) +private val errorLight = Color(0xFFBA1A1A) +private val onErrorLight = Color(0xFFFFFFFF) +private val errorContainerLight = Color(0xFFFFDAD6) +private val onErrorContainerLight = Color(0xFF410002) +private val backgroundLight = Color(0xFFF4FAFB) +private val onBackgroundLight = Color(0xFF161D1D) +private val surfaceLight = Color(0xFFF4FAFB) +private val onSurfaceLight = Color(0xFF161D1D) +private val surfaceVariantLight = Color(0xFFDAE4E5) +private val onSurfaceVariantLight = Color(0xFF3F4849) +private val outlineLight = Color(0xFF6F797A) +private val outlineVariantLight = Color(0xFFBEC8C9) +private val scrimLight = Color(0xFF000000) +private val inverseSurfaceLight = Color(0xFF2B3232) +private val inverseOnSurfaceLight = Color(0xFFECF2F2) +private val inversePrimaryLight = Color(0xFF80D4DC) +private val surfaceDimLight = Color(0xFFD5DBDC) +private val surfaceBrightLight = Color(0xFFF4FAFB) +private val surfaceContainerLowestLight = Color(0xFFFFFFFF) +private val surfaceContainerLowLight = Color(0xFFEFF5F5) +private val surfaceContainerLight = Color(0xFFE9EFEF) +private val surfaceContainerHighLight = Color(0xFFE3E9EA) +private val surfaceContainerHighestLight = Color(0xFFDEE4E4) + +private val primaryDark = Color(0xFF80D4DC) +private val onPrimaryDark = Color(0xFF00363B) +private val primaryContainerDark = Color(0xFF004F55) +private val onPrimaryContainerDark = Color(0xFF9DF0F8) +private val secondaryDark = Color(0xFFB1CBCE) +private val onSecondaryDark = Color(0xFF1B3437) +private val secondaryContainerDark = Color(0xFF324B4D) +private val onSecondaryContainerDark = Color(0xFFCCE8EA) +private val tertiaryDark = Color(0xFFB7C7EA) +private val onTertiaryDark = Color(0xFF21304C) +private val tertiaryContainerDark = Color(0xFF384764) +private val onTertiaryContainerDark = Color(0xFFD7E2FF) +private val errorDark = Color(0xFFFFB4AB) +private val onErrorDark = Color(0xFF690005) +private val errorContainerDark = Color(0xFF93000A) +private val onErrorContainerDark = Color(0xFFFFDAD6) +private val backgroundDark = Color(0xFF0E1415) +private val onBackgroundDark = Color(0xFFDEE4E4) +private val surfaceDark = Color(0xFF0E1415) +private val onSurfaceDark = Color(0xFFDEE4E4) +private val surfaceVariantDark = Color(0xFF3F4849) +private val onSurfaceVariantDark = Color(0xFFBEC8C9) +private val outlineDark = Color(0xFF899393) +private val outlineVariantDark = Color(0xFF3F4849) +private val scrimDark = Color(0xFF000000) +private val inverseSurfaceDark = Color(0xFFDEE4E4) +private val inverseOnSurfaceDark = Color(0xFF2B3232) +private val inversePrimaryDark = Color(0xFF006970) +private val surfaceDimDark = Color(0xFF0E1415) +private val surfaceBrightDark = Color(0xFF343A3B) +private val surfaceContainerLowestDark = Color(0xFF090F10) +private val surfaceContainerLowDark = Color(0xFF161D1D) +private val surfaceContainerDark = Color(0xFF1A2121) +private val surfaceContainerHighDark = Color(0xFF252B2C) +private val surfaceContainerHighestDark = Color(0xFF303637) + +val AlmondBlossomLightScheme = lightColorScheme( + primary = primaryLight, + onPrimary = onPrimaryLight, + primaryContainer = primaryContainerLight, + onPrimaryContainer = onPrimaryContainerLight, + secondary = secondaryLight, + onSecondary = onSecondaryLight, + secondaryContainer = secondaryContainerLight, + onSecondaryContainer = onSecondaryContainerLight, + tertiary = tertiaryLight, + onTertiary = onTertiaryLight, + tertiaryContainer = tertiaryContainerLight, + onTertiaryContainer = onTertiaryContainerLight, + error = errorLight, + onError = onErrorLight, + errorContainer = errorContainerLight, + onErrorContainer = onErrorContainerLight, + background = backgroundLight, + onBackground = onBackgroundLight, + surface = surfaceLight, + onSurface = onSurfaceLight, + surfaceVariant = surfaceVariantLight, + onSurfaceVariant = onSurfaceVariantLight, + outline = outlineLight, + outlineVariant = outlineVariantLight, + scrim = scrimLight, + inverseSurface = inverseSurfaceLight, + inverseOnSurface = inverseOnSurfaceLight, + inversePrimary = inversePrimaryLight, + surfaceDim = surfaceDimLight, + surfaceBright = surfaceBrightLight, + surfaceContainerLowest = surfaceContainerLowestLight, + surfaceContainerLow = surfaceContainerLowLight, + surfaceContainer = surfaceContainerLight, + surfaceContainerHigh = surfaceContainerHighLight, + surfaceContainerHighest = surfaceContainerHighestLight, +) + +val AlmondBlossomDarkScheme = darkColorScheme( + primary = primaryDark, + onPrimary = onPrimaryDark, + primaryContainer = primaryContainerDark, + onPrimaryContainer = onPrimaryContainerDark, + secondary = secondaryDark, + onSecondary = onSecondaryDark, + secondaryContainer = secondaryContainerDark, + onSecondaryContainer = onSecondaryContainerDark, + tertiary = tertiaryDark, + onTertiary = onTertiaryDark, + tertiaryContainer = tertiaryContainerDark, + onTertiaryContainer = onTertiaryContainerDark, + error = errorDark, + onError = onErrorDark, + errorContainer = errorContainerDark, + onErrorContainer = onErrorContainerDark, + background = backgroundDark, + onBackground = onBackgroundDark, + surface = surfaceDark, + onSurface = onSurfaceDark, + surfaceVariant = surfaceVariantDark, + onSurfaceVariant = onSurfaceVariantDark, + outline = outlineDark, + outlineVariant = outlineVariantDark, + scrim = scrimDark, + inverseSurface = inverseSurfaceDark, + inverseOnSurface = inverseOnSurfaceDark, + inversePrimary = inversePrimaryDark, + surfaceDim = surfaceDimDark, + surfaceBright = surfaceBrightDark, + surfaceContainerLowest = surfaceContainerLowestDark, + surfaceContainerLow = surfaceContainerLowDark, + surfaceContainer = surfaceContainerDark, + surfaceContainerHigh = surfaceContainerHighDark, + surfaceContainerHighest = surfaceContainerHighestDark, +) \ No newline at end of file diff --git a/app/src/main/kotlin/com/dergoogler/mmrl/ui/theme/color/Jeufosse.kt b/app/src/main/kotlin/com/dergoogler/mmrl/ui/theme/color/Jeufosse.kt new file mode 100644 index 00000000..349d4524 --- /dev/null +++ b/app/src/main/kotlin/com/dergoogler/mmrl/ui/theme/color/Jeufosse.kt @@ -0,0 +1,157 @@ +/* + * Monet, Claude; Autumn at Jeufosse; 1884 + */ + +package com.dergoogler.mmrl.ui.theme.color + +import androidx.compose.material3.darkColorScheme +import androidx.compose.material3.lightColorScheme +import androidx.compose.ui.graphics.Color + +private val primaryLight = Color(0xFF6B5E10) +private val onPrimaryLight = Color(0xFFFFFFFF) +private val primaryContainerLight = Color(0xFFF6E388) +private val onPrimaryContainerLight = Color(0xFF211B00) +private val secondaryLight = Color(0xFF655F40) +private val onSecondaryLight = Color(0xFFFFFFFF) +private val secondaryContainerLight = Color(0xFFEDE3BC) +private val onSecondaryContainerLight = Color(0xFF201C04) +private val tertiaryLight = Color(0xFF426650) +private val onTertiaryLight = Color(0xFFFFFFFF) +private val tertiaryContainerLight = Color(0xFFC4ECD0) +private val onTertiaryContainerLight = Color(0xFF002111) +private val errorLight = Color(0xFFBA1A1A) +private val onErrorLight = Color(0xFFFFFFFF) +private val errorContainerLight = Color(0xFFFFDAD6) +private val onErrorContainerLight = Color(0xFF410002) +private val backgroundLight = Color(0xFFFFF9EC) +private val onBackgroundLight = Color(0xFF1E1C13) +private val surfaceLight = Color(0xFFFFF9EC) +private val onSurfaceLight = Color(0xFF1E1C13) +private val surfaceVariantLight = Color(0xFFE9E2D0) +private val onSurfaceVariantLight = Color(0xFF4A4739) +private val outlineLight = Color(0xFF7C7768) +private val outlineVariantLight = Color(0xFFCCC6B4) +private val scrimLight = Color(0xFF000000) +private val inverseSurfaceLight = Color(0xFF333027) +private val inverseOnSurfaceLight = Color(0xFFF7F0E2) +private val inversePrimaryLight = Color(0xFFD9C76F) +private val surfaceDimLight = Color(0xFFDFDACC) +private val surfaceBrightLight = Color(0xFFFFF9EC) +private val surfaceContainerLowestLight = Color(0xFFFFFFFF) +private val surfaceContainerLowLight = Color(0xFFF9F3E5) +private val surfaceContainerLight = Color(0xFFF4EDDF) +private val surfaceContainerHighLight = Color(0xFFEEE8DA) +private val surfaceContainerHighestLight = Color(0xFFE8E2D4) + +private val primaryDark = Color(0xFFD9C76F) +private val onPrimaryDark = Color(0xFF383000) +private val primaryContainerDark = Color(0xFF524700) +private val onPrimaryContainerDark = Color(0xFFF6E388) +private val secondaryDark = Color(0xFFD0C7A2) +private val onSecondaryDark = Color(0xFF363016) +private val secondaryContainerDark = Color(0xFF4D472B) +private val onSecondaryContainerDark = Color(0xFFEDE3BC) +private val tertiaryDark = Color(0xFFA8D0B5) +private val onTertiaryDark = Color(0xFF133724) +private val tertiaryContainerDark = Color(0xFF2B4E39) +private val onTertiaryContainerDark = Color(0xFFC4ECD0) +private val errorDark = Color(0xFFFFB4AB) +private val onErrorDark = Color(0xFF690005) +private val errorContainerDark = Color(0xFF93000A) +private val onErrorContainerDark = Color(0xFFFFDAD6) +private val backgroundDark = Color(0xFF15130C) +private val onBackgroundDark = Color(0xFFE8E2D4) +private val surfaceDark = Color(0xFF15130C) +private val onSurfaceDark = Color(0xFFE8E2D4) +private val surfaceVariantDark = Color(0xFF4A4739) +private val onSurfaceVariantDark = Color(0xFFCCC6B4) +private val outlineDark = Color(0xFF969180) +private val outlineVariantDark = Color(0xFF4A4739) +private val scrimDark = Color(0xFF000000) +private val inverseSurfaceDark = Color(0xFFE8E2D4) +private val inverseOnSurfaceDark = Color(0xFF333027) +private val inversePrimaryDark = Color(0xFF6B5E10) +private val surfaceDimDark = Color(0xFF15130C) +private val surfaceBrightDark = Color(0xFF3C3930) +private val surfaceContainerLowestDark = Color(0xFF100E07) +private val surfaceContainerLowDark = Color(0xFF1E1C13) +private val surfaceContainerDark = Color(0xFF222017) +private val surfaceContainerHighDark = Color(0xFF2C2A21) +private val surfaceContainerHighestDark = Color(0xFF37352B) + +val JeufosseLightScheme = lightColorScheme( + primary = primaryLight, + onPrimary = onPrimaryLight, + primaryContainer = primaryContainerLight, + onPrimaryContainer = onPrimaryContainerLight, + secondary = secondaryLight, + onSecondary = onSecondaryLight, + secondaryContainer = secondaryContainerLight, + onSecondaryContainer = onSecondaryContainerLight, + tertiary = tertiaryLight, + onTertiary = onTertiaryLight, + tertiaryContainer = tertiaryContainerLight, + onTertiaryContainer = onTertiaryContainerLight, + error = errorLight, + onError = onErrorLight, + errorContainer = errorContainerLight, + onErrorContainer = onErrorContainerLight, + background = backgroundLight, + onBackground = onBackgroundLight, + surface = surfaceLight, + onSurface = onSurfaceLight, + surfaceVariant = surfaceVariantLight, + onSurfaceVariant = onSurfaceVariantLight, + outline = outlineLight, + outlineVariant = outlineVariantLight, + scrim = scrimLight, + inverseSurface = inverseSurfaceLight, + inverseOnSurface = inverseOnSurfaceLight, + inversePrimary = inversePrimaryLight, + surfaceDim = surfaceDimLight, + surfaceBright = surfaceBrightLight, + surfaceContainerLowest = surfaceContainerLowestLight, + surfaceContainerLow = surfaceContainerLowLight, + surfaceContainer = surfaceContainerLight, + surfaceContainerHigh = surfaceContainerHighLight, + surfaceContainerHighest = surfaceContainerHighestLight, +) + +val JeufosseDarkScheme = darkColorScheme( + primary = primaryDark, + onPrimary = onPrimaryDark, + primaryContainer = primaryContainerDark, + onPrimaryContainer = onPrimaryContainerDark, + secondary = secondaryDark, + onSecondary = onSecondaryDark, + secondaryContainer = secondaryContainerDark, + onSecondaryContainer = onSecondaryContainerDark, + tertiary = tertiaryDark, + onTertiary = onTertiaryDark, + tertiaryContainer = tertiaryContainerDark, + onTertiaryContainer = onTertiaryContainerDark, + error = errorDark, + onError = onErrorDark, + errorContainer = errorContainerDark, + onErrorContainer = onErrorContainerDark, + background = backgroundDark, + onBackground = onBackgroundDark, + surface = surfaceDark, + onSurface = onSurfaceDark, + surfaceVariant = surfaceVariantDark, + onSurfaceVariant = onSurfaceVariantDark, + outline = outlineDark, + outlineVariant = outlineVariantDark, + scrim = scrimDark, + inverseSurface = inverseSurfaceDark, + inverseOnSurface = inverseOnSurfaceDark, + inversePrimary = inversePrimaryDark, + surfaceDim = surfaceDimDark, + surfaceBright = surfaceBrightDark, + surfaceContainerLowest = surfaceContainerLowestDark, + surfaceContainerLow = surfaceContainerLowDark, + surfaceContainer = surfaceContainerDark, + surfaceContainerHigh = surfaceContainerHighDark, + surfaceContainerHighest = surfaceContainerHighestDark, +) \ No newline at end of file diff --git a/app/src/main/kotlin/com/dergoogler/mmrl/ui/theme/color/PlainAuvers.kt b/app/src/main/kotlin/com/dergoogler/mmrl/ui/theme/color/PlainAuvers.kt new file mode 100644 index 00000000..feccb9e8 --- /dev/null +++ b/app/src/main/kotlin/com/dergoogler/mmrl/ui/theme/color/PlainAuvers.kt @@ -0,0 +1,157 @@ +/* + * Gogh, Vincent van; The Plain of Auvers; 1890 + */ + +package com.dergoogler.mmrl.ui.theme.color + +import androidx.compose.material3.darkColorScheme +import androidx.compose.material3.lightColorScheme +import androidx.compose.ui.graphics.Color + +private val primaryLight = Color(0xFF416834) +private val onPrimaryLight = Color(0xFFFFFFFF) +private val primaryContainerLight = Color(0xFFC2EFAE) +private val onPrimaryContainerLight = Color(0xFF032100) +private val secondaryLight = Color(0xFF54624D) +private val onSecondaryLight = Color(0xFFFFFFFF) +private val secondaryContainerLight = Color(0xFFD7E7CC) +private val onSecondaryContainerLight = Color(0xFF121F0E) +private val tertiaryLight = Color(0xFF386668) +private val onTertiaryLight = Color(0xFFFFFFFF) +private val tertiaryContainerLight = Color(0xFFBCEBED) +private val onTertiaryContainerLight = Color(0xFF002021) +private val errorLight = Color(0xFFBA1A1A) +private val onErrorLight = Color(0xFFFFFFFF) +private val errorContainerLight = Color(0xFFFFDAD6) +private val onErrorContainerLight = Color(0xFF410002) +private val backgroundLight = Color(0xFFF8FAF0) +private val onBackgroundLight = Color(0xFF191D17) +private val surfaceLight = Color(0xFFF8FAF0) +private val onSurfaceLight = Color(0xFF191D17) +private val surfaceVariantLight = Color(0xFFDFE4D7) +private val onSurfaceVariantLight = Color(0xFF43483F) +private val outlineLight = Color(0xFF73796E) +private val outlineVariantLight = Color(0xFFC3C8BC) +private val scrimLight = Color(0xFF000000) +private val inverseSurfaceLight = Color(0xFF2E322B) +private val inverseOnSurfaceLight = Color(0xFFEFF2E8) +private val inversePrimaryLight = Color(0xFFA7D394) +private val surfaceDimLight = Color(0xFFD8DBD1) +private val surfaceBrightLight = Color(0xFFF8FAF0) +private val surfaceContainerLowestLight = Color(0xFFFFFFFF) +private val surfaceContainerLowLight = Color(0xFFF2F5EB) +private val surfaceContainerLight = Color(0xFFECEFE5) +private val surfaceContainerHighLight = Color(0xFFE7E9DF) +private val surfaceContainerHighestLight = Color(0xFFE1E4DA) + +private val primaryDark = Color(0xFFA7D394) +private val onPrimaryDark = Color(0xFF13380A) +private val primaryContainerDark = Color(0xFF2A4F1F) +private val onPrimaryContainerDark = Color(0xFFC2EFAE) +private val secondaryDark = Color(0xFFBCCBB1) +private val onSecondaryDark = Color(0xFF273421) +private val secondaryContainerDark = Color(0xFF3D4B36) +private val onSecondaryContainerDark = Color(0xFFD7E7CC) +private val tertiaryDark = Color(0xFFA0CFD1) +private val onTertiaryDark = Color(0xFF003739) +private val tertiaryContainerDark = Color(0xFF1E4E50) +private val onTertiaryContainerDark = Color(0xFFBCEBED) +private val errorDark = Color(0xFFFFB4AB) +private val onErrorDark = Color(0xFF690005) +private val errorContainerDark = Color(0xFF93000A) +private val onErrorContainerDark = Color(0xFFFFDAD6) +private val backgroundDark = Color(0xFF11140F) +private val onBackgroundDark = Color(0xFFE1E4DA) +private val surfaceDark = Color(0xFF11140F) +private val onSurfaceDark = Color(0xFFE1E4DA) +private val surfaceVariantDark = Color(0xFF43483F) +private val onSurfaceVariantDark = Color(0xFFC3C8BC) +private val outlineDark = Color(0xFF8D9387) +private val outlineVariantDark = Color(0xFF43483F) +private val scrimDark = Color(0xFF000000) +private val inverseSurfaceDark = Color(0xFFE1E4DA) +private val inverseOnSurfaceDark = Color(0xFF2E322B) +private val inversePrimaryDark = Color(0xFF416834) +private val surfaceDimDark = Color(0xFF11140F) +private val surfaceBrightDark = Color(0xFF373A34) +private val surfaceContainerLowestDark = Color(0xFF0C0F0A) +private val surfaceContainerLowDark = Color(0xFF191D17) +private val surfaceContainerDark = Color(0xFF1D211B) +private val surfaceContainerHighDark = Color(0xFF272B25) +private val surfaceContainerHighestDark = Color(0xFF32362F) + +val PlainAuversLightScheme = lightColorScheme( + primary = primaryLight, + onPrimary = onPrimaryLight, + primaryContainer = primaryContainerLight, + onPrimaryContainer = onPrimaryContainerLight, + secondary = secondaryLight, + onSecondary = onSecondaryLight, + secondaryContainer = secondaryContainerLight, + onSecondaryContainer = onSecondaryContainerLight, + tertiary = tertiaryLight, + onTertiary = onTertiaryLight, + tertiaryContainer = tertiaryContainerLight, + onTertiaryContainer = onTertiaryContainerLight, + error = errorLight, + onError = onErrorLight, + errorContainer = errorContainerLight, + onErrorContainer = onErrorContainerLight, + background = backgroundLight, + onBackground = onBackgroundLight, + surface = surfaceLight, + onSurface = onSurfaceLight, + surfaceVariant = surfaceVariantLight, + onSurfaceVariant = onSurfaceVariantLight, + outline = outlineLight, + outlineVariant = outlineVariantLight, + scrim = scrimLight, + inverseSurface = inverseSurfaceLight, + inverseOnSurface = inverseOnSurfaceLight, + inversePrimary = inversePrimaryLight, + surfaceDim = surfaceDimLight, + surfaceBright = surfaceBrightLight, + surfaceContainerLowest = surfaceContainerLowestLight, + surfaceContainerLow = surfaceContainerLowLight, + surfaceContainer = surfaceContainerLight, + surfaceContainerHigh = surfaceContainerHighLight, + surfaceContainerHighest = surfaceContainerHighestLight, +) + +val PlainAuversDarkScheme = darkColorScheme( + primary = primaryDark, + onPrimary = onPrimaryDark, + primaryContainer = primaryContainerDark, + onPrimaryContainer = onPrimaryContainerDark, + secondary = secondaryDark, + onSecondary = onSecondaryDark, + secondaryContainer = secondaryContainerDark, + onSecondaryContainer = onSecondaryContainerDark, + tertiary = tertiaryDark, + onTertiary = onTertiaryDark, + tertiaryContainer = tertiaryContainerDark, + onTertiaryContainer = onTertiaryContainerDark, + error = errorDark, + onError = onErrorDark, + errorContainer = errorContainerDark, + onErrorContainer = onErrorContainerDark, + background = backgroundDark, + onBackground = onBackgroundDark, + surface = surfaceDark, + onSurface = onSurfaceDark, + surfaceVariant = surfaceVariantDark, + onSurfaceVariant = onSurfaceVariantDark, + outline = outlineDark, + outlineVariant = outlineVariantDark, + scrim = scrimDark, + inverseSurface = inverseSurfaceDark, + inverseOnSurface = inverseOnSurfaceDark, + inversePrimary = inversePrimaryDark, + surfaceDim = surfaceDimDark, + surfaceBright = surfaceBrightDark, + surfaceContainerLowest = surfaceContainerLowestDark, + surfaceContainerLow = surfaceContainerLowDark, + surfaceContainer = surfaceContainerDark, + surfaceContainerHigh = surfaceContainerHighDark, + surfaceContainerHighest = surfaceContainerHighestDark, +) \ No newline at end of file diff --git a/app/src/main/kotlin/com/dergoogler/mmrl/ui/theme/color/PoppyField.kt b/app/src/main/kotlin/com/dergoogler/mmrl/ui/theme/color/PoppyField.kt new file mode 100644 index 00000000..1266314b --- /dev/null +++ b/app/src/main/kotlin/com/dergoogler/mmrl/ui/theme/color/PoppyField.kt @@ -0,0 +1,157 @@ +/* + * Monet, Claude; Poppy Field in a Hollow near Giverny; 1885 + */ + +package com.dergoogler.mmrl.ui.theme.color + +import androidx.compose.material3.darkColorScheme +import androidx.compose.material3.lightColorScheme +import androidx.compose.ui.graphics.Color + +private val primaryLight = Color(0xFF046B5C) +private val onPrimaryLight = Color(0xFFFFFFFF) +private val primaryContainerLight = Color(0xFFA0F2DF) +private val onPrimaryContainerLight = Color(0xFF00201B) +private val secondaryLight = Color(0xFF4A635D) +private val onSecondaryLight = Color(0xFFFFFFFF) +private val secondaryContainerLight = Color(0xFFCDE8E0) +private val onSecondaryContainerLight = Color(0xFF06201B) +private val tertiaryLight = Color(0xFF436278) +private val onTertiaryLight = Color(0xFFFFFFFF) +private val tertiaryContainerLight = Color(0xFFC9E6FF) +private val onTertiaryContainerLight = Color(0xFF001E2F) +private val errorLight = Color(0xFFBA1A1A) +private val onErrorLight = Color(0xFFFFFFFF) +private val errorContainerLight = Color(0xFFFFDAD6) +private val onErrorContainerLight = Color(0xFF410002) +private val backgroundLight = Color(0xFFF5FBF7) +private val onBackgroundLight = Color(0xFF171D1B) +private val surfaceLight = Color(0xFFF5FBF7) +private val onSurfaceLight = Color(0xFF171D1B) +private val surfaceVariantLight = Color(0xFFDAE5E0) +private val onSurfaceVariantLight = Color(0xFF3F4946) +private val outlineLight = Color(0xFF6F7976) +private val outlineVariantLight = Color(0xFFBEC9C5) +private val scrimLight = Color(0xFF000000) +private val inverseSurfaceLight = Color(0xFF2B3230) +private val inverseOnSurfaceLight = Color(0xFFECF2EF) +private val inversePrimaryLight = Color(0xFF84D6C3) +private val surfaceDimLight = Color(0xFFD5DBD8) +private val surfaceBrightLight = Color(0xFFF5FBF7) +private val surfaceContainerLowestLight = Color(0xFFFFFFFF) +private val surfaceContainerLowLight = Color(0xFFEFF5F2) +private val surfaceContainerLight = Color(0xFFE9EFEC) +private val surfaceContainerHighLight = Color(0xFFE3EAE6) +private val surfaceContainerHighestLight = Color(0xFFDEE4E1) + +private val primaryDark = Color(0xFF84D6C3) +private val onPrimaryDark = Color(0xFF00382F) +private val primaryContainerDark = Color(0xFF005045) +private val onPrimaryContainerDark = Color(0xFFA0F2DF) +private val secondaryDark = Color(0xFFB1CCC4) +private val onSecondaryDark = Color(0xFF1C352F) +private val secondaryContainerDark = Color(0xFF334B46) +private val onSecondaryContainerDark = Color(0xFFCDE8E0) +private val tertiaryDark = Color(0xFFABCAE4) +private val onTertiaryDark = Color(0xFF123348) +private val tertiaryContainerDark = Color(0xFF2B4A5F) +private val onTertiaryContainerDark = Color(0xFFC9E6FF) +private val errorDark = Color(0xFFFFB4AB) +private val onErrorDark = Color(0xFF690005) +private val errorContainerDark = Color(0xFF93000A) +private val onErrorContainerDark = Color(0xFFFFDAD6) +private val backgroundDark = Color(0xFF0E1513) +private val onBackgroundDark = Color(0xFFDEE4E1) +private val surfaceDark = Color(0xFF0E1513) +private val onSurfaceDark = Color(0xFFDEE4E1) +private val surfaceVariantDark = Color(0xFF3F4946) +private val onSurfaceVariantDark = Color(0xFFBEC9C5) +private val outlineDark = Color(0xFF89938F) +private val outlineVariantDark = Color(0xFF3F4946) +private val scrimDark = Color(0xFF000000) +private val inverseSurfaceDark = Color(0xFFDEE4E1) +private val inverseOnSurfaceDark = Color(0xFF2B3230) +private val inversePrimaryDark = Color(0xFF046B5C) +private val surfaceDimDark = Color(0xFF0E1513) +private val surfaceBrightDark = Color(0xFF343B38) +private val surfaceContainerLowestDark = Color(0xFF090F0E) +private val surfaceContainerLowDark = Color(0xFF171D1B) +private val surfaceContainerDark = Color(0xFF1B211F) +private val surfaceContainerHighDark = Color(0xFF252B29) +private val surfaceContainerHighestDark = Color(0xFF303634) + +val PoppyFieldLightScheme = lightColorScheme( + primary = primaryLight, + onPrimary = onPrimaryLight, + primaryContainer = primaryContainerLight, + onPrimaryContainer = onPrimaryContainerLight, + secondary = secondaryLight, + onSecondary = onSecondaryLight, + secondaryContainer = secondaryContainerLight, + onSecondaryContainer = onSecondaryContainerLight, + tertiary = tertiaryLight, + onTertiary = onTertiaryLight, + tertiaryContainer = tertiaryContainerLight, + onTertiaryContainer = onTertiaryContainerLight, + error = errorLight, + onError = onErrorLight, + errorContainer = errorContainerLight, + onErrorContainer = onErrorContainerLight, + background = backgroundLight, + onBackground = onBackgroundLight, + surface = surfaceLight, + onSurface = onSurfaceLight, + surfaceVariant = surfaceVariantLight, + onSurfaceVariant = onSurfaceVariantLight, + outline = outlineLight, + outlineVariant = outlineVariantLight, + scrim = scrimLight, + inverseSurface = inverseSurfaceLight, + inverseOnSurface = inverseOnSurfaceLight, + inversePrimary = inversePrimaryLight, + surfaceDim = surfaceDimLight, + surfaceBright = surfaceBrightLight, + surfaceContainerLowest = surfaceContainerLowestLight, + surfaceContainerLow = surfaceContainerLowLight, + surfaceContainer = surfaceContainerLight, + surfaceContainerHigh = surfaceContainerHighLight, + surfaceContainerHighest = surfaceContainerHighestLight, +) + +val PoppyFieldDarkScheme = darkColorScheme( + primary = primaryDark, + onPrimary = onPrimaryDark, + primaryContainer = primaryContainerDark, + onPrimaryContainer = onPrimaryContainerDark, + secondary = secondaryDark, + onSecondary = onSecondaryDark, + secondaryContainer = secondaryContainerDark, + onSecondaryContainer = onSecondaryContainerDark, + tertiary = tertiaryDark, + onTertiary = onTertiaryDark, + tertiaryContainer = tertiaryContainerDark, + onTertiaryContainer = onTertiaryContainerDark, + error = errorDark, + onError = onErrorDark, + errorContainer = errorContainerDark, + onErrorContainer = onErrorContainerDark, + background = backgroundDark, + onBackground = onBackgroundDark, + surface = surfaceDark, + onSurface = onSurfaceDark, + surfaceVariant = surfaceVariantDark, + onSurfaceVariant = onSurfaceVariantDark, + outline = outlineDark, + outlineVariant = outlineVariantDark, + scrim = scrimDark, + inverseSurface = inverseSurfaceDark, + inverseOnSurface = inverseOnSurfaceDark, + inversePrimary = inversePrimaryDark, + surfaceDim = surfaceDimDark, + surfaceBright = surfaceBrightDark, + surfaceContainerLowest = surfaceContainerLowestDark, + surfaceContainerLow = surfaceContainerLowDark, + surfaceContainer = surfaceContainerDark, + surfaceContainerHigh = surfaceContainerHighDark, + surfaceContainerHighest = surfaceContainerHighestDark, +) \ No newline at end of file diff --git a/app/src/main/kotlin/com/dergoogler/mmrl/ui/theme/color/Pourville.kt b/app/src/main/kotlin/com/dergoogler/mmrl/ui/theme/color/Pourville.kt new file mode 100644 index 00000000..95d6905c --- /dev/null +++ b/app/src/main/kotlin/com/dergoogler/mmrl/ui/theme/color/Pourville.kt @@ -0,0 +1,157 @@ +/* + * Monet, Claude; Cliff Walk at Pourville; 1882 + */ + +package com.dergoogler.mmrl.ui.theme.color + +import androidx.compose.material3.darkColorScheme +import androidx.compose.material3.lightColorScheme +import androidx.compose.ui.graphics.Color + +private val primaryLight = Color(0xFF1D6586) +private val onPrimaryLight = Color(0xFFFFFFFF) +private val primaryContainerLight = Color(0xFFC4E7FF) +private val onPrimaryContainerLight = Color(0xFF001E2C) +private val secondaryLight = Color(0xFF4E616D) +private val onSecondaryLight = Color(0xFFFFFFFF) +private val secondaryContainerLight = Color(0xFFD1E5F4) +private val onSecondaryContainerLight = Color(0xFF0A1E28) +private val tertiaryLight = Color(0xFF615A7D) +private val onTertiaryLight = Color(0xFFFFFFFF) +private val tertiaryContainerLight = Color(0xFFE7DEFF) +private val onTertiaryContainerLight = Color(0xFF1D1736) +private val errorLight = Color(0xFFBA1A1A) +private val onErrorLight = Color(0xFFFFFFFF) +private val errorContainerLight = Color(0xFFFFDAD6) +private val onErrorContainerLight = Color(0xFF410002) +private val backgroundLight = Color(0xFFF6FAFE) +private val onBackgroundLight = Color(0xFF181C1F) +private val surfaceLight = Color(0xFFF6FAFE) +private val onSurfaceLight = Color(0xFF181C1F) +private val surfaceVariantLight = Color(0xFFDDE3EA) +private val onSurfaceVariantLight = Color(0xFF41484D) +private val outlineLight = Color(0xFF71787E) +private val outlineVariantLight = Color(0xFFC0C7CD) +private val scrimLight = Color(0xFF000000) +private val inverseSurfaceLight = Color(0xFF2C3134) +private val inverseOnSurfaceLight = Color(0xFFEDF1F5) +private val inversePrimaryLight = Color(0xFF90CEF4) +private val surfaceDimLight = Color(0xFFD7DADF) +private val surfaceBrightLight = Color(0xFFF6FAFE) +private val surfaceContainerLowestLight = Color(0xFFFFFFFF) +private val surfaceContainerLowLight = Color(0xFFF0F4F8) +private val surfaceContainerLight = Color(0xFFEBEEF3) +private val surfaceContainerHighLight = Color(0xFFE5E8ED) +private val surfaceContainerHighestLight = Color(0xFFDFE3E7) + +private val primaryDark = Color(0xFF90CEF4) +private val onPrimaryDark = Color(0xFF00344A) +private val primaryContainerDark = Color(0xFF004C69) +private val onPrimaryContainerDark = Color(0xFFC4E7FF) +private val secondaryDark = Color(0xFFB5C9D7) +private val onSecondaryDark = Color(0xFF20333E) +private val secondaryContainerDark = Color(0xFF374955) +private val onSecondaryContainerDark = Color(0xFFD1E5F4) +private val tertiaryDark = Color(0xFFCAC1E9) +private val onTertiaryDark = Color(0xFF322C4C) +private val tertiaryContainerDark = Color(0xFF494264) +private val onTertiaryContainerDark = Color(0xFFE7DEFF) +private val errorDark = Color(0xFFFFB4AB) +private val onErrorDark = Color(0xFF690005) +private val errorContainerDark = Color(0xFF93000A) +private val onErrorContainerDark = Color(0xFFFFDAD6) +private val backgroundDark = Color(0xFF0F1417) +private val onBackgroundDark = Color(0xFFDFE3E7) +private val surfaceDark = Color(0xFF0F1417) +private val onSurfaceDark = Color(0xFFDFE3E7) +private val surfaceVariantDark = Color(0xFF41484D) +private val onSurfaceVariantDark = Color(0xFFC0C7CD) +private val outlineDark = Color(0xFF8B9297) +private val outlineVariantDark = Color(0xFF41484D) +private val scrimDark = Color(0xFF000000) +private val inverseSurfaceDark = Color(0xFFDFE3E7) +private val inverseOnSurfaceDark = Color(0xFF2C3134) +private val inversePrimaryDark = Color(0xFF1D6586) +private val surfaceDimDark = Color(0xFF0F1417) +private val surfaceBrightDark = Color(0xFF353A3D) +private val surfaceContainerLowestDark = Color(0xFF0A0F12) +private val surfaceContainerLowDark = Color(0xFF181C1F) +private val surfaceContainerDark = Color(0xFF1C2023) +private val surfaceContainerHighDark = Color(0xFF262B2E) +private val surfaceContainerHighestDark = Color(0xFF313539) + +val PourvilleLightScheme = lightColorScheme( + primary = primaryLight, + onPrimary = onPrimaryLight, + primaryContainer = primaryContainerLight, + onPrimaryContainer = onPrimaryContainerLight, + secondary = secondaryLight, + onSecondary = onSecondaryLight, + secondaryContainer = secondaryContainerLight, + onSecondaryContainer = onSecondaryContainerLight, + tertiary = tertiaryLight, + onTertiary = onTertiaryLight, + tertiaryContainer = tertiaryContainerLight, + onTertiaryContainer = onTertiaryContainerLight, + error = errorLight, + onError = onErrorLight, + errorContainer = errorContainerLight, + onErrorContainer = onErrorContainerLight, + background = backgroundLight, + onBackground = onBackgroundLight, + surface = surfaceLight, + onSurface = onSurfaceLight, + surfaceVariant = surfaceVariantLight, + onSurfaceVariant = onSurfaceVariantLight, + outline = outlineLight, + outlineVariant = outlineVariantLight, + scrim = scrimLight, + inverseSurface = inverseSurfaceLight, + inverseOnSurface = inverseOnSurfaceLight, + inversePrimary = inversePrimaryLight, + surfaceDim = surfaceDimLight, + surfaceBright = surfaceBrightLight, + surfaceContainerLowest = surfaceContainerLowestLight, + surfaceContainerLow = surfaceContainerLowLight, + surfaceContainer = surfaceContainerLight, + surfaceContainerHigh = surfaceContainerHighLight, + surfaceContainerHighest = surfaceContainerHighestLight, +) + +val PourvilleDarkScheme = darkColorScheme( + primary = primaryDark, + onPrimary = onPrimaryDark, + primaryContainer = primaryContainerDark, + onPrimaryContainer = onPrimaryContainerDark, + secondary = secondaryDark, + onSecondary = onSecondaryDark, + secondaryContainer = secondaryContainerDark, + onSecondaryContainer = onSecondaryContainerDark, + tertiary = tertiaryDark, + onTertiary = onTertiaryDark, + tertiaryContainer = tertiaryContainerDark, + onTertiaryContainer = onTertiaryContainerDark, + error = errorDark, + onError = onErrorDark, + errorContainer = errorContainerDark, + onErrorContainer = onErrorContainerDark, + background = backgroundDark, + onBackground = onBackgroundDark, + surface = surfaceDark, + onSurface = onSurfaceDark, + surfaceVariant = surfaceVariantDark, + onSurfaceVariant = onSurfaceVariantDark, + outline = outlineDark, + outlineVariant = outlineVariantDark, + scrim = scrimDark, + inverseSurface = inverseSurfaceDark, + inverseOnSurface = inverseOnSurfaceDark, + inversePrimary = inversePrimaryDark, + surfaceDim = surfaceDimDark, + surfaceBright = surfaceBrightDark, + surfaceContainerLowest = surfaceContainerLowestDark, + surfaceContainerLow = surfaceContainerLowDark, + surfaceContainer = surfaceContainerDark, + surfaceContainerHigh = surfaceContainerHighDark, + surfaceContainerHighest = surfaceContainerHighestDark, +) \ No newline at end of file diff --git a/app/src/main/kotlin/com/dergoogler/mmrl/ui/theme/color/SoleilLevant.kt b/app/src/main/kotlin/com/dergoogler/mmrl/ui/theme/color/SoleilLevant.kt new file mode 100644 index 00000000..66086afe --- /dev/null +++ b/app/src/main/kotlin/com/dergoogler/mmrl/ui/theme/color/SoleilLevant.kt @@ -0,0 +1,157 @@ +/* + * Impression, Soleil Levant (Rising Sun), 1872 - Claude Monet + */ + +package com.dergoogler.mmrl.ui.theme.color + +import androidx.compose.material3.darkColorScheme +import androidx.compose.material3.lightColorScheme +import androidx.compose.ui.graphics.Color + +private val primaryLight = Color(0xFF465D91) +private val onPrimaryLight = Color(0xFFFFFFFF) +private val primaryContainerLight = Color(0xFFD9E2FF) +private val onPrimaryContainerLight = Color(0xFF001944) +private val secondaryLight = Color(0xFF575E71) +private val onSecondaryLight = Color(0xFFFFFFFF) +private val secondaryContainerLight = Color(0xFFDCE2F9) +private val onSecondaryContainerLight = Color(0xFF141B2C) +private val tertiaryLight = Color(0xFF725572) +private val onTertiaryLight = Color(0xFFFFFFFF) +private val tertiaryContainerLight = Color(0xFFFDD7FA) +private val onTertiaryContainerLight = Color(0xFF2A132C) +private val errorLight = Color(0xFFBA1A1A) +private val onErrorLight = Color(0xFFFFFFFF) +private val errorContainerLight = Color(0xFFFFDAD6) +private val onErrorContainerLight = Color(0xFF410002) +private val backgroundLight = Color(0xFFFAF8FF) +private val onBackgroundLight = Color(0xFF1A1B20) +private val surfaceLight = Color(0xFFFAF8FF) +private val onSurfaceLight = Color(0xFF1A1B20) +private val surfaceVariantLight = Color(0xFFE1E2EC) +private val onSurfaceVariantLight = Color(0xFF44464F) +private val outlineLight = Color(0xFF757780) +private val outlineVariantLight = Color(0xFFC5C6D0) +private val scrimLight = Color(0xFF000000) +private val inverseSurfaceLight = Color(0xFF2F3036) +private val inverseOnSurfaceLight = Color(0xFFF1F0F7) +private val inversePrimaryLight = Color(0xFFB0C6FF) +private val surfaceDimLight = Color(0xFFDAD9E0) +private val surfaceBrightLight = Color(0xFFFAF8FF) +private val surfaceContainerLowestLight = Color(0xFFFFFFFF) +private val surfaceContainerLowLight = Color(0xFFF4F3FA) +private val surfaceContainerLight = Color(0xFFEEEDF4) +private val surfaceContainerHighLight = Color(0xFFE8E7EF) +private val surfaceContainerHighestLight = Color(0xFFE2E2E9) + +private val primaryDark = Color(0xFFB0C6FF) +private val onPrimaryDark = Color(0xFF142E60) +private val primaryContainerDark = Color(0xFF2E4578) +private val onPrimaryContainerDark = Color(0xFFD9E2FF) +private val secondaryDark = Color(0xFFC0C6DC) +private val onSecondaryDark = Color(0xFF293042) +private val secondaryContainerDark = Color(0xFF404659) +private val onSecondaryContainerDark = Color(0xFFDCE2F9) +private val tertiaryDark = Color(0xFFE0BBDE) +private val onTertiaryDark = Color(0xFF412742) +private val tertiaryContainerDark = Color(0xFF593D5A) +private val onTertiaryContainerDark = Color(0xFFFDD7FA) +private val errorDark = Color(0xFFFFB4AB) +private val onErrorDark = Color(0xFF690005) +private val errorContainerDark = Color(0xFF93000A) +private val onErrorContainerDark = Color(0xFFFFDAD6) +private val backgroundDark = Color(0xFF121318) +private val onBackgroundDark = Color(0xFFE2E2E9) +private val surfaceDark = Color(0xFF121318) +private val onSurfaceDark = Color(0xFFE2E2E9) +private val surfaceVariantDark = Color(0xFF44464F) +private val onSurfaceVariantDark = Color(0xFFC5C6D0) +private val outlineDark = Color(0xFF8F9099) +private val outlineVariantDark = Color(0xFF44464F) +private val scrimDark = Color(0xFF000000) +private val inverseSurfaceDark = Color(0xFFE2E2E9) +private val inverseOnSurfaceDark = Color(0xFF2F3036) +private val inversePrimaryDark = Color(0xFF465D91) +private val surfaceDimDark = Color(0xFF121318) +private val surfaceBrightDark = Color(0xFF38393F) +private val surfaceContainerLowestDark = Color(0xFF0C0E13) +private val surfaceContainerLowDark = Color(0xFF1A1B20) +private val surfaceContainerDark = Color(0xFF1E1F25) +private val surfaceContainerHighDark = Color(0xFF282A2F) +private val surfaceContainerHighestDark = Color(0xFF33353A) + +val SoleilLevantLightScheme = lightColorScheme( + primary = primaryLight, + onPrimary = onPrimaryLight, + primaryContainer = primaryContainerLight, + onPrimaryContainer = onPrimaryContainerLight, + secondary = secondaryLight, + onSecondary = onSecondaryLight, + secondaryContainer = secondaryContainerLight, + onSecondaryContainer = onSecondaryContainerLight, + tertiary = tertiaryLight, + onTertiary = onTertiaryLight, + tertiaryContainer = tertiaryContainerLight, + onTertiaryContainer = onTertiaryContainerLight, + error = errorLight, + onError = onErrorLight, + errorContainer = errorContainerLight, + onErrorContainer = onErrorContainerLight, + background = backgroundLight, + onBackground = onBackgroundLight, + surface = surfaceLight, + onSurface = onSurfaceLight, + surfaceVariant = surfaceVariantLight, + onSurfaceVariant = onSurfaceVariantLight, + outline = outlineLight, + outlineVariant = outlineVariantLight, + scrim = scrimLight, + inverseSurface = inverseSurfaceLight, + inverseOnSurface = inverseOnSurfaceLight, + inversePrimary = inversePrimaryLight, + surfaceDim = surfaceDimLight, + surfaceBright = surfaceBrightLight, + surfaceContainerLowest = surfaceContainerLowestLight, + surfaceContainerLow = surfaceContainerLowLight, + surfaceContainer = surfaceContainerLight, + surfaceContainerHigh = surfaceContainerHighLight, + surfaceContainerHighest = surfaceContainerHighestLight, +) + +val SoleilLevantDarkScheme = darkColorScheme( + primary = primaryDark, + onPrimary = onPrimaryDark, + primaryContainer = primaryContainerDark, + onPrimaryContainer = onPrimaryContainerDark, + secondary = secondaryDark, + onSecondary = onSecondaryDark, + secondaryContainer = secondaryContainerDark, + onSecondaryContainer = onSecondaryContainerDark, + tertiary = tertiaryDark, + onTertiary = onTertiaryDark, + tertiaryContainer = tertiaryContainerDark, + onTertiaryContainer = onTertiaryContainerDark, + error = errorDark, + onError = onErrorDark, + errorContainer = errorContainerDark, + onErrorContainer = onErrorContainerDark, + background = backgroundDark, + onBackground = onBackgroundDark, + surface = surfaceDark, + onSurface = onSurfaceDark, + surfaceVariant = surfaceVariantDark, + onSurfaceVariant = onSurfaceVariantDark, + outline = outlineDark, + outlineVariant = outlineVariantDark, + scrim = scrimDark, + inverseSurface = inverseSurfaceDark, + inverseOnSurface = inverseOnSurfaceDark, + inversePrimary = inversePrimaryDark, + surfaceDim = surfaceDimDark, + surfaceBright = surfaceBrightDark, + surfaceContainerLowest = surfaceContainerLowestDark, + surfaceContainerLow = surfaceContainerLowDark, + surfaceContainer = surfaceContainerDark, + surfaceContainerHigh = surfaceContainerHighDark, + surfaceContainerHighest = surfaceContainerHighestDark, +) \ No newline at end of file diff --git a/app/src/main/kotlin/com/dergoogler/mmrl/ui/theme/color/WildRoses.kt b/app/src/main/kotlin/com/dergoogler/mmrl/ui/theme/color/WildRoses.kt new file mode 100644 index 00000000..548803f1 --- /dev/null +++ b/app/src/main/kotlin/com/dergoogler/mmrl/ui/theme/color/WildRoses.kt @@ -0,0 +1,157 @@ +/* + * Gogh, Vincent van; Wild Roses; April 1890 - May 1890 + */ + +package com.dergoogler.mmrl.ui.theme.color + +import androidx.compose.material3.darkColorScheme +import androidx.compose.material3.lightColorScheme +import androidx.compose.ui.graphics.Color + +private val primaryLight = Color(0xFF576422) +private val onPrimaryLight = Color(0xFFFFFFFF) +private val primaryContainerLight = Color(0xFFDAEB99) +private val onPrimaryContainerLight = Color(0xFF171E00) +private val secondaryLight = Color(0xFF5C6146) +private val onSecondaryLight = Color(0xFFFFFFFF) +private val secondaryContainerLight = Color(0xFFE1E6C3) +private val onSecondaryContainerLight = Color(0xFF191D08) +private val tertiaryLight = Color(0xFF3A665D) +private val onTertiaryLight = Color(0xFFFFFFFF) +private val tertiaryContainerLight = Color(0xFFBDECE0) +private val onTertiaryContainerLight = Color(0xFF00201B) +private val errorLight = Color(0xFFBA1A1A) +private val onErrorLight = Color(0xFFFFFFFF) +private val errorContainerLight = Color(0xFFFFDAD6) +private val onErrorContainerLight = Color(0xFF410002) +private val backgroundLight = Color(0xFFFBFAED) +private val onBackgroundLight = Color(0xFF1B1C15) +private val surfaceLight = Color(0xFFFBFAED) +private val onSurfaceLight = Color(0xFF1B1C15) +private val surfaceVariantLight = Color(0xFFE3E4D3) +private val onSurfaceVariantLight = Color(0xFF46483C) +private val outlineLight = Color(0xFF77786A) +private val outlineVariantLight = Color(0xFFC7C8B8) +private val scrimLight = Color(0xFF000000) +private val inverseSurfaceLight = Color(0xFF303129) +private val inverseOnSurfaceLight = Color(0xFFF2F1E5) +private val inversePrimaryLight = Color(0xFFBECE7F) +private val surfaceDimLight = Color(0xFFDBDBCE) +private val surfaceBrightLight = Color(0xFFFBFAED) +private val surfaceContainerLowestLight = Color(0xFFFFFFFF) +private val surfaceContainerLowLight = Color(0xFFF5F4E8) +private val surfaceContainerLight = Color(0xFFEFEEE2) +private val surfaceContainerHighLight = Color(0xFFEAE9DC) +private val surfaceContainerHighestLight = Color(0xFFE4E3D7) + +private val primaryDark = Color(0xFFBECE7F) +private val onPrimaryDark = Color(0xFF2A3400) +private val primaryContainerDark = Color(0xFF3F4C0A) +private val onPrimaryContainerDark = Color(0xFFDAEB99) +private val secondaryDark = Color(0xFFC4CAA8) +private val onSecondaryDark = Color(0xFF2E331B) +private val secondaryContainerDark = Color(0xFF444930) +private val onSecondaryContainerDark = Color(0xFFE1E6C3) +private val tertiaryDark = Color(0xFFA1D0C4) +private val onTertiaryDark = Color(0xFF04372F) +private val tertiaryContainerDark = Color(0xFF214E46) +private val onTertiaryContainerDark = Color(0xFFBDECE0) +private val errorDark = Color(0xFFFFB4AB) +private val onErrorDark = Color(0xFF690005) +private val errorContainerDark = Color(0xFF93000A) +private val onErrorContainerDark = Color(0xFFFFDAD6) +private val backgroundDark = Color(0xFF13140D) +private val onBackgroundDark = Color(0xFFE4E3D7) +private val surfaceDark = Color(0xFF13140D) +private val onSurfaceDark = Color(0xFFE4E3D7) +private val surfaceVariantDark = Color(0xFF46483C) +private val onSurfaceVariantDark = Color(0xFFC7C8B8) +private val outlineDark = Color(0xFF919283) +private val outlineVariantDark = Color(0xFF46483C) +private val scrimDark = Color(0xFF000000) +private val inverseSurfaceDark = Color(0xFFE4E3D7) +private val inverseOnSurfaceDark = Color(0xFF303129) +private val inversePrimaryDark = Color(0xFF576422) +private val surfaceDimDark = Color(0xFF13140D) +private val surfaceBrightDark = Color(0xFF393A31) +private val surfaceContainerLowestDark = Color(0xFF0E0F08) +private val surfaceContainerLowDark = Color(0xFF1B1C15) +private val surfaceContainerDark = Color(0xFF1F2019) +private val surfaceContainerHighDark = Color(0xFF292B23) +private val surfaceContainerHighestDark = Color(0xFF34352D) + +val WildRosesLightScheme = lightColorScheme( + primary = primaryLight, + onPrimary = onPrimaryLight, + primaryContainer = primaryContainerLight, + onPrimaryContainer = onPrimaryContainerLight, + secondary = secondaryLight, + onSecondary = onSecondaryLight, + secondaryContainer = secondaryContainerLight, + onSecondaryContainer = onSecondaryContainerLight, + tertiary = tertiaryLight, + onTertiary = onTertiaryLight, + tertiaryContainer = tertiaryContainerLight, + onTertiaryContainer = onTertiaryContainerLight, + error = errorLight, + onError = onErrorLight, + errorContainer = errorContainerLight, + onErrorContainer = onErrorContainerLight, + background = backgroundLight, + onBackground = onBackgroundLight, + surface = surfaceLight, + onSurface = onSurfaceLight, + surfaceVariant = surfaceVariantLight, + onSurfaceVariant = onSurfaceVariantLight, + outline = outlineLight, + outlineVariant = outlineVariantLight, + scrim = scrimLight, + inverseSurface = inverseSurfaceLight, + inverseOnSurface = inverseOnSurfaceLight, + inversePrimary = inversePrimaryLight, + surfaceDim = surfaceDimLight, + surfaceBright = surfaceBrightLight, + surfaceContainerLowest = surfaceContainerLowestLight, + surfaceContainerLow = surfaceContainerLowLight, + surfaceContainer = surfaceContainerLight, + surfaceContainerHigh = surfaceContainerHighLight, + surfaceContainerHighest = surfaceContainerHighestLight, +) + +val WildRosesDarkScheme = darkColorScheme( + primary = primaryDark, + onPrimary = onPrimaryDark, + primaryContainer = primaryContainerDark, + onPrimaryContainer = onPrimaryContainerDark, + secondary = secondaryDark, + onSecondary = onSecondaryDark, + secondaryContainer = secondaryContainerDark, + onSecondaryContainer = onSecondaryContainerDark, + tertiary = tertiaryDark, + onTertiary = onTertiaryDark, + tertiaryContainer = tertiaryContainerDark, + onTertiaryContainer = onTertiaryContainerDark, + error = errorDark, + onError = onErrorDark, + errorContainer = errorContainerDark, + onErrorContainer = onErrorContainerDark, + background = backgroundDark, + onBackground = onBackgroundDark, + surface = surfaceDark, + onSurface = onSurfaceDark, + surfaceVariant = surfaceVariantDark, + onSurfaceVariant = onSurfaceVariantDark, + outline = outlineDark, + outlineVariant = outlineVariantDark, + scrim = scrimDark, + inverseSurface = inverseSurfaceDark, + inverseOnSurface = inverseOnSurfaceDark, + inversePrimary = inversePrimaryDark, + surfaceDim = surfaceDimDark, + surfaceBright = surfaceBrightDark, + surfaceContainerLowest = surfaceContainerLowestDark, + surfaceContainerLow = surfaceContainerLowDark, + surfaceContainer = surfaceContainerDark, + surfaceContainerHigh = surfaceContainerHighDark, + surfaceContainerHighest = surfaceContainerHighestDark, +) \ No newline at end of file diff --git a/app/src/main/kotlin/com/dergoogler/mmrl/ui/utils/BottomSheetExt.kt b/app/src/main/kotlin/com/dergoogler/mmrl/ui/utils/BottomSheetExt.kt new file mode 100644 index 00000000..3a6ebcd0 --- /dev/null +++ b/app/src/main/kotlin/com/dergoogler/mmrl/ui/utils/BottomSheetExt.kt @@ -0,0 +1,9 @@ +package com.dergoogler.mmrl.ui.utils + +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.BottomSheetDefaults +import androidx.compose.ui.unit.Dp + +@Suppress("UnusedReceiverParameter") +fun BottomSheetDefaults.expandedShape(size: Dp) = + RoundedCornerShape(topStart = size, topEnd = size) \ No newline at end of file diff --git a/app/src/main/kotlin/com/dergoogler/mmrl/ui/utils/LazyListStateExt.kt b/app/src/main/kotlin/com/dergoogler/mmrl/ui/utils/LazyListStateExt.kt new file mode 100644 index 00000000..49dcedce --- /dev/null +++ b/app/src/main/kotlin/com/dergoogler/mmrl/ui/utils/LazyListStateExt.kt @@ -0,0 +1,28 @@ +package com.dergoogler.mmrl.ui.utils + +import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.State +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue + +@Composable +fun LazyListState.isScrollingUp(): State { + var previousIndex by remember(this) { mutableIntStateOf(firstVisibleItemIndex) } + var previousScrollOffset by remember(this) { mutableIntStateOf(firstVisibleItemScrollOffset) } + return remember(this) { + derivedStateOf { + if (previousIndex != firstVisibleItemIndex) { + previousIndex > firstVisibleItemIndex + } else { + previousScrollOffset >= firstVisibleItemScrollOffset + }.also { + previousIndex = firstVisibleItemIndex + previousScrollOffset = firstVisibleItemScrollOffset + } + } + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/com/dergoogler/mmrl/ui/utils/MenuExt.kt b/app/src/main/kotlin/com/dergoogler/mmrl/ui/utils/MenuExt.kt new file mode 100644 index 00000000..9a54bf80 --- /dev/null +++ b/app/src/main/kotlin/com/dergoogler/mmrl/ui/utils/MenuExt.kt @@ -0,0 +1,16 @@ +package com.dergoogler.mmrl.ui.utils + +import androidx.compose.foundation.shape.CornerBasedShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.unit.dp + +@Composable +fun ProvideMenuShape( + value: CornerBasedShape = RoundedCornerShape(8.dp), + content: @Composable () -> Unit +) = MaterialTheme( + shapes = MaterialTheme.shapes.copy(extraSmall = value), + content = content +) \ No newline at end of file diff --git a/app/src/main/kotlin/com/dergoogler/mmrl/ui/utils/NavControllerExt.kt b/app/src/main/kotlin/com/dergoogler/mmrl/ui/utils/NavControllerExt.kt new file mode 100644 index 00000000..482c0200 --- /dev/null +++ b/app/src/main/kotlin/com/dergoogler/mmrl/ui/utils/NavControllerExt.kt @@ -0,0 +1,29 @@ +package com.dergoogler.mmrl.ui.utils + +import androidx.navigation.NavController +import androidx.navigation.NavGraph.Companion.findStartDestination +import androidx.navigation.NavOptionsBuilder + +fun NavController.navigateSingleTopTo( + route: String, + builder: NavOptionsBuilder.() -> Unit = {} +) = navigate( + route = route +) { + launchSingleTop = true + restoreState = true + builder() +} + +fun NavController.navigatePopUpTo( + route: String +) = navigateSingleTopTo( + route = route +) { + popUpTo( + id = currentDestination?.parent?.id ?: graph.findStartDestination().id + ) { + saveState = true + inclusive = true + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/com/dergoogler/mmrl/ui/utils/WindowInsetsExt.kt b/app/src/main/kotlin/com/dergoogler/mmrl/ui/utils/WindowInsetsExt.kt new file mode 100644 index 00000000..cc646f54 --- /dev/null +++ b/app/src/main/kotlin/com/dergoogler/mmrl/ui/utils/WindowInsetsExt.kt @@ -0,0 +1,5 @@ +package com.dergoogler.mmrl.ui.utils + +import androidx.compose.foundation.layout.WindowInsets + +val WindowInsets.Companion.none get() = WindowInsets(0, 0, 0, 0) \ No newline at end of file diff --git a/app/src/main/kotlin/com/dergoogler/mmrl/utils/StringListTypeConverter.kt b/app/src/main/kotlin/com/dergoogler/mmrl/utils/StringListTypeConverter.kt new file mode 100644 index 00000000..89b4de28 --- /dev/null +++ b/app/src/main/kotlin/com/dergoogler/mmrl/utils/StringListTypeConverter.kt @@ -0,0 +1,22 @@ +package com.dergoogler.mmrl.utils + +import androidx.room.TypeConverter +import com.squareup.moshi.JsonAdapter +import com.squareup.moshi.Moshi +import com.squareup.moshi.Types + +class StringListTypeConverter { + private val moshi = Moshi.Builder().build() + private val type = Types.newParameterizedType(List::class.java, String::class.java) + private val adapter: JsonAdapter> = moshi.adapter(type) + + @TypeConverter + fun fromStringList(value: List?): String { + return adapter.toJson(value) + } + + @TypeConverter + fun toStringList(value: String?): List { + return value?.let { adapter.fromJson(it) } ?: emptyList() + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/com/dergoogler/mmrl/utils/Utils.kt b/app/src/main/kotlin/com/dergoogler/mmrl/utils/Utils.kt new file mode 100644 index 00000000..16039011 --- /dev/null +++ b/app/src/main/kotlin/com/dergoogler/mmrl/utils/Utils.kt @@ -0,0 +1,20 @@ +package com.dergoogler.mmrl.utils + +object Utils { + fun getVersionDisplay(version: String, versionCode: Int): String { + val included = "\\(.*?${versionCode}.*?\\)".toRegex() + .containsMatchIn(version) + + return if (included) { + version + } else { + "$version (${versionCode})" + } + } + + fun getFilename(name: String, version: String, versionCode: Int, extension: String): String { + val versionNew = version.replace("\\([^)]*\\)".toRegex(), "") + return "${name}-${versionNew}-${versionCode}.${extension}" + .replace("[\\\\/:*?\"<>|]".toRegex(), "") + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/com/dergoogler/mmrl/utils/extensions/ContextExt.kt b/app/src/main/kotlin/com/dergoogler/mmrl/utils/extensions/ContextExt.kt new file mode 100644 index 00000000..0edbd950 --- /dev/null +++ b/app/src/main/kotlin/com/dergoogler/mmrl/utils/extensions/ContextExt.kt @@ -0,0 +1,23 @@ +package com.dergoogler.mmrl.utils.extensions + +import android.content.Context +import android.content.Intent +import androidx.core.app.ShareCompat + +val Context.tmpDir get() = cacheDir.resolve("tmp") + .apply { + if (!exists()) mkdirs() + } + +fun Context.openUrl(url: String) { + Intent.parseUri(url, Intent.URI_INTENT_SCHEME).apply { + startActivity(this) + } +} + +fun Context.shareText(text: String) { + ShareCompat.IntentBuilder(this) + .setType("text/plain") + .setText(text) + .startChooser() +} \ No newline at end of file diff --git a/app/src/main/kotlin/com/dergoogler/mmrl/utils/extensions/ListExt.kt b/app/src/main/kotlin/com/dergoogler/mmrl/utils/extensions/ListExt.kt new file mode 100644 index 00000000..26ac9321 --- /dev/null +++ b/app/src/main/kotlin/com/dergoogler/mmrl/utils/extensions/ListExt.kt @@ -0,0 +1,7 @@ +package com.dergoogler.mmrl.utils.extensions + +inline fun List>.merge(): List { + val values = mutableListOf() + forEach { values.addAll(it) } + return values +} \ No newline at end of file diff --git a/app/src/main/kotlin/com/dergoogler/mmrl/utils/extensions/LocalDateTimeExt.kt b/app/src/main/kotlin/com/dergoogler/mmrl/utils/extensions/LocalDateTimeExt.kt new file mode 100644 index 00000000..841b7cc5 --- /dev/null +++ b/app/src/main/kotlin/com/dergoogler/mmrl/utils/extensions/LocalDateTimeExt.kt @@ -0,0 +1,50 @@ +package com.dergoogler.mmrl.utils.extensions + +import kotlinx.datetime.Clock +import kotlinx.datetime.Instant +import kotlinx.datetime.LocalDateTime +import kotlinx.datetime.TimeZone +import kotlinx.datetime.format +import kotlinx.datetime.toLocalDateTime +import java.time.format.DateTimeFormatter + +fun Float.toDateTime(): String { + val instant = Instant.fromEpochMilliseconds(times(1000).toLong()) + return instant.toLocalDateTime(TimeZone.currentSystemDefault()).toString() +} + +fun Float.toDate(): String { + val instant = Instant.fromEpochMilliseconds(times(1000).toLong()) + return instant.toLocalDateTime(TimeZone.currentSystemDefault()).date.toString() +} + +fun Long.toDateTime(): String { + val instant = Instant.fromEpochMilliseconds(this) + return instant.toLocalDateTime(TimeZone.currentSystemDefault()).toString() +} + +fun Long.toDate(): String { + val instant = Instant.fromEpochMilliseconds(this) + return instant.toLocalDateTime(TimeZone.currentSystemDefault()).date.toString() +} + +fun Float.toFormattedDate(): String { + val instant = Instant.fromEpochMilliseconds((this * 1000).toLong()) + val localDateTime = instant.toLocalDateTime(TimeZone.currentSystemDefault()) + return formatDate(localDateTime) +} + +fun Long.toFormattedDate(): String { + val instant = Instant.fromEpochMilliseconds(this) + val localDateTime = instant.toLocalDateTime(TimeZone.currentSystemDefault()) + return formatDate(localDateTime) +} + +private fun formatDate(localDateTime: LocalDateTime): String { + val month = localDateTime.month.name.lowercase().replaceFirstChar { it.uppercase() } + val day = localDateTime.dayOfMonth + val year = localDateTime.year + return "$month $day, $year" +} + +fun LocalDateTime.Companion.now() = Clock.System.now().toLocalDateTime(TimeZone.currentSystemDefault()) \ No newline at end of file diff --git a/app/src/main/kotlin/com/dergoogler/mmrl/utils/extensions/ObjectExt.kt b/app/src/main/kotlin/com/dergoogler/mmrl/utils/extensions/ObjectExt.kt new file mode 100644 index 00000000..d6b281f5 --- /dev/null +++ b/app/src/main/kotlin/com/dergoogler/mmrl/utils/extensions/ObjectExt.kt @@ -0,0 +1,14 @@ +package com.dergoogler.mmrl.utils.extensions +import kotlin.reflect.full.memberProperties + +inline fun T.isObjectEmpty(): Boolean { + return this::class.memberProperties.all { prop -> + when (val value = prop.call(this)) { + null -> true + is String -> value.isEmpty() + is Collection<*> -> value.isEmpty() + is Map<*, *> -> value.isEmpty() + else -> false + } + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/com/dergoogler/mmrl/utils/extensions/ParcelableExt.kt b/app/src/main/kotlin/com/dergoogler/mmrl/utils/extensions/ParcelableExt.kt new file mode 100644 index 00000000..351e9520 --- /dev/null +++ b/app/src/main/kotlin/com/dergoogler/mmrl/utils/extensions/ParcelableExt.kt @@ -0,0 +1,15 @@ +package com.dergoogler.mmrl.utils.extensions + +import android.content.Intent +import android.os.Bundle +import android.os.Parcelable +import androidx.core.content.IntentCompat +import androidx.core.os.BundleCompat + +inline fun Intent.parcelable( + key: String +): T? = IntentCompat.getParcelableExtra(this, key, T::class.java) + +inline fun Bundle.parcelable( + key: String +): T? = BundleCompat.getParcelable(this, key, T::class.java) \ No newline at end of file diff --git a/app/src/main/kotlin/com/dergoogler/mmrl/utils/timber/DebugTree.kt b/app/src/main/kotlin/com/dergoogler/mmrl/utils/timber/DebugTree.kt new file mode 100644 index 00000000..0cee482e --- /dev/null +++ b/app/src/main/kotlin/com/dergoogler/mmrl/utils/timber/DebugTree.kt @@ -0,0 +1,13 @@ +package com.dergoogler.mmrl.utils.timber + +import timber.log.Timber + +class DebugTree : Timber.DebugTree() { + override fun log(priority: Int, tag: String?, message: String, t: Throwable?) { + super.log(priority, "$tag", message, t) + } + + override fun createStackElementTag(element: StackTraceElement): String { + return super.createStackElementTag(element) + "(L${element.lineNumber})" + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/com/dergoogler/mmrl/utils/timber/ReleaseTree.kt b/app/src/main/kotlin/com/dergoogler/mmrl/utils/timber/ReleaseTree.kt new file mode 100644 index 00000000..e0582f8e --- /dev/null +++ b/app/src/main/kotlin/com/dergoogler/mmrl/utils/timber/ReleaseTree.kt @@ -0,0 +1,9 @@ +package com.dergoogler.mmrl.utils.timber + +import timber.log.Timber + +class ReleaseTree : Timber.DebugTree() { + override fun log(priority: Int, tag: String?, message: String, t: Throwable?) { + super.log(priority, "$tag", message, t) + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/com/dergoogler/mmrl/viewmodel/InstallViewModel.kt b/app/src/main/kotlin/com/dergoogler/mmrl/viewmodel/InstallViewModel.kt new file mode 100644 index 00000000..e5e20f36 --- /dev/null +++ b/app/src/main/kotlin/com/dergoogler/mmrl/viewmodel/InstallViewModel.kt @@ -0,0 +1,155 @@ +package com.dergoogler.mmrl.viewmodel + +import android.content.Context +import android.net.Uri +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 com.dergoogler.mmrl.Compat +import com.dergoogler.mmrl.app.Event +import com.dergoogler.mmrl.compat.MediaStoreCompat.copyToDir +import com.dergoogler.mmrl.compat.MediaStoreCompat.getPathForUri +import com.dergoogler.mmrl.model.local.LocalModule +import com.dergoogler.mmrl.repository.LocalRepository +import com.dergoogler.mmrl.repository.UserPreferencesRepository +import com.dergoogler.mmrl.utils.extensions.tmpDir +import dagger.hilt.android.lifecycle.HiltViewModel +import dev.dergoogler.mmrl.compat.content.State +import dev.dergoogler.mmrl.compat.delegate.PowerManagerDelegate +import dev.dergoogler.mmrl.compat.stub.IInstallCallback +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import timber.log.Timber +import java.io.File +import java.time.LocalDateTime +import javax.inject.Inject + +@HiltViewModel +class InstallViewModel @Inject constructor( + private val localRepository: LocalRepository, + private val userPreferencesRepository: UserPreferencesRepository, +) : ViewModel() { + val logs = mutableListOf() + val console = mutableStateListOf() + var event by mutableStateOf(Event.LOADING) + private set + + val logfile get() = "Install_${LocalDateTime.now()}.log" + + init { + Timber.d("InstallViewModel init") + } + + fun reboot() { + PowerManagerDelegate(Compat.powerManager).apply { + reboot() + } + } + + suspend fun writeLogsTo(context: Context, uri: Uri) = withContext(Dispatchers.IO) { + runCatching { + val cr = context.contentResolver + cr.openOutputStream(uri)?.use { + it.write(logs.joinToString(separator = "\n").toByteArray()) + } + }.onFailure { + Timber.d(it) + } + } + + suspend fun loadModule(context: Context, uri: Uri) = withContext(Dispatchers.IO) { + val userPreferences = userPreferencesRepository.data.first() + + if (!Compat.init(userPreferences.workingMode)) { + event = Event.FAILED + console.add("- Service is not available") + return@withContext + } + + val path = context.getPathForUri(uri) + Timber.d("path = $path") + + Compat.moduleManager + .getModuleInfo(path)?.let { + Timber.d("module = $it") + install(path) + + return@withContext + } + + console.add("- Copying zip to temp directory") + val tmpFile = context.copyToDir(uri, context.tmpDir) + val cr = context.contentResolver + cr.openInputStream(uri)?.use { input -> + tmpFile.outputStream().use { output -> + input.copyTo(output) + } + } + + Compat.moduleManager + .getModuleInfo(tmpFile.path)?.let { + Timber.d("module = $it") + install(tmpFile.path) + + return@withContext + } + + event = Event.FAILED + console.add("- Zip parsing failed") + } + + private suspend fun install(zipPath: String) = withContext(Dispatchers.IO) { + val zipFile = File(zipPath) + val deleteZipFile = userPreferencesRepository + .data.first().deleteZipFile + + val callback = object : IInstallCallback.Stub() { + override fun onStdout(msg: String) { + console.add(msg) + logs.add(msg) + } + + override fun onStderr(msg: String) { + logs.add(msg) + } + + override fun onSuccess(module: LocalModule?) { + event = Event.SUCCEEDED + module?.let(::insertLocal) + + if (deleteZipFile) { + deleteBySu(zipPath) + } + } + override fun onFailure() { + event = Event.FAILED + } + } + + console.add("- Installing ${zipFile.name}") + Compat.moduleManager.install(zipPath, callback) + } + + private fun insertLocal(module: LocalModule) { + viewModelScope.launch { + localRepository.insertLocal( + module.copy(state = State.UPDATE) + ) + } + } + + private fun deleteBySu(zipPath: String) { + runCatching { + Compat.fileManager.deleteOnExit(zipPath) + }.onFailure { + Timber.e(it) + }.onSuccess { + Timber.d("deleteOnExit: $it") + } + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/com/dergoogler/mmrl/viewmodel/ModuleViewModel.kt b/app/src/main/kotlin/com/dergoogler/mmrl/viewmodel/ModuleViewModel.kt new file mode 100644 index 00000000..f7aab525 --- /dev/null +++ b/app/src/main/kotlin/com/dergoogler/mmrl/viewmodel/ModuleViewModel.kt @@ -0,0 +1,179 @@ +package com.dergoogler.mmrl.viewmodel + +import android.content.Context +import androidx.compose.runtime.Composable +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateListOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue +import androidx.lifecycle.SavedStateHandle +import androidx.lifecycle.ViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import androidx.lifecycle.viewModelScope +import com.dergoogler.mmrl.Compat +import com.dergoogler.mmrl.database.entity.Repo +import com.dergoogler.mmrl.database.entity.Repo.Companion.toRepo +import com.dergoogler.mmrl.model.json.UpdateJson +import com.dergoogler.mmrl.model.local.LocalModule +import com.dergoogler.mmrl.model.online.OnlineModule +import com.dergoogler.mmrl.model.online.TrackJson +import com.dergoogler.mmrl.model.online.VersionItem +import com.dergoogler.mmrl.repository.LocalRepository +import com.dergoogler.mmrl.repository.UserPreferencesRepository +import com.dergoogler.mmrl.service.DownloadService +import com.dergoogler.mmrl.ui.navigation.graphs.RepositoryScreen +import com.dergoogler.mmrl.utils.Utils +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.launch +import timber.log.Timber +import java.io.File +import javax.inject.Inject + +@HiltViewModel +class ModuleViewModel @Inject constructor( + private val localRepository: LocalRepository, + private val userPreferencesRepository: UserPreferencesRepository, + savedStateHandle: SavedStateHandle +) : ViewModel() { + val isProviderAlive get() = Compat.isAlive + + + val version: String + get() = Compat.get("") { + with(moduleManager) { version } + } + + private val moduleId = getModuleId(savedStateHandle) + var online: OnlineModule by mutableStateOf(OnlineModule.example()) + private set + val lastVersionItem by derivedStateOf { + versions.firstOrNull()?.second + } + + val isEmptyAbout get() = online.homepage.orEmpty().isBlank() + && online.track.source.isBlank() + && online.support.orEmpty().isBlank() + + val isEmptyReadme get() = !online.hasReadme + val readme get() = online.readme.orEmpty() + var local: LocalModule? by mutableStateOf(null) + private set + + private val installed get() = local?.let { it.author == online.author } ?: false + var notifyUpdates by mutableStateOf(false) + private set + + val localVersionCode get() = + if (notifyUpdates && installed) local!!.versionCode else Int.MAX_VALUE + val updatableSize by derivedStateOf { + versions.count { it.second.versionCode > localVersionCode } + } + + val versions = mutableStateListOf>() + val tracks = mutableStateListOf>() + + init { + Timber.d("ModuleViewModel init: $moduleId") + loadData() + } + + private fun loadData() = viewModelScope.launch { + localRepository.getOnlineAllById(moduleId).first().let { + online = it + } + + localRepository.getLocalByIdOrNull(moduleId)?.let { + local = it + notifyUpdates = localRepository.hasUpdatableTag(moduleId) + } + + localRepository.getVersionById(moduleId).forEach { + val repo = localRepository.getRepoByUrl(it.repoUrl) + + val item = repo to it + val track = repo to localRepository.getOnlineByIdAndUrl( + id = online.id, + repoUrl = it.repoUrl + ).track + + versions.add(item) + if (track !in tracks) tracks.add(track) + } + + if (installed) { + UpdateJson.loadToVersionItem(local!!.updateJson)?.let { + versions.add(0, "Update Json".toRepo() to it) + } + } + } + + fun setUpdatesTag(updatable: Boolean) { + viewModelScope.launch { + notifyUpdates = updatable + localRepository.insertUpdatableTag(moduleId, updatable) + } + } + + fun downloader( + context: Context, + item: VersionItem, + onSuccess: (File) -> Unit + ) { + viewModelScope.launch { + val downloadPath = userPreferencesRepository.data + .first().downloadPath + + val filename = Utils.getFilename( + name = online.name, + version = item.version, + versionCode = item.versionCode, + extension = "zip" + ) + + val task = DownloadService.TaskItem( + key = item.toString(), + url = item.zipUrl, + filename = filename, + title = online.name, + desc = item.versionDisplay + ) + + val listener = object : DownloadService.IDownloadListener { + override fun getProgress(value: Float) {} + override fun onSuccess() { + onSuccess(downloadPath.resolve(filename)) + } + + override fun onFailure(e: Throwable) { + Timber.d(e) + } + } + + DownloadService.start( + context = context, + task = task, + listener = listener + ) + } + } + + @Composable + fun getProgress(item: VersionItem): Float { + val progress by DownloadService.getProgressByKey(item.toString()) + .collectAsStateWithLifecycle(initialValue = 0f) + + return progress + } + + companion object { + fun putModuleId(module: OnlineModule) = + RepositoryScreen.View.route.replace( + "{moduleId}", module.id + ) + + fun getModuleId(savedStateHandle: SavedStateHandle): String = + checkNotNull(savedStateHandle["moduleId"]) + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/com/dergoogler/mmrl/viewmodel/ModulesViewModel.kt b/app/src/main/kotlin/com/dergoogler/mmrl/viewmodel/ModulesViewModel.kt new file mode 100644 index 00000000..2e8f4b9b --- /dev/null +++ b/app/src/main/kotlin/com/dergoogler/mmrl/viewmodel/ModulesViewModel.kt @@ -0,0 +1,310 @@ +package com.dergoogler.mmrl.viewmodel + +import android.content.Context +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateListOf +import androidx.compose.runtime.mutableStateMapOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.lifecycle.ViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import androidx.lifecycle.viewModelScope +import com.dergoogler.mmrl.Compat +import com.dergoogler.mmrl.datastore.modules.ModulesMenuCompat +import com.dergoogler.mmrl.datastore.repository.Option +import com.dergoogler.mmrl.model.json.UpdateJson +import com.dergoogler.mmrl.model.local.LocalModule +import com.dergoogler.mmrl.model.local.State +import com.dergoogler.mmrl.model.online.VersionItem +import com.dergoogler.mmrl.repository.LocalRepository +import com.dergoogler.mmrl.repository.ModulesRepository +import com.dergoogler.mmrl.repository.UserPreferencesRepository +import com.dergoogler.mmrl.service.DownloadService +import com.dergoogler.mmrl.utils.Utils +import dagger.hilt.android.lifecycle.HiltViewModel +import dev.dergoogler.mmrl.compat.stub.IModuleOpsCallback +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.launch +import timber.log.Timber +import java.io.File +import javax.inject.Inject + +@HiltViewModel +class ModulesViewModel @Inject constructor( + private val localRepository: LocalRepository, + private val modulesRepository: ModulesRepository, + private val userPreferencesRepository: UserPreferencesRepository, +) : ViewModel() { + val isProviderAlive get() = Compat.isAlive + + private val modulesMenu get() = userPreferencesRepository.data + .map { it.modulesMenu } + + var isSearch by mutableStateOf(false) + private set + private val keyFlow = MutableStateFlow("") + + private val cacheFlow = MutableStateFlow(listOf()) + private val localFlow = MutableStateFlow(listOf()) + val local get() = localFlow.asStateFlow() + + var isLoading by mutableStateOf(true) + private set + + private val versionItemCache = mutableStateMapOf() + + private val opsTasks = mutableStateListOf() + private val opsCallback = object : IModuleOpsCallback.Stub() { + override fun onSuccess(id: String) { + viewModelScope.launch { + modulesRepository.getLocal(id) + opsTasks.remove(id) + } + } + + override fun onFailure(id: String, msg: String?) { + opsTasks.remove(id) + Timber.w("$id: $msg") + } + } + + init { + Timber.d("ModulesViewModel init") + providerObserver() + dataObserver() + keyObserver() + } + + private fun providerObserver() { + Compat.isAliveFlow + .onEach { + if (it) getLocalAll() + + }.launchIn(viewModelScope) + } + + private fun dataObserver() { + combine( + localRepository.getLocalAllAsFlow(), + modulesMenu + ) { list, menu -> + if (list.isEmpty()) return@combine + + cacheFlow.value = list.sortedWith( + comparator(menu.option, menu.descending) + ).let { v -> + if (menu.pinEnabled) { + v.sortedByDescending { it.state == State.ENABLE } + } else { + v + } + } + + isLoading = false + + }.launchIn(viewModelScope) + } + + private fun keyObserver() { + combine( + keyFlow, + cacheFlow + ) { key, source -> + localFlow.value = source + .filter { + if (key.isNotBlank()) { + it.name.contains(key, ignoreCase = true) + || it.author.contains(key, ignoreCase = true) + || it.description.contains(key, ignoreCase = true) + } else { + true + } + } + + }.launchIn(viewModelScope) + } + + private fun comparator( + option: Option, + descending: Boolean + ): Comparator = if (descending) { + when (option) { + Option.NAME -> compareByDescending { it.name.lowercase() } + Option.UPDATED_TIME -> compareBy { it.lastUpdated } + else -> compareByDescending { null } + } + + } else { + when (option) { + Option.NAME -> compareBy { it.name.lowercase() } + Option.UPDATED_TIME -> compareByDescending { it.lastUpdated } + else -> compareByDescending { null } + } + } + + fun search(key: String) { + keyFlow.value = key + } + + fun openSearch() { + isSearch = true + } + + fun closeSearch() { + isSearch = false + keyFlow.value = "" + } + + private fun getLocalAll() { + viewModelScope.launch { + modulesRepository.getLocalAll() + } + } + + fun setModulesMenu(value: ModulesMenuCompat) { + viewModelScope.launch { + userPreferencesRepository.setModulesMenu(value) + } + } + + fun createModuleOps(module: LocalModule) = when (module.state) { + State.ENABLE -> ModuleOps( + isOpsRunning = opsTasks.contains(module.id), + toggle = { + opsTasks.add(module.id) + Compat.moduleManager + .disable(module.id, opsCallback) + }, + change = { + opsTasks.add(module.id) + Compat.moduleManager + .remove(module.id, opsCallback) + } + ) + + State.DISABLE -> ModuleOps( + isOpsRunning = opsTasks.contains(module.id), + toggle = { + opsTasks.add(module.id) + Compat.moduleManager + .enable(module.id, opsCallback) + }, + change = { + opsTasks.add(module.id) + Compat.moduleManager + .remove(module.id, opsCallback) + } + ) + + State.REMOVE -> ModuleOps( + isOpsRunning = opsTasks.contains(module.id), + toggle = {}, + change = { + opsTasks.add(module.id) + Compat.moduleManager + .enable(module.id, opsCallback) + } + ) + + State.UPDATE -> ModuleOps( + isOpsRunning = opsTasks.contains(module.id), + toggle = {}, + change = {} + ) + } + + @Composable + fun getVersionItem(module: LocalModule): VersionItem? { + val item by remember { + derivedStateOf { versionItemCache[module.id] } + } + + LaunchedEffect(key1 = module) { + if (!localRepository.hasUpdatableTag(module.id)) { + versionItemCache.remove(module.id) + return@LaunchedEffect + } + + if (versionItemCache.containsKey(module.id)) return@LaunchedEffect + + val versionItem = if (module.updateJson.isNotBlank()) { + UpdateJson.loadToVersionItem(module.updateJson) + } else { + localRepository.getVersionById(module.id) + .firstOrNull() + } + + versionItemCache[module.id] = versionItem + } + + return item + } + + fun downloader( + context: Context, + module: LocalModule, + item: VersionItem, + onSuccess: (File) -> Unit + ) { + viewModelScope.launch { + val downloadPath = userPreferencesRepository.data + .first().downloadPath + + val filename = Utils.getFilename( + name = module.name, + version = item.version, + versionCode = item.versionCode, + extension = "zip" + ) + + val task = DownloadService.TaskItem( + key = item.toString(), + url = item.zipUrl, + filename = filename, + title = module.name, + desc = item.versionDisplay + ) + + val listener = object : DownloadService.IDownloadListener { + override fun getProgress(value: Float) {} + override fun onSuccess() { + onSuccess(downloadPath.resolve(filename)) + } + + override fun onFailure(e: Throwable) { + Timber.d(e) + } + } + + DownloadService.start( + context = context, + task = task, + listener = listener + ) + } + } + + @Composable + fun getProgress(item: VersionItem?): Float { + val progress by DownloadService.getProgressByKey(item.toString()) + .collectAsStateWithLifecycle(initialValue = 0f) + + return progress + } + + data class ModuleOps( + val isOpsRunning: Boolean, + val toggle: (Boolean) -> Unit, + val change: () -> Unit + ) +} \ No newline at end of file diff --git a/app/src/main/kotlin/com/dergoogler/mmrl/viewmodel/RepositoriesViewModel.kt b/app/src/main/kotlin/com/dergoogler/mmrl/viewmodel/RepositoriesViewModel.kt new file mode 100644 index 00000000..de01fcbe --- /dev/null +++ b/app/src/main/kotlin/com/dergoogler/mmrl/viewmodel/RepositoriesViewModel.kt @@ -0,0 +1,89 @@ +package com.dergoogler.mmrl.viewmodel + +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.dergoogler.mmrl.database.entity.Repo.Companion.toRepo +import com.dergoogler.mmrl.model.state.RepoState +import com.dergoogler.mmrl.repository.LocalRepository +import com.dergoogler.mmrl.repository.ModulesRepository +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.launch +import timber.log.Timber +import javax.inject.Inject + +@HiltViewModel +class RepositoriesViewModel @Inject constructor( + private val localRepository: LocalRepository, + private val modulesRepository: ModulesRepository +) : ViewModel() { + private val reposFlow = MutableStateFlow(listOf()) + val repos get() = reposFlow.asStateFlow() + + var isLoading by mutableStateOf(true) + private set + var progress by mutableStateOf(false) + private set + private inline fun T.refreshing(callback: T.() -> Unit) { + progress = true + callback() + progress = false + } + + init { + Timber.d("RepositoriesViewModel init") + dataObserver() + } + + private fun dataObserver() { + localRepository.getRepoAllAsFlow() + .onEach { list -> + reposFlow.value = list.map { RepoState(it) } + .sortedBy { it.name } + + isLoading = false + + }.launchIn(viewModelScope) + } + + fun insert( + url: String, + onFailure: (Throwable) -> Unit + ) = viewModelScope.launch { + refreshing { + modulesRepository.getRepo(url.toRepo()) + .onFailure(onFailure) + } + } + + fun update(repo: RepoState) = viewModelScope.launch { + localRepository.insertRepo(repo.toRepo()) + } + + fun delete(repo: RepoState) = viewModelScope.launch { + localRepository.deleteRepo(repo.toRepo()) + localRepository.deleteOnlineByUrl(repo.url) + } + + fun getUpdate( + repo: RepoState, + onFailure: (Throwable) -> Unit + ) = viewModelScope.launch { + refreshing { + modulesRepository.getRepo(repo.toRepo()) + .onFailure(onFailure) + } + } + + fun getRepoAll() = viewModelScope.launch { + refreshing { + modulesRepository.getRepoAll(onlyEnable = false) + } + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/com/dergoogler/mmrl/viewmodel/RepositoryViewModel.kt b/app/src/main/kotlin/com/dergoogler/mmrl/viewmodel/RepositoryViewModel.kt new file mode 100644 index 00000000..6fac8937 --- /dev/null +++ b/app/src/main/kotlin/com/dergoogler/mmrl/viewmodel/RepositoryViewModel.kt @@ -0,0 +1,138 @@ +package com.dergoogler.mmrl.viewmodel + +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.dergoogler.mmrl.datastore.repository.Option +import com.dergoogler.mmrl.datastore.repository.RepositoryMenuCompat +import com.dergoogler.mmrl.model.online.OnlineModule +import com.dergoogler.mmrl.model.state.OnlineState +import com.dergoogler.mmrl.model.state.OnlineState.Companion.createState +import com.dergoogler.mmrl.repository.LocalRepository +import com.dergoogler.mmrl.repository.ModulesRepository +import com.dergoogler.mmrl.repository.UserPreferencesRepository +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.launch +import timber.log.Timber +import javax.inject.Inject + +@HiltViewModel +class RepositoryViewModel @Inject constructor( + private val localRepository: LocalRepository, + private val modulesRepository: ModulesRepository, + private val userPreferencesRepository: UserPreferencesRepository +) : ViewModel() { + private val repositoryMenu get() = userPreferencesRepository.data + .map { it.repositoryMenu } + + var isSearch by mutableStateOf(false) + private set + private val keyFlow = MutableStateFlow("") + + private val cacheFlow = MutableStateFlow(listOf>()) + private val onlineFlow = MutableStateFlow(listOf>()) + val online get() = onlineFlow.asStateFlow() + + var isLoading by mutableStateOf(true) + private set + + init { + Timber.d("RepositoryViewModel init") + dataObserver() + keyObserver() + } + + private fun dataObserver() { + combine( + localRepository.getOnlineAllAsFlow(), + repositoryMenu + ) { list, menu -> + cacheFlow.value = list.map { + it.createState( + local = localRepository.getLocalByIdOrNull(it.id), + hasUpdatableTag = localRepository.hasUpdatableTag(it.id) + ) to it + }.sortedWith( + comparator(menu.option, menu.descending) + ).let { v -> + val a = if (menu.pinInstalled) { + v.sortedByDescending { it.first.installed } + } else { + v + } + + if (menu.pinUpdatable) { + a.sortedByDescending { it.first.updatable } + } else { + a + } + } + + isLoading = false + + }.launchIn(viewModelScope) + } + + private fun keyObserver() { + combine( + keyFlow, + cacheFlow + ) { key, source -> + onlineFlow.value = source + .filter { (_, m) -> + if (key.isNotBlank()) { + m.name.contains(key, ignoreCase = true) + || m.author.contains(key, ignoreCase = true) + || m.description.contains(key, ignoreCase = true) + } else { + true + } + } + + }.launchIn(viewModelScope) + } + + private fun comparator( + option: Option, + descending: Boolean + ): Comparator> = if (descending) { + when (option) { + Option.NAME -> compareByDescending { it.second.name.lowercase() } + Option.UPDATED_TIME -> compareBy { it.first.lastUpdated } + else -> compareByDescending { null } + } + + } else { + when (option) { + Option.NAME -> compareBy { it.second.name.lowercase() } + Option.UPDATED_TIME -> compareByDescending { it.first.lastUpdated } + else -> compareByDescending { null } + } + } + + fun search(key: String) { + keyFlow.value = key + } + + fun openSearch() { + isSearch = true + } + + fun closeSearch() { + isSearch = false + keyFlow.value = "" + } + + fun setRepositoryMenu(value: RepositoryMenuCompat) { + viewModelScope.launch { + userPreferencesRepository.setRepositoryMenu(value) + } + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/com/dergoogler/mmrl/viewmodel/SettingsViewModel.kt b/app/src/main/kotlin/com/dergoogler/mmrl/viewmodel/SettingsViewModel.kt new file mode 100644 index 00000000..5c93eed2 --- /dev/null +++ b/app/src/main/kotlin/com/dergoogler/mmrl/viewmodel/SettingsViewModel.kt @@ -0,0 +1,64 @@ +package com.dergoogler.mmrl.viewmodel + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.dergoogler.mmrl.Compat +import com.dergoogler.mmrl.datastore.DarkMode +import com.dergoogler.mmrl.datastore.WorkingMode +import com.dergoogler.mmrl.repository.UserPreferencesRepository +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.launch +import timber.log.Timber +import java.io.File +import javax.inject.Inject + +@HiltViewModel +class SettingsViewModel @Inject constructor( + private val userPreferencesRepository: UserPreferencesRepository +) : ViewModel() { + val isProviderAlive get() = Compat.isAlive + + val version get() = Compat.get("") { + with(moduleManager) { "$version (${versionCode})" } + } + + init { + Timber.d("SettingsViewModel init") + } + + fun setWorkingMode(value: WorkingMode) { + viewModelScope.launch { + userPreferencesRepository.setWorkingMode(value) + } + } + + fun setDarkTheme(value: DarkMode) { + viewModelScope.launch { + userPreferencesRepository.setDarkTheme(value) + } + } + + fun setThemeColor(value: Int) { + viewModelScope.launch { + userPreferencesRepository.setThemeColor(value) + } + } + + fun setDeleteZipFile(value: Boolean) { + viewModelScope.launch { + userPreferencesRepository.setDeleteZipFile(value) + } + } + + fun setUseDoh(value: Boolean) { + viewModelScope.launch { + userPreferencesRepository.setUseDoh(value) + } + } + + fun setDownloadPath(value: File) { + viewModelScope.launch { + userPreferencesRepository.setDownloadPath(value) + } + } +} \ No newline at end of file diff --git a/app/src/main/proto/DarkMode.proto b/app/src/main/proto/DarkMode.proto new file mode 100644 index 00000000..2dc65fa4 --- /dev/null +++ b/app/src/main/proto/DarkMode.proto @@ -0,0 +1,10 @@ +syntax = "proto3"; + +option java_package = "com.dergoogler.mmrl.datastore"; +option java_multiple_files = true; + +enum DarkMode { + FOLLOW_SYSTEM = 0; + ALWAYS_OFF = 1; + ALWAYS_ON = 2; +} \ No newline at end of file diff --git a/app/src/main/proto/UserPreferences.proto b/app/src/main/proto/UserPreferences.proto new file mode 100644 index 00000000..e9339f75 --- /dev/null +++ b/app/src/main/proto/UserPreferences.proto @@ -0,0 +1,20 @@ +syntax = "proto3"; + +import "WorkingMode.proto"; +import "DarkMode.proto"; +import "repository/RepositoryMenu.proto"; +import "modules/ModulesMenu.proto"; + +option java_package = "com.dergoogler.mmrl.datastore"; +option java_multiple_files = true; + +message UserPreferences { + WorkingMode workingMode = 1; + DarkMode darkMode = 2; + int32 themeColor = 3; + bool deleteZipFile = 4; + RepositoryMenu repositoryMenu = 5; + ModulesMenu modulesMenu = 6; + bool useDoh = 7; + string downloadPath = 8; +} \ No newline at end of file diff --git a/app/src/main/proto/WorkingMode.proto b/app/src/main/proto/WorkingMode.proto new file mode 100644 index 00000000..540e9792 --- /dev/null +++ b/app/src/main/proto/WorkingMode.proto @@ -0,0 +1,11 @@ +syntax = "proto3"; + +option java_package = "com.dergoogler.mmrl.datastore"; +option java_multiple_files = true; + +enum WorkingMode { + FIRST_SETUP = 0; + MODE_ROOT = 1; + MODE_NON_ROOT = 2; + MODE_SHIZUKU = 3; +} \ No newline at end of file diff --git a/app/src/main/proto/modules/ModulesMenu.proto b/app/src/main/proto/modules/ModulesMenu.proto new file mode 100644 index 00000000..e2957fbf --- /dev/null +++ b/app/src/main/proto/modules/ModulesMenu.proto @@ -0,0 +1,13 @@ +syntax = "proto3"; + +import "repository/RepositoryMenu.proto"; + +option java_package = "com.dergoogler.mmrl.datastore.modules"; +option java_multiple_files = true; + +message ModulesMenu { + Option option = 1; + bool descending = 2; + bool pinEnabled = 3; + bool showUpdatedTime = 4; +} \ No newline at end of file diff --git a/app/src/main/proto/repository/RepositoryMenu.proto b/app/src/main/proto/repository/RepositoryMenu.proto new file mode 100644 index 00000000..5853cded --- /dev/null +++ b/app/src/main/proto/repository/RepositoryMenu.proto @@ -0,0 +1,22 @@ +syntax = "proto3"; + +option java_package = "com.dergoogler.mmrl.datastore.repository"; +option java_multiple_files = true; + +enum Option { + NAME = 0; + UPDATED_TIME = 1; +} + +message RepositoryMenu { + Option option = 1; + bool descending = 2; + bool pinInstalled = 3; + bool pinUpdatable = 4; + bool showIcon = 5; + bool showLicense = 6; + bool showUpdatedTime = 7; + bool showCover = 8; + bool showVerified = 9; + bool showAntiFeatures = 10; +} \ No newline at end of file diff --git a/app/src/main/res/drawable/alert_circle_filled.xml b/app/src/main/res/drawable/alert_circle_filled.xml new file mode 100644 index 00000000..3e3c44a7 --- /dev/null +++ b/app/src/main/res/drawable/alert_circle_filled.xml @@ -0,0 +1,12 @@ + + + diff --git a/app/src/main/res/drawable/alert_triangle.xml b/app/src/main/res/drawable/alert_triangle.xml new file mode 100644 index 00000000..ae02f2e4 --- /dev/null +++ b/app/src/main/res/drawable/alert_triangle.xml @@ -0,0 +1,24 @@ + + + + + diff --git a/app/src/main/res/drawable/arrow_left.xml b/app/src/main/res/drawable/arrow_left.xml new file mode 100644 index 00000000..b5ebe182 --- /dev/null +++ b/app/src/main/res/drawable/arrow_left.xml @@ -0,0 +1,24 @@ + + + + + diff --git a/app/src/main/res/drawable/at.xml b/app/src/main/res/drawable/at.xml new file mode 100644 index 00000000..059c7ad3 --- /dev/null +++ b/app/src/main/res/drawable/at.xml @@ -0,0 +1,18 @@ + + + + diff --git a/app/src/main/res/drawable/award.xml b/app/src/main/res/drawable/award.xml new file mode 100644 index 00000000..1d81f0ee --- /dev/null +++ b/app/src/main/res/drawable/award.xml @@ -0,0 +1,24 @@ + + + + + diff --git a/app/src/main/res/drawable/box.xml b/app/src/main/res/drawable/box.xml new file mode 100644 index 00000000..108ea148 --- /dev/null +++ b/app/src/main/res/drawable/box.xml @@ -0,0 +1,30 @@ + + + + + + diff --git a/app/src/main/res/drawable/brand_cloudflare.xml b/app/src/main/res/drawable/brand_cloudflare.xml new file mode 100644 index 00000000..80a13a4f --- /dev/null +++ b/app/src/main/res/drawable/brand_cloudflare.xml @@ -0,0 +1,24 @@ + + + + + diff --git a/app/src/main/res/drawable/brand_git.xml b/app/src/main/res/drawable/brand_git.xml new file mode 100644 index 00000000..7bc91e82 --- /dev/null +++ b/app/src/main/res/drawable/brand_git.xml @@ -0,0 +1,48 @@ + + + + + + + + + diff --git a/app/src/main/res/drawable/brand_open_source.xml b/app/src/main/res/drawable/brand_open_source.xml new file mode 100644 index 00000000..5f1d8ea3 --- /dev/null +++ b/app/src/main/res/drawable/brand_open_source.xml @@ -0,0 +1,12 @@ + + + diff --git a/app/src/main/res/drawable/brightness_2.xml b/app/src/main/res/drawable/brightness_2.xml new file mode 100644 index 00000000..b28b1fd6 --- /dev/null +++ b/app/src/main/res/drawable/brightness_2.xml @@ -0,0 +1,18 @@ + + + + diff --git a/app/src/main/res/drawable/ci_label.xml b/app/src/main/res/drawable/ci_label.xml new file mode 100644 index 00000000..264b7f69 --- /dev/null +++ b/app/src/main/res/drawable/ci_label.xml @@ -0,0 +1,14 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/circle_check_filled.xml b/app/src/main/res/drawable/circle_check_filled.xml new file mode 100644 index 00000000..aee9c6dd --- /dev/null +++ b/app/src/main/res/drawable/circle_check_filled.xml @@ -0,0 +1,12 @@ + + + diff --git a/app/src/main/res/drawable/circle_x_filled.xml b/app/src/main/res/drawable/circle_x_filled.xml new file mode 100644 index 00000000..5d770b0d --- /dev/null +++ b/app/src/main/res/drawable/circle_x_filled.xml @@ -0,0 +1,12 @@ + + + diff --git a/app/src/main/res/drawable/cloud.xml b/app/src/main/res/drawable/cloud.xml new file mode 100644 index 00000000..a10360f0 --- /dev/null +++ b/app/src/main/res/drawable/cloud.xml @@ -0,0 +1,12 @@ + + + diff --git a/app/src/main/res/drawable/cloud_download.xml b/app/src/main/res/drawable/cloud_download.xml new file mode 100644 index 00000000..1f5e9063 --- /dev/null +++ b/app/src/main/res/drawable/cloud_download.xml @@ -0,0 +1,24 @@ + + + + + diff --git a/app/src/main/res/drawable/cloud_filled.xml b/app/src/main/res/drawable/cloud_filled.xml new file mode 100644 index 00000000..92adeed3 --- /dev/null +++ b/app/src/main/res/drawable/cloud_filled.xml @@ -0,0 +1,12 @@ + + + diff --git a/app/src/main/res/drawable/cloud_upload.xml b/app/src/main/res/drawable/cloud_upload.xml new file mode 100644 index 00000000..d42b41e7 --- /dev/null +++ b/app/src/main/res/drawable/cloud_upload.xml @@ -0,0 +1,24 @@ + + + + + diff --git a/app/src/main/res/drawable/code_asterix.xml b/app/src/main/res/drawable/code_asterix.xml new file mode 100644 index 00000000..685d1ebe --- /dev/null +++ b/app/src/main/res/drawable/code_asterix.xml @@ -0,0 +1,54 @@ + + + + + + + + + + diff --git a/app/src/main/res/drawable/color_swatch.xml b/app/src/main/res/drawable/color_swatch.xml new file mode 100644 index 00000000..9c9c498e --- /dev/null +++ b/app/src/main/res/drawable/color_swatch.xml @@ -0,0 +1,30 @@ + + + + + + diff --git a/app/src/main/res/drawable/components.xml b/app/src/main/res/drawable/components.xml new file mode 100644 index 00000000..7322d25c --- /dev/null +++ b/app/src/main/res/drawable/components.xml @@ -0,0 +1,30 @@ + + + + + + diff --git a/app/src/main/res/drawable/currency_dollar.xml b/app/src/main/res/drawable/currency_dollar.xml new file mode 100644 index 00000000..05a87a53 --- /dev/null +++ b/app/src/main/res/drawable/currency_dollar.xml @@ -0,0 +1,18 @@ + + + + diff --git a/app/src/main/res/drawable/device_floppy.xml b/app/src/main/res/drawable/device_floppy.xml new file mode 100644 index 00000000..ae634297 --- /dev/null +++ b/app/src/main/res/drawable/device_floppy.xml @@ -0,0 +1,24 @@ + + + + + diff --git a/app/src/main/res/drawable/device_mobile_down.xml b/app/src/main/res/drawable/device_mobile_down.xml new file mode 100644 index 00000000..cf0af306 --- /dev/null +++ b/app/src/main/res/drawable/device_mobile_down.xml @@ -0,0 +1,36 @@ + + + + + + + diff --git a/app/src/main/res/drawable/file_3d.xml b/app/src/main/res/drawable/file_3d.xml new file mode 100644 index 00000000..236d0847 --- /dev/null +++ b/app/src/main/res/drawable/file_3d.xml @@ -0,0 +1,36 @@ + + + + + + + diff --git a/app/src/main/res/drawable/file_certificate.xml b/app/src/main/res/drawable/file_certificate.xml new file mode 100644 index 00000000..548b0314 --- /dev/null +++ b/app/src/main/res/drawable/file_certificate.xml @@ -0,0 +1,30 @@ + + + + + + diff --git a/app/src/main/res/drawable/file_download.xml b/app/src/main/res/drawable/file_download.xml new file mode 100644 index 00000000..b88722ed --- /dev/null +++ b/app/src/main/res/drawable/file_download.xml @@ -0,0 +1,30 @@ + + + + + + diff --git a/app/src/main/res/drawable/file_type_zip.xml b/app/src/main/res/drawable/file_type_zip.xml new file mode 100644 index 00000000..0af13645 --- /dev/null +++ b/app/src/main/res/drawable/file_type_zip.xml @@ -0,0 +1,36 @@ + + + + + + + diff --git a/app/src/main/res/drawable/files.xml b/app/src/main/res/drawable/files.xml new file mode 100644 index 00000000..99079e9c --- /dev/null +++ b/app/src/main/res/drawable/files.xml @@ -0,0 +1,24 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/git_pull_request.xml b/app/src/main/res/drawable/git_pull_request.xml new file mode 100644 index 00000000..533730af --- /dev/null +++ b/app/src/main/res/drawable/git_pull_request.xml @@ -0,0 +1,42 @@ + + + + + + + + diff --git a/app/src/main/res/drawable/github.xml b/app/src/main/res/drawable/github.xml new file mode 100644 index 00000000..4d321852 --- /dev/null +++ b/app/src/main/res/drawable/github.xml @@ -0,0 +1,10 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/heart_handshake.xml b/app/src/main/res/drawable/heart_handshake.xml new file mode 100644 index 00000000..24309b34 --- /dev/null +++ b/app/src/main/res/drawable/heart_handshake.xml @@ -0,0 +1,30 @@ + + + + + + diff --git a/app/src/main/res/drawable/ic_cdv_splashscreen.xml b/app/src/main/res/drawable/ic_cdv_splashscreen.xml deleted file mode 100644 index 7bc4107d..00000000 --- a/app/src/main/res/drawable/ic_cdv_splashscreen.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/ic_launcher_foreground.xml b/app/src/main/res/drawable/ic_launcher_foreground.xml deleted file mode 100644 index b32be28e..00000000 --- a/app/src/main/res/drawable/ic_launcher_foreground.xml +++ /dev/null @@ -1,15 +0,0 @@ - - - - - diff --git a/app/src/main/res/drawable/info_circle_filled.xml b/app/src/main/res/drawable/info_circle_filled.xml new file mode 100644 index 00000000..e9435881 --- /dev/null +++ b/app/src/main/res/drawable/info_circle_filled.xml @@ -0,0 +1,12 @@ + + + diff --git a/app/src/main/res/drawable/keyframes.xml b/app/src/main/res/drawable/keyframes.xml new file mode 100644 index 00000000..3c880416 --- /dev/null +++ b/app/src/main/res/drawable/keyframes.xml @@ -0,0 +1,24 @@ + + + + + diff --git a/app/src/main/res/drawable/keyframes_filled.xml b/app/src/main/res/drawable/keyframes_filled.xml new file mode 100644 index 00000000..ee315969 --- /dev/null +++ b/app/src/main/res/drawable/keyframes_filled.xml @@ -0,0 +1,24 @@ + + + + + diff --git a/app/src/main/res/drawable/launcher_foreground.xml b/app/src/main/res/drawable/launcher_foreground.xml new file mode 100644 index 00000000..e74c8d92 --- /dev/null +++ b/app/src/main/res/drawable/launcher_foreground.xml @@ -0,0 +1,41 @@ + + + + + + + + + diff --git a/app/src/main/res/drawable/launcher_outline.xml b/app/src/main/res/drawable/launcher_outline.xml new file mode 100644 index 00000000..5d399534 --- /dev/null +++ b/app/src/main/res/drawable/launcher_outline.xml @@ -0,0 +1,36 @@ + + + + + + + diff --git a/app/src/main/res/drawable/launcher_splash.xml b/app/src/main/res/drawable/launcher_splash.xml new file mode 100644 index 00000000..87e333fd --- /dev/null +++ b/app/src/main/res/drawable/launcher_splash.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/menu_2.xml b/app/src/main/res/drawable/menu_2.xml new file mode 100644 index 00000000..a36825a6 --- /dev/null +++ b/app/src/main/res/drawable/menu_2.xml @@ -0,0 +1,24 @@ + + + + + diff --git a/app/src/main/res/drawable/moon_stars.xml b/app/src/main/res/drawable/moon_stars.xml new file mode 100644 index 00000000..3f788051 --- /dev/null +++ b/app/src/main/res/drawable/moon_stars.xml @@ -0,0 +1,24 @@ + + + + + diff --git a/app/src/main/res/drawable/package_import.xml b/app/src/main/res/drawable/package_import.xml new file mode 100644 index 00000000..922f2dfe --- /dev/null +++ b/app/src/main/res/drawable/package_import.xml @@ -0,0 +1,42 @@ + + + + + + + + diff --git a/app/src/main/res/drawable/pencil_plus.xml b/app/src/main/res/drawable/pencil_plus.xml new file mode 100644 index 00000000..07d2e9fd --- /dev/null +++ b/app/src/main/res/drawable/pencil_plus.xml @@ -0,0 +1,30 @@ + + + + + + diff --git a/app/src/main/res/drawable/refresh.xml b/app/src/main/res/drawable/refresh.xml new file mode 100644 index 00000000..1ef1e432 --- /dev/null +++ b/app/src/main/res/drawable/refresh.xml @@ -0,0 +1,18 @@ + + + + diff --git a/app/src/main/res/drawable/reload.xml b/app/src/main/res/drawable/reload.xml new file mode 100644 index 00000000..5c9ceeb0 --- /dev/null +++ b/app/src/main/res/drawable/reload.xml @@ -0,0 +1,18 @@ + + + + diff --git a/app/src/main/res/drawable/rosette_discount_check.xml b/app/src/main/res/drawable/rosette_discount_check.xml new file mode 100644 index 00000000..0e660c1e --- /dev/null +++ b/app/src/main/res/drawable/rosette_discount_check.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/rotate.xml b/app/src/main/res/drawable/rotate.xml new file mode 100644 index 00000000..29fd3240 --- /dev/null +++ b/app/src/main/res/drawable/rotate.xml @@ -0,0 +1,12 @@ + + + diff --git a/app/src/main/res/drawable/search.xml b/app/src/main/res/drawable/search.xml new file mode 100644 index 00000000..e680c0a3 --- /dev/null +++ b/app/src/main/res/drawable/search.xml @@ -0,0 +1,18 @@ + + + + diff --git a/app/src/main/res/drawable/settings.xml b/app/src/main/res/drawable/settings.xml new file mode 100644 index 00000000..a4312d50 --- /dev/null +++ b/app/src/main/res/drawable/settings.xml @@ -0,0 +1,18 @@ + + + + diff --git a/app/src/main/res/drawable/settings_filled.xml b/app/src/main/res/drawable/settings_filled.xml new file mode 100644 index 00000000..52bf8ebc --- /dev/null +++ b/app/src/main/res/drawable/settings_filled.xml @@ -0,0 +1,12 @@ + + + diff --git a/app/src/main/res/drawable/share.xml b/app/src/main/res/drawable/share.xml new file mode 100644 index 00000000..69808f18 --- /dev/null +++ b/app/src/main/res/drawable/share.xml @@ -0,0 +1,36 @@ + + + + + + + diff --git a/app/src/main/res/drawable/sun.xml b/app/src/main/res/drawable/sun.xml new file mode 100644 index 00000000..1a669cdd --- /dev/null +++ b/app/src/main/res/drawable/sun.xml @@ -0,0 +1,18 @@ + + + + diff --git a/app/src/main/res/drawable/tag.xml b/app/src/main/res/drawable/tag.xml new file mode 100644 index 00000000..6accc34a --- /dev/null +++ b/app/src/main/res/drawable/tag.xml @@ -0,0 +1,20 @@ + + + + diff --git a/app/src/main/res/drawable/target.xml b/app/src/main/res/drawable/target.xml new file mode 100644 index 00000000..ed43ff39 --- /dev/null +++ b/app/src/main/res/drawable/target.xml @@ -0,0 +1,24 @@ + + + + + diff --git a/app/src/main/res/drawable/target_off.xml b/app/src/main/res/drawable/target_off.xml new file mode 100644 index 00000000..7d6b90b7 --- /dev/null +++ b/app/src/main/res/drawable/target_off.xml @@ -0,0 +1,30 @@ + + + + + + diff --git a/app/src/main/res/drawable/telegram.xml b/app/src/main/res/drawable/telegram.xml new file mode 100644 index 00000000..1cb7dad5 --- /dev/null +++ b/app/src/main/res/drawable/telegram.xml @@ -0,0 +1,9 @@ + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/trash.xml b/app/src/main/res/drawable/trash.xml new file mode 100644 index 00000000..dfbb056d --- /dev/null +++ b/app/src/main/res/drawable/trash.xml @@ -0,0 +1,36 @@ + + + + + + + diff --git a/app/src/main/res/drawable/users.xml b/app/src/main/res/drawable/users.xml new file mode 100644 index 00000000..edb15f89 --- /dev/null +++ b/app/src/main/res/drawable/users.xml @@ -0,0 +1,30 @@ + + + + + + diff --git a/app/src/main/res/drawable/weblate.xml b/app/src/main/res/drawable/weblate.xml new file mode 100644 index 00000000..e66e62dd --- /dev/null +++ b/app/src/main/res/drawable/weblate.xml @@ -0,0 +1,55 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/world_www.xml b/app/src/main/res/drawable/world_www.xml new file mode 100644 index 00000000..1bd2e213 --- /dev/null +++ b/app/src/main/res/drawable/world_www.xml @@ -0,0 +1,60 @@ + + + + + + + + + + + diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml deleted file mode 100644 index e06b4638..00000000 --- a/app/src/main/res/layout/activity_main.xml +++ /dev/null @@ -1,14 +0,0 @@ - - - - - - diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml deleted file mode 100644 index 6f2acb4f..00000000 --- a/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - - - \ No newline at end of file diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml deleted file mode 100644 index 6f2acb4f..00000000 --- a/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - - - \ No newline at end of file diff --git a/app/src/main/res/mipmap-anydpi-v26/launcher.xml b/app/src/main/res/mipmap-anydpi-v26/launcher.xml new file mode 100644 index 00000000..ae4a4979 --- /dev/null +++ b/app/src/main/res/mipmap-anydpi-v26/launcher.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher.webp b/app/src/main/res/mipmap-hdpi/ic_launcher.webp deleted file mode 100644 index 3b6fcc57..00000000 Binary files a/app/src/main/res/mipmap-hdpi/ic_launcher.webp and /dev/null differ diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher_background.webp b/app/src/main/res/mipmap-hdpi/ic_launcher_background.webp deleted file mode 100644 index a683b1c6..00000000 Binary files a/app/src/main/res/mipmap-hdpi/ic_launcher_background.webp and /dev/null differ diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp deleted file mode 100644 index 09397ede..00000000 Binary files a/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp and /dev/null differ diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher.webp b/app/src/main/res/mipmap-mdpi/ic_launcher.webp deleted file mode 100644 index 3cd32cee..00000000 Binary files a/app/src/main/res/mipmap-mdpi/ic_launcher.webp and /dev/null differ diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher_background.webp b/app/src/main/res/mipmap-mdpi/ic_launcher_background.webp deleted file mode 100644 index 4cc27024..00000000 Binary files a/app/src/main/res/mipmap-mdpi/ic_launcher_background.webp and /dev/null differ diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp deleted file mode 100644 index fa07ce8a..00000000 Binary files a/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp and /dev/null differ diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xhdpi/ic_launcher.webp deleted file mode 100644 index 775a8887..00000000 Binary files a/app/src/main/res/mipmap-xhdpi/ic_launcher.webp and /dev/null differ diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher_background.webp b/app/src/main/res/mipmap-xhdpi/ic_launcher_background.webp deleted file mode 100644 index c9762298..00000000 Binary files a/app/src/main/res/mipmap-xhdpi/ic_launcher_background.webp and /dev/null differ diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp deleted file mode 100644 index 643537dc..00000000 Binary files a/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp and /dev/null differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp deleted file mode 100644 index 3765a169..00000000 Binary files a/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp and /dev/null differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher_background.webp b/app/src/main/res/mipmap-xxhdpi/ic_launcher_background.webp deleted file mode 100644 index e83ec9e9..00000000 Binary files a/app/src/main/res/mipmap-xxhdpi/ic_launcher_background.webp and /dev/null differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp deleted file mode 100644 index c5229218..00000000 Binary files a/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp and /dev/null differ diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp deleted file mode 100644 index ba9dba1c..00000000 Binary files a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp and /dev/null differ diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_background.webp b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_background.webp deleted file mode 100644 index b67ba904..00000000 Binary files a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_background.webp and /dev/null differ diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp deleted file mode 100644 index 52c2b0c8..00000000 Binary files a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp and /dev/null differ diff --git a/app/src/main/res/raw/bashrc b/app/src/main/res/raw/bashrc deleted file mode 100644 index 1f93f8a5..00000000 --- a/app/src/main/res/raw/bashrc +++ /dev/null @@ -1,3 +0,0 @@ -#!/system/bin/sh - -getProp() { sed -n "s|^$1=||p" ${2:-$2}; } \ No newline at end of file diff --git a/app/src/main/res/raw/overrides b/app/src/main/res/raw/overrides deleted file mode 100644 index 212c8a31..00000000 --- a/app/src/main/res/raw/overrides +++ /dev/null @@ -1,19 +0,0 @@ -window.open = (url) => { - nos.open(url); -}; - -window.close = () => { - nos.close(); -}; - -console.log = (tag, message) => { - nos.logi(tag, message); -}; - -console.warn = (tag, message) => { - nos.logw(tag, message); -}; - -console.error = (tag, message) => { - nos.loge(tag, message); -}; diff --git a/app/src/main/res/values-ar/strings.xml b/app/src/main/res/values-ar/strings.xml new file mode 100644 index 00000000..7735342f --- /dev/null +++ b/app/src/main/res/values-ar/strings.xml @@ -0,0 +1,103 @@ + + + تصريح الروت: %s + روت + بدون روت + تفليش + تم + فشل + عرض الترخيص + تراخيص معتمدة + الإضافات + المزود%s + غير متوفر + الوظائف محدودة + وضع العمل + الإعدادات + تم التصريح + غير متاح + %1$sبواسطة%2$s + FSF الحر + إزالة + استرجاع + تثبيت + تنزيل + لم يتم العثور على إضافات + المستودعات فارغة + بحث… + الإصدارات + مقدم بواسطة%s + سمة التطبيق + تطبيق + قائمة فارغة + مسار التنزيل + المستودعات + لوحة الموضوع + السمة الداكنة + تلقائي + مضيء + داكن + تحديث + حذف + "%d إضافات" + اضافة مستودع + اضافة + ملاحظة + إلغاء + حسناً + تم التنزيل بنجاح + انتظار… + خدمة التنزيل + خطاء غير معروف + تغيير الوضع الليلي ولوحة الألوان + إدارة مستودعات وإضافات Magisk + هل انت متأكد من حذف %s؟ + مثبته + صنع ❤️ بواسطة %s + وصف مختصر + حول + الوصف + محلي + الإصدار + إدارة الإضافات وتوفير مستودعات الإضافات النمطية لـMagisk وKernelSU. + احذف الملف المضغوط للوحدة بعد تثبيته بنجاح ، لاحظ أن هذا هو أفضل جهد ولن يعمل إذا لم يكن لدى التطبيق إذن بحذف هذا سيتم تنفيذه بواسطة صلاحية الروت + حذف الملف المضغوط + المستودع + هل تريد تثبيت او تنزيل هذا الإصدار من الوحدة؟ + اعدادات حول تطبيق MMRL نفسه + حول + الإصدار %1$s (%2$s) + عرض وقت الحديث + جديد + تدبيس القابلة لتحديث + سحابي + تدبيس المثبتة + عرض الايقونة + عرض الرخصة + الصفحة الرئيسية + الرمز المصدري + الدعم + عرض المسار + النوع: %s + مضاف في : %s + المحدثه مؤخرا + محدث في: %s + غير متوافق + القائمة المتقدمة + وضع الفرز + الاسم + وقت التحديث + تنازلي + تدبيس المفعلة + الإخطار + يتطلب أذونات الروت التي يوفرها Magisk أو KernelSU + لا يتطلب أذونات الروت، تتوفر وظيفة تنزيل الإضافة فقط + تثبيت + فشل حفظ: %s + يتطلب إذنًا مقدمًا من Sui أو Shizuku (روت) + تم حفظ السجلات + تجاهل + تحديث + DNS عبر HTTPS + الحل البديل لتسمم DNS في بعض الدول + \ No newline at end of file diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml new file mode 100644 index 00000000..241024d2 --- /dev/null +++ b/app/src/main/res/values-de/strings.xml @@ -0,0 +1,100 @@ + + + Root + Non-Root + Installiet + Fehlgeschlagen + Lizenz anschauen + OSI Zugelassen + FSF Libre + Fertig + Einstellungen + Installiert + Neu + Installierte anheften + Updatefähige anheften + Icon anzeigen + Lizenz anzeigen + Übersicht + Versionen + Über + Beschreibung + Letztes Update + Bereitgestellt von %s + Repository + Online + Version + Lokal + Quellcode + Keine Module gefunden + Löschen + Wiederherstellen + Installieren + Webseite + Unterstützen + Typ: %s + Hinzugefügt am: %s + %1$s von %2$s + Track ansehen + Einstellungen der MMRL App selbst + Magisk Modul Repositories anpassen + Über + Root Zugriff: %s + Gewährt + Nicht verfügbar + Keiner + Funktionalität eingeschränkt + App Design + Farben und Nacht-Modus ändern + Download Pfad + Zip Datei löschen + Herunterladen + App + Repositories + Anbieter: %s + Farbpaletten + Nachtmodus + Automatisch + Hell + Dunkel + %d Module + Aktualisiert am: %s + Inkompatibel + Löschen + Repository hinzufügen + Hinzufügen + Füge Modul Repositories hinzu und manage Module für Magisk und KernelSU. + Achtung + Abbrechen + Erweitertes Menü + Zuletzt geupdated + Absteigend + Aktivierte anheften + Zuletzt aktualisiert anzeigen + Update + Made with ❤️ by %s + OK + Name + Herunterladen erfolgreich + Lädt… + Download arbeitet + Leere Liste + Suche… + Unbekannter Fehler + Arbeitsmodus + Module + Repository ist leer + Möchtest du diese Version installieren oder herunterladen\? + Löscht Zip Dateien nach der erfolgreichen Installation + Willst du wirklich %s löschen\? + Sortierreihenfolge + Version %1$s (%2$s) + Installation + Logs gespeichert + Fehler beim Speichern: %s + DNS über HTTPS + Problemumgehung für DNS-Poisoning in einigen Ländern + Benachrichtigen + Ignorieren + Update + \ No newline at end of file diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml new file mode 100644 index 00000000..b3685d20 --- /dev/null +++ b/app/src/main/res/values-es/strings.xml @@ -0,0 +1,103 @@ + + + Cambia la combinación de colores y el modo nocturno + %1$s por %2$s + Eliminar + Restaurar + Instalar + Repositorio está vacío + No se han encontrado módulos + Lista vacía + Buscar… + Versiones + Proporcionado por %s + Temas de la App + App + Ruta de descarga + Repositorio + Gestionar los repositorios de módulos Magisk + Descargar + Root + Sin Root + Flasheando + Hecho + Fallida + Ver licencia + Aprobado por OSI + FSF Libre + Módulos + Configuración + Acceso Root: %s + Proveedor: %s + Concedido + No disponible + Ninguno + Modo de trabajo + La funcionalidad es limitada + Colores del fondo + Tema oscuro + Automático + Claro + Oscuro + %d módulos + Actualizar + Eliminar + ¿Estás seguro de eliminarlo %s\? + Agregar repositorio + Agregar + Atención + Cancelar + OK + Descarga con éxito + Cargando… + Servicio de Descargas + Error desconocido + Última actualización + Local + ¿Desea instalar o descargar esta versión del módulo\? + Página de inicio + Código fuente + Soporte + Ver origen + Tipo: %s + Añadido: %s + Acerca de… + Hecho con ❤️ por %s + Descendente + Encabezar activados + Mostrar tiempo de actualización + Configuraciones de MMRL + Incompatible + Versión %1$s (%2$s) + Mostrar icono + Mostrar licencia + Resumen + Instalado + Nuevo + Encabezar instalados + Encabezar actualizables + Acerca de… + Descripción + Nube + Versión + Eliminar archivo Zip + Eliminar archivo comprimido del módulo tras instalación exitosa - ejecutado por Root + Actualizado: %s + Agregar y administrar repositorios de módulos de Magisk y KernelSU. + Opciones avanzadas + Modo de sorteo + Nombre + Tiempo de actualización + Repositorio + DNS sobre HTTPS + Solución al envenenamiento de DNS en algunos países + Notifícame + Ignorar + Instalación + Registros guardados + Error al guardar: %s + Actualizar + Requiere permisos Root proporcionados por Magisk o KernelSU + Requiere permiso de Sui o Shizuku (root) + No requiere permisos Root, sólo está disponible la función de módulo de descarga + \ No newline at end of file diff --git a/app/src/main/res/values-fil/strings.xml b/app/src/main/res/values-fil/strings.xml new file mode 100644 index 00000000..a6b3daec --- /dev/null +++ b/app/src/main/res/values-fil/strings.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml new file mode 100644 index 00000000..eee82e9b --- /dev/null +++ b/app/src/main/res/values-fr/strings.xml @@ -0,0 +1,75 @@ + + + Traitement en cours + Raçine + Sans racine + Flashage + Terminé + Échec + Afficher Licence + OSI Approuvé + FSF Libre + Modules + Réglages + Accès Administrateur : %s + Fournisseur : %s + Permis + Non disponible + Aucun + Le fonctionnement est limité + %1$s par %2$s + Supprimer + Restaurer + Installer + Télécharger + Dépôt vide + Aucun module trouvé + Liste vide + Recherche… + Versions + Fournis par %s + Thème de l\’application + Changer le mode nuit et le thème platte + Application + Emplacement des téléchargements + Dépôt + Gérrer le dépôt des modules Magisk + Palette du thème + Thème sombre + Automatique + Clair + Noir + %d modules + Mise à jour + Supprimer + Êtes-vous sûr de supprimer %s \? + ajouter dépôt + Ajouter + Attention + Annuler + OK + téléchargement réussi + Chargement… + Service de téléchargement + Erreur inconnue + Supprimer le fichier zip + Supprimer le fichier zip une fois l\'installation réussie, notez que cela peut ne pas fonctionner si l\'application n\'a pas l\'autorisation de supprimer + Nouveau + Version + Local + Paramètres de MMRL + Dépôt + Installé + Épingler installé + Afficher icône + Afficher licence + Aperçu + À propos + Description + Voulez-vous télécharger ou installer cette version du module \? + Cloud + Page d\'accueil + Code source + Support + Ajouté à : %s + \ No newline at end of file diff --git a/app/src/main/res/values-hi/strings.xml b/app/src/main/res/values-hi/strings.xml new file mode 100644 index 00000000..9e58c468 --- /dev/null +++ b/app/src/main/res/values-hi/strings.xml @@ -0,0 +1,103 @@ + + + सम्पूर्ण + सावधान + उचित हैं + संलेख सहेजा गया + नया + ओएसआई से स्वीकृत + श्वेत + पिछली बार अद्यनित + क्रमिका का प्रकार + विवरण (अनुप्रयोग के बारे में) + अवरोही + संस्थापन + श्याम + कार्य प्रणाली + हमारे बारे में + अद्यतन का समय दिखाएँ + मूल + भाग + उन्नत सूची + स्थापित करें + %d भाग (माॅडयूल) + अद्यनित किया गया: %s + अद्यतन का समय + संस्करण %1$s (%2$s) + अधिभारण + स्वतः + संकेत दिखाएं + एफएसएफ स्वतंत्र + अनुज्ञप्ति देखें + विवरण + अभिधारण सफल + भाग (मॉड्यूल) कोष प्रदान करें और मैजिस्क एवं कर्नेलएसयू के लिए भाग (मॉड्यूल) प्रबंधित करें। + नाम + संस्करण + असफल + प्रगतिशील… + स्थानीय + अनुज्ञप्ति दर्शाएँ + श्याम रूप + सहेजने में विफल: %s + मूलरहित + \'मूल\' अनुमतियों की आवश्यकता नहीं, केवल \'भाग\' (माॅडयूल) अधिभारण क्रिया उपलब्ध + उपेक्षा करें + कोष + व्यवस्थापन + कोष रिक्त है + संस्थापित + पिन स्थापित + पिन अद्ययन योग्य + समीक्षा + %s द्वारा प्रदत्त + सिजुकु (मूल) अथवा \'सुइ\' द्वारा प्रदत्त अनुमतियों की आवश्यकता + स्थापित कर रहा है + हटाएं + संस्करण + दूरसंचारित + स्त्रोत लेख + सहायता + पथ देखे + प्रकार: %s + %s: पर योगित + सूचित + क्या आप इस भाग के इस संस्करण को अधिभारित अथवा संस्थापित करना चाहते हैं? + गृहस्थान + \'मेजिस्क\' अथवा \'कर्नल एस यू\' द्वारा \'मूल\' अनुमतियों की आवश्यकता + भाग अनुपलब्ध + %1$s के द्वारा %2$s + अद्यतन करें + अनुप्रयोग + स्वयं एमरेपो अनुप्रयोग का व्यवस्थापन + कोष + पुनः स्थापित करें + मूल अनुमति: %s + दाता: %s + प्रदित्त + अनुपलब्ध + मेजिस्क के भागों के कोषों को व्यवस्थित करें + \'एच टी टी पी एस\' के ऊपर \'डी एन एस\' + संस्थापन सफल होने के पश्चात संपीडित भाग (माॅडयूल) को हटाएं, यह उच्चतम शक्ति उपयोगकर्ता (सुपरयुजर) द्वारा संचालित होगा + नया मिलाएं + %s के द्वारा ❤️ से निर्मित + रद्द करें + अभिधारण पथ + रंगपट्टिका का रूप + अनुप्रयोग का रूप (विषय) + रात्रि स्थिति एवं रंगरूप बदले + संपीडित सञ्चिका हटाएं + कुछ राष्ट्रों में \'डी एन एस\' को उचित रूप से संचालित करने का उपाय + पिन सक्षम + सेवा अभिधारित करें + रिक्त सूची + खोज… + असंगत + हटाएं + रिक्त + अद्यतन + नया कोष मिलाएं + अज्ञात त्रुटि + कार्यक्षमता सीमित है + क्या आप वास्तव में %s हटाना चाहते हैं? + \ No newline at end of file diff --git a/app/src/main/res/values-in/strings.xml b/app/src/main/res/values-in/strings.xml new file mode 100644 index 00000000..b2884241 --- /dev/null +++ b/app/src/main/res/values-in/strings.xml @@ -0,0 +1,103 @@ + + + Hapus + Flashing + Repositori + Setelan seputar aplikasi MMRL ini sendiri + Mode Kerja + Root + Non-Root + Selesai + Gagal + Lihat Lisensi + Disetujui OSI + FSF Libre + Modul + Setelan + Repositori kosong + Terpasang + Deskripsi Umum + Versi + Mengenai + Deskripsi + Lokal + Versi + Disediakan oleh %s + Tidak ditemukan modul + %1$s oleh %2$s + Pulihkan + Pasang + Download + Repositori + Kelola repositori modul Magisk + App + Mengenai + Akses Root: %s + Penyedia: %s + Diberikan + Tidak tersedia + Tidak ada + Fungsionalitas terbatas + Tema Aplikasi + Mengubah skema warna dan mode malam + Jalur Download + Hapus File Zip + Hapus modul zip setelah pemasangan berhasil, ini akan dieksekusi dengan Superuser + Tema Gelap + Otomatis + Terang + Gelap + %d modul + Perbarui + Hapus + Apakah Anda yakin akan menghapus %s\? + Apakah Anda ingin memasang atau men-download modul versi ini\? + Palet Tema + Tambah repositori + Tambah + Versi %1$s (%2$s) + Menyediakan repositori modul dan Mengelola modul untuk Magisk dan KernelSU. + Dibuat dengan ❤️ oleh %s + Perhatian + Batal + Oke + Download berhasil + Memuat… + Layanan Download + Daftar kosong + Cari… + Kesalahan tidak diketahui + Cloud + Terakhir diperbarui + Homepage + Source Code + Dukungan + Lihat Track + Jenis: %s + Ditambahkan pada: %s + Tampilkan Ikon + Diperbarui pada: %s + Menu Lanjutan + Mode Urutkan + Nama + Menurun + Pin Yang Aktif + Pin Yg. Dapat Diperbarui + Baru + Pin Yang Terpasang + Tampilkan Lisensi + Tidak kompatibel + Waktu Terbaru + Tampilkan Waktu Pembaruan + Log tersimpan + Gagal untuk menyimpan: %s + Pemasangan + Perbarui + DNS melalui HTTPS + Mengatasi masalah DNS di beberapa negara + Abaikan + Beritahu + Membutuhkan izin Root yang diberikan oleh Magisk atau KernelSU + Membutuhkan izin yang diberikan oleh Sui atau Shizuku (root) + Tidak membutuhkan izin Root, hanya menyediakan fitur men-download modul + \ No newline at end of file diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml new file mode 100644 index 00000000..7f0ae28f --- /dev/null +++ b/app/src/main/res/values-it/strings.xml @@ -0,0 +1,103 @@ + + + Modalità di funzionamento + Root + Richiede i permessi di root forniti da Magisk o KernelSU + Richiede l\'autorizzazione fornita da Sui o Shizuku (root) + Non-Root + Installazione + Flashing + Fatto + Fallito + Log salvato + Visualizza licenza + Approvato da OSI + FSF Libre + Repository + Moduli + Impostazioni + Installato + Nuovo + Pin Installato + Pin Aggiornabile + Mostra Icona + Mostra Licenza + Panoramica + Versioni + Info + Descrizione + Cloud + Versione + Ultimo Aggiornamento + Locale + Ignora + Fornito da %s + Vuoi installare o scaricare questa versione del modulo? + Homepage + Codice Sorgente + Supporto + Visualizza Traccia + Nessun modulo trovato + Aggiunto il: %s + %1$s di %2$s + Rimuovi + Ripristina + Installa + Download + Aggiorna + App + Repository + Gestisci i repository dei moduli Magisk + Info + Accesso Root: %s + Fornitore: %s + Concesso + Non disponibile + Nessuno + Non richiede permessi di root, è disponibile solo la funzione per scaricare i moduli + Impossibile salvare: %s + La repository è vuota + Notifica + Tipo: %s + Impostazioni di MMRL + Tema App + Percorso per i Download + Elimina File Zip + DNS over HTTPS + Soluzione alternativa Avvelenamento DNS in alcune nazioni + Tavolozza dei Temi + Tema scuro + Automatico + Chiaro + Scuro + %d moduli + Aggiornato il : %s + Incompatibile + Aggiorna + Elimina + Sei sicuro di voler eliminare %s? + Aggiungi repository + Aggiungi + Versione %1$s (%2$s) + Realizzato con il ❤️ da %s + Attenzione + Cancella + OK + Menù Avanzato + Modalità di Ordinamento + Nome + Data di Aggiornamento + Discendente + Pin Abilitato + Mostra Data di Aggiornamento + Download riuscito + Caricamento… + Elenco vuoto + Cerca… + Errore Sconosciuto + La funzionalità è limitata + Elimina lo zip del modulo una volta completata l\'installazione, questa operazione verrà eseguita da Superuser + Fornire e gestire repository di moduli per Magisk e KernelSU. + Servizio di Download + Cambia la combinazione di colori e la modalità notturna + \ No newline at end of file diff --git a/app/src/main/res/values-ja/strings.xml b/app/src/main/res/values-ja/strings.xml new file mode 100644 index 00000000..9f1258fe --- /dev/null +++ b/app/src/main/res/values-ja/strings.xml @@ -0,0 +1,96 @@ + + + 不明なエラー + リポジトリの追加 + 削除 + リポジトリは空です + バージョン + 検索… + 設定 + 完了 + 失敗 + ライセンスを表示 + モジュール + 動作モード + Root + 非 Root + OSI Approved + FSF Libre + インストール + ダウンロード + モジュールが見つかりません + 削除 + 復元 + リポジトリ + 自動 + 注意 + 追加 + 機能が制限されています + ダウンロード場所 + ダークテーマ + ダーク + 更新 + ライト + ダウンロード成功 + 読み込み中… + カラースキームとナイトモードの変更 + Magisk モジュールリポジトリを管理 + テーマパレット + アプリテーマ + OK + キャンセル + ダウンロード + Zip ファイルを削除 + インストールが成功したらモジュールの Zip ファイルを削除します。これはスーパーユーザーで実行されます。 + Magisk、KernelSU のモジュール管理、モジュールリポジトリの提供。 + アプリ + MMRL アプリの設定 + リポジトリ + Root アクセス: %s + プロバイダー: %s + インストール済 + 概要 + 説明 + ローカル + バージョン + 本当に %s を削除しますか? + 通知 + 無視 + DNS over HTTPS + 一部の国での DNS ポイズニングを回避します + ログを保存しました + 保存に失敗しました: %s + インストール + 最終更新 + 新着 + 提供: %s + アイコンの表示 + ライセンスの表示 + クラウド + このバージョンのモジュールをインストールまたはダウンロードしますか? + 付与済み + 利用不可 + ホームページ + ソースコード + サポート + タイプ: %s + 追加日: %s + %1$s — %2$s + なし + このアプリについて + %d モジュール + 最終更新: %s + 互換性なし + バージョン %1$s (%2$s) + 更新 + 並べ替え + 高度なメニュー + 名前 + 更新日時 + 降順 + 更新日時の表示 + モジュールについて + 書込中 + インストール済みのピン留め + 更新可能のピン留め + \ No newline at end of file diff --git a/app/src/main/res/values-night/themes.xml b/app/src/main/res/values-night/themes.xml new file mode 100644 index 00000000..e5cbda4a --- /dev/null +++ b/app/src/main/res/values-night/themes.xml @@ -0,0 +1,4 @@ + + + - - - diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml new file mode 100644 index 00000000..80b1b3eb --- /dev/null +++ b/app/src/main/res/values/themes.xml @@ -0,0 +1,23 @@ + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/xml/config.xml b/app/src/main/res/xml/config.xml deleted file mode 100644 index a885af2f..00000000 --- a/app/src/main/res/xml/config.xml +++ /dev/null @@ -1,44 +0,0 @@ - - - MMRL - No description - - Der_Googler - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/xml/data_extraction_rules.xml b/app/src/main/res/xml/data_extraction_rules.xml deleted file mode 100644 index 8b570f15..00000000 --- a/app/src/main/res/xml/data_extraction_rules.xml +++ /dev/null @@ -1,36 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/xml/locales_config.xml b/app/src/main/res/xml/locales_config.xml new file mode 100644 index 00000000..484fe3f6 --- /dev/null +++ b/app/src/main/res/xml/locales_config.xml @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/xml/provider_paths.xml b/app/src/main/res/xml/provider_paths.xml deleted file mode 100644 index d65fd755..00000000 --- a/app/src/main/res/xml/provider_paths.xml +++ /dev/null @@ -1,28 +0,0 @@ - - - - - - - - - - - - \ No newline at end of file diff --git a/assets/MMRL-CLI-Cover.sketch b/assets/MMRL-CLI-Cover.sketch deleted file mode 100644 index 993fd9ab..00000000 Binary files a/assets/MMRL-CLI-Cover.sketch and /dev/null differ diff --git a/assets/MMRL-Cover.sketch b/assets/MMRL-Cover.sketch deleted file mode 100644 index 6c85b79a..00000000 Binary files a/assets/MMRL-Cover.sketch and /dev/null differ diff --git a/assets/favicons.sketch b/assets/favicons.sketch deleted file mode 100644 index 97ad0482..00000000 Binary files a/assets/favicons.sketch and /dev/null differ diff --git a/assets/gradient-1080px.sketch b/assets/gradient-1080px.sketch deleted file mode 100644 index 183c9992..00000000 Binary files a/assets/gradient-1080px.sketch and /dev/null differ diff --git a/assets/gradient-512px.sketch b/assets/gradient-512px.sketch deleted file mode 100644 index 3864e751..00000000 Binary files a/assets/gradient-512px.sketch and /dev/null differ diff --git a/assets/icon-normal-512px.sketch b/assets/icon-normal-512px.sketch deleted file mode 100644 index 1fc50cab..00000000 Binary files a/assets/icon-normal-512px.sketch and /dev/null differ diff --git a/assets/icon-xmas-512px.sketch b/assets/icon-xmas-512px.sketch deleted file mode 100644 index 935989a6..00000000 Binary files a/assets/icon-xmas-512px.sketch and /dev/null differ diff --git a/assets/normal-icon.sketch b/assets/normal-icon.sketch deleted file mode 100644 index 01859495..00000000 Binary files a/assets/normal-icon.sketch and /dev/null differ diff --git a/assets/screen_01.png b/assets/screen_01.png deleted file mode 100644 index 2c8b66f7..00000000 Binary files a/assets/screen_01.png and /dev/null differ diff --git a/assets/screen_02.png b/assets/screen_02.png deleted file mode 100644 index bf95b736..00000000 Binary files a/assets/screen_02.png and /dev/null differ diff --git a/assets/screen_03.png b/assets/screen_03.png deleted file mode 100644 index b20ee827..00000000 Binary files a/assets/screen_03.png and /dev/null differ diff --git a/assets/screen_04.png b/assets/screen_04.png deleted file mode 100644 index 1cd9488f..00000000 Binary files a/assets/screen_04.png and /dev/null differ diff --git a/assets/screen_05.png b/assets/screen_05.png deleted file mode 100644 index dfba3d85..00000000 Binary files a/assets/screen_05.png and /dev/null differ diff --git a/assets/screen_06.png b/assets/screen_06.png deleted file mode 100644 index f349ae3a..00000000 Binary files a/assets/screen_06.png and /dev/null differ diff --git a/assets/screen_07.png b/assets/screen_07.png deleted file mode 100644 index 4f26a48e..00000000 Binary files a/assets/screen_07.png and /dev/null differ diff --git a/assets/screen_08.png b/assets/screen_08.png deleted file mode 100644 index 54eecf29..00000000 Binary files a/assets/screen_08.png and /dev/null differ diff --git a/assets/screen_09.png b/assets/screen_09.png deleted file mode 100644 index 9afa34cc..00000000 Binary files a/assets/screen_09.png and /dev/null differ diff --git a/assets/store_images.sketch b/assets/store_images.sketch deleted file mode 100644 index 716730d6..00000000 Binary files a/assets/store_images.sketch and /dev/null differ diff --git a/assets/store_ready/1.png b/assets/store_ready/1.png deleted file mode 100644 index 52cf6ca8..00000000 Binary files a/assets/store_ready/1.png and /dev/null differ diff --git a/assets/store_ready/1.webp b/assets/store_ready/1.webp deleted file mode 100644 index 11ee6591..00000000 Binary files a/assets/store_ready/1.webp and /dev/null differ diff --git a/assets/store_ready/2.png b/assets/store_ready/2.png deleted file mode 100644 index 737a1a1f..00000000 Binary files a/assets/store_ready/2.png and /dev/null differ diff --git a/assets/store_ready/2.webp b/assets/store_ready/2.webp deleted file mode 100644 index 60f28071..00000000 Binary files a/assets/store_ready/2.webp and /dev/null differ diff --git a/assets/store_ready/3.png b/assets/store_ready/3.png deleted file mode 100644 index 1f52df0a..00000000 Binary files a/assets/store_ready/3.png and /dev/null differ diff --git a/assets/store_ready/3.webp b/assets/store_ready/3.webp deleted file mode 100644 index 05617cd5..00000000 Binary files a/assets/store_ready/3.webp and /dev/null differ diff --git a/assets/store_ready/4.png b/assets/store_ready/4.png deleted file mode 100644 index 75029d9b..00000000 Binary files a/assets/store_ready/4.png and /dev/null differ diff --git a/assets/store_ready/4.webp b/assets/store_ready/4.webp deleted file mode 100644 index 4bc455b0..00000000 Binary files a/assets/store_ready/4.webp and /dev/null differ diff --git a/assets/store_ready/5.png b/assets/store_ready/5.png deleted file mode 100644 index 0d437566..00000000 Binary files a/assets/store_ready/5.png and /dev/null differ diff --git a/assets/store_ready/5.webp b/assets/store_ready/5.webp deleted file mode 100644 index c71945f7..00000000 Binary files a/assets/store_ready/5.webp and /dev/null differ diff --git a/assets/store_ready/6.png b/assets/store_ready/6.png deleted file mode 100644 index 977f84b0..00000000 Binary files a/assets/store_ready/6.png and /dev/null differ diff --git a/assets/store_ready/6.webp b/assets/store_ready/6.webp deleted file mode 100644 index c7dc331d..00000000 Binary files a/assets/store_ready/6.webp and /dev/null differ diff --git a/assets/store_ready/7.png b/assets/store_ready/7.png deleted file mode 100644 index 399ac016..00000000 Binary files a/assets/store_ready/7.png and /dev/null differ diff --git a/assets/store_ready/7.webp b/assets/store_ready/7.webp deleted file mode 100644 index 3a3f545b..00000000 Binary files a/assets/store_ready/7.webp and /dev/null differ diff --git a/assets/store_ready/8.png b/assets/store_ready/8.png deleted file mode 100644 index 77208a54..00000000 Binary files a/assets/store_ready/8.png and /dev/null differ diff --git a/assets/store_ready/8.webp b/assets/store_ready/8.webp deleted file mode 100644 index 77734610..00000000 Binary files a/assets/store_ready/8.webp and /dev/null differ diff --git a/build-logic/bin/main/ApplicationConventionPlugin.kt b/build-logic/bin/main/ApplicationConventionPlugin.kt new file mode 100644 index 00000000..c274f2bb --- /dev/null +++ b/build-logic/bin/main/ApplicationConventionPlugin.kt @@ -0,0 +1,48 @@ +import com.android.build.api.dsl.ApplicationExtension +import org.gradle.api.JavaVersion +import org.gradle.api.Plugin +import org.gradle.api.Project +import org.gradle.api.plugins.JavaPluginExtension +import org.gradle.jvm.toolchain.JavaLanguageVersion +import org.gradle.kotlin.dsl.apply +import org.gradle.kotlin.dsl.configure +import org.jetbrains.kotlin.gradle.dsl.KotlinAndroidProjectExtension + +class ApplicationConventionPlugin : Plugin { + override fun apply(target: Project) = with(target) { + apply(plugin = "com.android.application") + apply(plugin = "org.jetbrains.kotlin.android") + + extensions.configure { + compileSdk = 34 + buildToolsVersion = "34.0.0" + + defaultConfig { + minSdk = 26 + targetSdk = compileSdk + } + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_21 + targetCompatibility = JavaVersion.VERSION_21 + } + } + + extensions.configure { + toolchain { + languageVersion.set(JavaLanguageVersion.of(21)) + } + } + + extensions.configure { + jvmToolchain(21) + + sourceSets.all { + languageSettings { + optIn("kotlin.ExperimentalStdlibApi") + optIn("kotlinx.coroutines.FlowPreview") + } + } + } + } +} \ No newline at end of file diff --git a/build-logic/bin/main/ComposeConventionPlugin.kt b/build-logic/bin/main/ComposeConventionPlugin.kt new file mode 100644 index 00000000..21fa1c2d --- /dev/null +++ b/build-logic/bin/main/ComposeConventionPlugin.kt @@ -0,0 +1,41 @@ +import com.android.build.api.dsl.ApplicationExtension +import org.gradle.api.Plugin +import org.gradle.api.Project +import org.gradle.api.artifacts.VersionCatalogsExtension +import org.gradle.kotlin.dsl.apply +import org.gradle.kotlin.dsl.configure +import org.gradle.kotlin.dsl.dependencies +import org.gradle.kotlin.dsl.getByType +import org.jetbrains.kotlin.gradle.dsl.KotlinAndroidProjectExtension + +class ComposeConventionPlugin : Plugin { + override fun apply(target: Project) = with(target) { + apply(plugin = "com.android.application") + apply(plugin = "org.jetbrains.kotlin.android") + apply(plugin = "org.jetbrains.kotlin.plugin.compose") + + extensions.configure { + buildFeatures { + compose = true + } + } + + extensions.configure { + sourceSets.all { + languageSettings { + optIn("androidx.compose.material3.ExperimentalMaterial3Api") + optIn("androidx.compose.foundation.ExperimentalFoundationApi") + optIn("androidx.compose.foundation.layout.ExperimentalLayoutApi") + } + } + } + + val libs = extensions.getByType().named("libs") + dependencies { + "implementation"(libs.findLibrary("androidx.compose.material3").get()) + "implementation"(libs.findLibrary("androidx.compose.ui").get()) + "implementation"(libs.findLibrary("androidx.compose.ui.tooling.preview").get()) + "debugImplementation"(libs.findLibrary("androidx.compose.ui.tooling").get()) + } + } +} \ No newline at end of file diff --git a/build-logic/bin/main/HiltConventionPlugin.kt b/build-logic/bin/main/HiltConventionPlugin.kt new file mode 100644 index 00000000..b823ed2d --- /dev/null +++ b/build-logic/bin/main/HiltConventionPlugin.kt @@ -0,0 +1,19 @@ +import org.gradle.api.Plugin +import org.gradle.api.Project +import org.gradle.api.artifacts.VersionCatalogsExtension +import org.gradle.kotlin.dsl.apply +import org.gradle.kotlin.dsl.dependencies +import org.gradle.kotlin.dsl.getByType + +class HiltConventionPlugin : Plugin { + override fun apply(target: Project) = with(target) { + apply(plugin = "dagger.hilt.android.plugin") + apply(plugin = "com.google.devtools.ksp") + + val libs = extensions.getByType().named("libs") + dependencies { + "implementation"(libs.findLibrary("hilt.android").get()) + "ksp"(libs.findLibrary("hilt.compiler").get()) + } + } +} \ No newline at end of file diff --git a/build-logic/bin/main/LibraryConventionPlugin.kt b/build-logic/bin/main/LibraryConventionPlugin.kt new file mode 100644 index 00000000..7804491e --- /dev/null +++ b/build-logic/bin/main/LibraryConventionPlugin.kt @@ -0,0 +1,40 @@ +import com.android.build.api.dsl.LibraryExtension +import org.gradle.api.JavaVersion +import org.gradle.api.Plugin +import org.gradle.api.Project +import org.gradle.api.plugins.JavaPluginExtension +import org.gradle.jvm.toolchain.JavaLanguageVersion +import org.gradle.kotlin.dsl.apply +import org.gradle.kotlin.dsl.configure +import org.jetbrains.kotlin.gradle.dsl.KotlinAndroidProjectExtension + +class LibraryConventionPlugin : Plugin { + override fun apply(target: Project) = with(target) { + apply(plugin = "com.android.library") + apply(plugin = "org.jetbrains.kotlin.android") + + extensions.configure { + compileSdk = 34 + buildToolsVersion = "34.0.0" + + defaultConfig { + minSdk = 26 + } + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_21 + targetCompatibility = JavaVersion.VERSION_21 + } + } + + extensions.configure { + toolchain { + languageVersion.set(JavaLanguageVersion.of(21)) + } + } + + extensions.configure { + jvmToolchain(21) + } + } +} \ No newline at end of file diff --git a/build-logic/bin/main/ProjectExt.kt b/build-logic/bin/main/ProjectExt.kt new file mode 100644 index 00000000..aa69d2e7 --- /dev/null +++ b/build-logic/bin/main/ProjectExt.kt @@ -0,0 +1,40 @@ +import org.gradle.api.Project +import org.gradle.kotlin.dsl.extra +import java.io.File +import java.io.FileInputStream +import java.io.InputStreamReader +import java.util.Properties + +val Project.commitId: String get() = exec("git rev-parse --short HEAD") +val Project.commitCount: Int get() = exec("git rev-list --count HEAD").toInt() + +fun Project.exec(command: String): String = providers.exec { + commandLine(command.split(" ")) +}.standardOutput.asText.get().trim() + +val Project.releaseKeyStore: File get() = File(project.properties["keyStore"] as String) +val Project.releaseKeyStorePassword: String get() = project.properties["keyStorePassword"] as String +val Project.releaseKeyAlias: String get() = project.properties["keyAlias"] as String +val Project.releaseKeyPassword: String get() = project.properties["keyPassword"] as String +val Project.hasReleaseKeyStore: Boolean get() { + gradleSigningProperties(rootDir).apply { + stringPropertyNames().forEach { + project.extra[it] = getProperty(it) + } + } + + return project.hasProperty("keyStore") +} + +private fun gradleSigningProperties(rootDir: File): Properties { + val properties = Properties() + val signingProperties = rootDir.resolve("signing.properties") + + if (signingProperties.isFile && signingProperties.exists()) { + InputStreamReader(FileInputStream(signingProperties), Charsets.UTF_8).use { reader -> + properties.load(reader) + } + } + + return properties +} \ No newline at end of file diff --git a/build-logic/bin/main/RoomConventionPlugin.kt b/build-logic/bin/main/RoomConventionPlugin.kt new file mode 100644 index 00000000..d8a17416 --- /dev/null +++ b/build-logic/bin/main/RoomConventionPlugin.kt @@ -0,0 +1,27 @@ +import com.google.devtools.ksp.gradle.KspExtension +import org.gradle.api.Plugin +import org.gradle.api.Project +import org.gradle.api.artifacts.VersionCatalogsExtension +import org.gradle.kotlin.dsl.apply +import org.gradle.kotlin.dsl.configure +import org.gradle.kotlin.dsl.dependencies +import org.gradle.kotlin.dsl.getByType + +class RoomConventionPlugin : Plugin { + override fun apply(target: Project) = with(target) { + apply(plugin = "com.google.devtools.ksp") + + extensions.configure { + arg("room.incremental", "true") + arg("room.expandProjection", "true") + arg("room.schemaLocation", "$projectDir/schemas") + } + + val libs = extensions.getByType().named("libs") + dependencies { + "implementation"(libs.findLibrary("androidx.room.ktx").get()) + "implementation"(libs.findLibrary("androidx.room.runtime").get()) + "ksp"(libs.findLibrary("androidx.room.compiler").get()) + } + } +} \ No newline at end of file diff --git a/build-logic/build.gradle.kts b/build-logic/build.gradle.kts new file mode 100644 index 00000000..3a031a76 --- /dev/null +++ b/build-logic/build.gradle.kts @@ -0,0 +1,39 @@ +plugins { + `kotlin-dsl` +} + +dependencies { + compileOnly(libs.android.gradle) + compileOnly(libs.compose.gradle) + compileOnly(libs.kotlin.gradle) + compileOnly(libs.ksp.gradle) +} + +gradlePlugin { + plugins { + register("self.application") { + id = "self.application" + implementationClass = "ApplicationConventionPlugin" + } + + register("self.library") { + id = "self.library" + implementationClass = "LibraryConventionPlugin" + } + + register("self.compose") { + id = "self.compose" + implementationClass = "ComposeConventionPlugin" + } + + register("self.hilt") { + id = "self.hilt" + implementationClass = "HiltConventionPlugin" + } + + register("self.room") { + id = "self.room" + implementationClass = "RoomConventionPlugin" + } + } +} \ No newline at end of file diff --git a/build-logic/settings.gradle.kts b/build-logic/settings.gradle.kts new file mode 100644 index 00000000..a8f7ae2c --- /dev/null +++ b/build-logic/settings.gradle.kts @@ -0,0 +1,12 @@ +dependencyResolutionManagement { + repositories { + google() + mavenCentral() + } + + versionCatalogs { + create("libs") { + from(files("../gradle/libs.versions.toml")) + } + } +} \ No newline at end of file diff --git a/build-logic/src/main/kotlin/ApplicationConventionPlugin.kt b/build-logic/src/main/kotlin/ApplicationConventionPlugin.kt new file mode 100644 index 00000000..c274f2bb --- /dev/null +++ b/build-logic/src/main/kotlin/ApplicationConventionPlugin.kt @@ -0,0 +1,48 @@ +import com.android.build.api.dsl.ApplicationExtension +import org.gradle.api.JavaVersion +import org.gradle.api.Plugin +import org.gradle.api.Project +import org.gradle.api.plugins.JavaPluginExtension +import org.gradle.jvm.toolchain.JavaLanguageVersion +import org.gradle.kotlin.dsl.apply +import org.gradle.kotlin.dsl.configure +import org.jetbrains.kotlin.gradle.dsl.KotlinAndroidProjectExtension + +class ApplicationConventionPlugin : Plugin { + override fun apply(target: Project) = with(target) { + apply(plugin = "com.android.application") + apply(plugin = "org.jetbrains.kotlin.android") + + extensions.configure { + compileSdk = 34 + buildToolsVersion = "34.0.0" + + defaultConfig { + minSdk = 26 + targetSdk = compileSdk + } + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_21 + targetCompatibility = JavaVersion.VERSION_21 + } + } + + extensions.configure { + toolchain { + languageVersion.set(JavaLanguageVersion.of(21)) + } + } + + extensions.configure { + jvmToolchain(21) + + sourceSets.all { + languageSettings { + optIn("kotlin.ExperimentalStdlibApi") + optIn("kotlinx.coroutines.FlowPreview") + } + } + } + } +} \ No newline at end of file diff --git a/build-logic/src/main/kotlin/ComposeConventionPlugin.kt b/build-logic/src/main/kotlin/ComposeConventionPlugin.kt new file mode 100644 index 00000000..21fa1c2d --- /dev/null +++ b/build-logic/src/main/kotlin/ComposeConventionPlugin.kt @@ -0,0 +1,41 @@ +import com.android.build.api.dsl.ApplicationExtension +import org.gradle.api.Plugin +import org.gradle.api.Project +import org.gradle.api.artifacts.VersionCatalogsExtension +import org.gradle.kotlin.dsl.apply +import org.gradle.kotlin.dsl.configure +import org.gradle.kotlin.dsl.dependencies +import org.gradle.kotlin.dsl.getByType +import org.jetbrains.kotlin.gradle.dsl.KotlinAndroidProjectExtension + +class ComposeConventionPlugin : Plugin { + override fun apply(target: Project) = with(target) { + apply(plugin = "com.android.application") + apply(plugin = "org.jetbrains.kotlin.android") + apply(plugin = "org.jetbrains.kotlin.plugin.compose") + + extensions.configure { + buildFeatures { + compose = true + } + } + + extensions.configure { + sourceSets.all { + languageSettings { + optIn("androidx.compose.material3.ExperimentalMaterial3Api") + optIn("androidx.compose.foundation.ExperimentalFoundationApi") + optIn("androidx.compose.foundation.layout.ExperimentalLayoutApi") + } + } + } + + val libs = extensions.getByType().named("libs") + dependencies { + "implementation"(libs.findLibrary("androidx.compose.material3").get()) + "implementation"(libs.findLibrary("androidx.compose.ui").get()) + "implementation"(libs.findLibrary("androidx.compose.ui.tooling.preview").get()) + "debugImplementation"(libs.findLibrary("androidx.compose.ui.tooling").get()) + } + } +} \ No newline at end of file diff --git a/build-logic/src/main/kotlin/HiltConventionPlugin.kt b/build-logic/src/main/kotlin/HiltConventionPlugin.kt new file mode 100644 index 00000000..b823ed2d --- /dev/null +++ b/build-logic/src/main/kotlin/HiltConventionPlugin.kt @@ -0,0 +1,19 @@ +import org.gradle.api.Plugin +import org.gradle.api.Project +import org.gradle.api.artifacts.VersionCatalogsExtension +import org.gradle.kotlin.dsl.apply +import org.gradle.kotlin.dsl.dependencies +import org.gradle.kotlin.dsl.getByType + +class HiltConventionPlugin : Plugin { + override fun apply(target: Project) = with(target) { + apply(plugin = "dagger.hilt.android.plugin") + apply(plugin = "com.google.devtools.ksp") + + val libs = extensions.getByType().named("libs") + dependencies { + "implementation"(libs.findLibrary("hilt.android").get()) + "ksp"(libs.findLibrary("hilt.compiler").get()) + } + } +} \ No newline at end of file diff --git a/build-logic/src/main/kotlin/LibraryConventionPlugin.kt b/build-logic/src/main/kotlin/LibraryConventionPlugin.kt new file mode 100644 index 00000000..7804491e --- /dev/null +++ b/build-logic/src/main/kotlin/LibraryConventionPlugin.kt @@ -0,0 +1,40 @@ +import com.android.build.api.dsl.LibraryExtension +import org.gradle.api.JavaVersion +import org.gradle.api.Plugin +import org.gradle.api.Project +import org.gradle.api.plugins.JavaPluginExtension +import org.gradle.jvm.toolchain.JavaLanguageVersion +import org.gradle.kotlin.dsl.apply +import org.gradle.kotlin.dsl.configure +import org.jetbrains.kotlin.gradle.dsl.KotlinAndroidProjectExtension + +class LibraryConventionPlugin : Plugin { + override fun apply(target: Project) = with(target) { + apply(plugin = "com.android.library") + apply(plugin = "org.jetbrains.kotlin.android") + + extensions.configure { + compileSdk = 34 + buildToolsVersion = "34.0.0" + + defaultConfig { + minSdk = 26 + } + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_21 + targetCompatibility = JavaVersion.VERSION_21 + } + } + + extensions.configure { + toolchain { + languageVersion.set(JavaLanguageVersion.of(21)) + } + } + + extensions.configure { + jvmToolchain(21) + } + } +} \ No newline at end of file diff --git a/build-logic/src/main/kotlin/ProjectExt.kt b/build-logic/src/main/kotlin/ProjectExt.kt new file mode 100644 index 00000000..aa69d2e7 --- /dev/null +++ b/build-logic/src/main/kotlin/ProjectExt.kt @@ -0,0 +1,40 @@ +import org.gradle.api.Project +import org.gradle.kotlin.dsl.extra +import java.io.File +import java.io.FileInputStream +import java.io.InputStreamReader +import java.util.Properties + +val Project.commitId: String get() = exec("git rev-parse --short HEAD") +val Project.commitCount: Int get() = exec("git rev-list --count HEAD").toInt() + +fun Project.exec(command: String): String = providers.exec { + commandLine(command.split(" ")) +}.standardOutput.asText.get().trim() + +val Project.releaseKeyStore: File get() = File(project.properties["keyStore"] as String) +val Project.releaseKeyStorePassword: String get() = project.properties["keyStorePassword"] as String +val Project.releaseKeyAlias: String get() = project.properties["keyAlias"] as String +val Project.releaseKeyPassword: String get() = project.properties["keyPassword"] as String +val Project.hasReleaseKeyStore: Boolean get() { + gradleSigningProperties(rootDir).apply { + stringPropertyNames().forEach { + project.extra[it] = getProperty(it) + } + } + + return project.hasProperty("keyStore") +} + +private fun gradleSigningProperties(rootDir: File): Properties { + val properties = Properties() + val signingProperties = rootDir.resolve("signing.properties") + + if (signingProperties.isFile && signingProperties.exists()) { + InputStreamReader(FileInputStream(signingProperties), Charsets.UTF_8).use { reader -> + properties.load(reader) + } + } + + return properties +} \ No newline at end of file diff --git a/build-logic/src/main/kotlin/RoomConventionPlugin.kt b/build-logic/src/main/kotlin/RoomConventionPlugin.kt new file mode 100644 index 00000000..d8a17416 --- /dev/null +++ b/build-logic/src/main/kotlin/RoomConventionPlugin.kt @@ -0,0 +1,27 @@ +import com.google.devtools.ksp.gradle.KspExtension +import org.gradle.api.Plugin +import org.gradle.api.Project +import org.gradle.api.artifacts.VersionCatalogsExtension +import org.gradle.kotlin.dsl.apply +import org.gradle.kotlin.dsl.configure +import org.gradle.kotlin.dsl.dependencies +import org.gradle.kotlin.dsl.getByType + +class RoomConventionPlugin : Plugin { + override fun apply(target: Project) = with(target) { + apply(plugin = "com.google.devtools.ksp") + + extensions.configure { + arg("room.incremental", "true") + arg("room.expandProjection", "true") + arg("room.schemaLocation", "$projectDir/schemas") + } + + val libs = extensions.getByType().named("libs") + dependencies { + "implementation"(libs.findLibrary("androidx.room.ktx").get()) + "implementation"(libs.findLibrary("androidx.room.runtime").get()) + "ksp"(libs.findLibrary("androidx.room.compiler").get()) + } + } +} \ No newline at end of file diff --git a/build.gradle b/build.gradle deleted file mode 100644 index aa68368d..00000000 --- a/build.gradle +++ /dev/null @@ -1,26 +0,0 @@ -// Top-level build file where you can add configuration options common to all sub-projects/modules. - -buildscript { - ext { - agp_version = "8.0.0" - kotlin_version = "1.9.10" - agp_version1 = '8.5.2' - } - repositories { - mavenCentral() - gradlePluginPortal() - google() - } - dependencies { - classpath "com.android.tools.build:gradle:$agp_version1" - classpath "com.github.node-gradle:gradle-node-plugin:7.0.2" - classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" - } -} - -allprojects { - repositories { - mavenCentral() - google() - } -} diff --git a/build.gradle.kts b/build.gradle.kts new file mode 100644 index 00000000..20927987 --- /dev/null +++ b/build.gradle.kts @@ -0,0 +1,13 @@ +plugins { + alias(libs.plugins.android.application) apply false + alias(libs.plugins.android.library) apply false + alias(libs.plugins.compose.compiler) apply false + alias(libs.plugins.kotlin.jvm) apply false + alias(libs.plugins.kotlin.parcelize) apply false + alias(libs.plugins.hilt) apply false + alias(libs.plugins.ksp) apply false +} + +task("clean") { + delete(layout.buildDirectory) +} \ No newline at end of file diff --git a/compat/build.gradle.kts b/compat/build.gradle.kts new file mode 100644 index 00000000..4e00f7d1 --- /dev/null +++ b/compat/build.gradle.kts @@ -0,0 +1,28 @@ +plugins { + alias(libs.plugins.self.library) + alias(libs.plugins.kotlin.parcelize) + alias(libs.plugins.rikka.refine) +} + +android { + namespace = "dev.dergoogler.mmrl.compat" + + buildFeatures { + aidl = true + } +} + +dependencies { + compileOnly(projects.hiddenApi) + implementation(libs.hiddenApiBypass) + implementation(libs.rikka.refine.runtime) + + implementation(libs.libsu.core) + implementation(libs.libsu.service) + + implementation(libs.rikka.shizuku.api) + implementation(libs.rikka.shizuku.provider) + + implementation(libs.androidx.annotation) + implementation(libs.kotlinx.coroutines.android) +} \ No newline at end of file diff --git a/compat/src/main/AndroidManifest.xml b/compat/src/main/AndroidManifest.xml new file mode 100644 index 00000000..1ac33631 --- /dev/null +++ b/compat/src/main/AndroidManifest.xml @@ -0,0 +1,12 @@ + + + + + + \ No newline at end of file diff --git a/compat/src/main/aidl/dev/dergoogler/mmrl/compat/content/LocalModule.aidl b/compat/src/main/aidl/dev/dergoogler/mmrl/compat/content/LocalModule.aidl new file mode 100644 index 00000000..ef7a2db2 --- /dev/null +++ b/compat/src/main/aidl/dev/dergoogler/mmrl/compat/content/LocalModule.aidl @@ -0,0 +1,3 @@ +package dev.dergoogler.mmrl.compat.content; + +parcelable LocalModule; \ No newline at end of file diff --git a/compat/src/main/aidl/dev/dergoogler/mmrl/compat/stub/IFileManager.aidl b/compat/src/main/aidl/dev/dergoogler/mmrl/compat/stub/IFileManager.aidl new file mode 100644 index 00000000..bb45bbce --- /dev/null +++ b/compat/src/main/aidl/dev/dergoogler/mmrl/compat/stub/IFileManager.aidl @@ -0,0 +1,5 @@ +package dev.dergoogler.mmrl.compat.stub; + +interface IFileManager { + boolean deleteOnExit(String path); +} \ No newline at end of file diff --git a/compat/src/main/aidl/dev/dergoogler/mmrl/compat/stub/IInstallCallback.aidl b/compat/src/main/aidl/dev/dergoogler/mmrl/compat/stub/IInstallCallback.aidl new file mode 100644 index 00000000..e832b439 --- /dev/null +++ b/compat/src/main/aidl/dev/dergoogler/mmrl/compat/stub/IInstallCallback.aidl @@ -0,0 +1,10 @@ +package dev.dergoogler.mmrl.compat.stub; + +import dev.dergoogler.mmrl.compat.content.LocalModule; + +interface IInstallCallback { + void onStdout(String msg); + void onStderr(String msg); + void onSuccess(in LocalModule module); + void onFailure(); +} \ No newline at end of file diff --git a/compat/src/main/aidl/dev/dergoogler/mmrl/compat/stub/IModuleManager.aidl b/compat/src/main/aidl/dev/dergoogler/mmrl/compat/stub/IModuleManager.aidl new file mode 100644 index 00000000..43a5450d --- /dev/null +++ b/compat/src/main/aidl/dev/dergoogler/mmrl/compat/stub/IModuleManager.aidl @@ -0,0 +1,17 @@ +package dev.dergoogler.mmrl.compat.stub; + +import dev.dergoogler.mmrl.compat.content.LocalModule; +import dev.dergoogler.mmrl.compat.stub.IInstallCallback; +import dev.dergoogler.mmrl.compat.stub.IModuleOpsCallback; + +interface IModuleManager { + String getVersion(); + int getVersionCode(); + List getModules(); + LocalModule getModuleById(String id); + LocalModule getModuleInfo(String zipPath); + oneway void enable(String id, IModuleOpsCallback callback); + oneway void disable(String id, IModuleOpsCallback callback); + oneway void remove(String id, IModuleOpsCallback callback); + oneway void install(String path, IInstallCallback callback); +} \ No newline at end of file diff --git a/compat/src/main/aidl/dev/dergoogler/mmrl/compat/stub/IModuleOpsCallback.aidl b/compat/src/main/aidl/dev/dergoogler/mmrl/compat/stub/IModuleOpsCallback.aidl new file mode 100644 index 00000000..9c7dba87 --- /dev/null +++ b/compat/src/main/aidl/dev/dergoogler/mmrl/compat/stub/IModuleOpsCallback.aidl @@ -0,0 +1,6 @@ +package dev.dergoogler.mmrl.compat.stub; + +interface IModuleOpsCallback { + void onSuccess(String id); + void onFailure(String id, String msg); +} \ No newline at end of file diff --git a/compat/src/main/aidl/dev/dergoogler/mmrl/compat/stub/IPowerManager.aidl b/compat/src/main/aidl/dev/dergoogler/mmrl/compat/stub/IPowerManager.aidl new file mode 100644 index 00000000..76384eaf --- /dev/null +++ b/compat/src/main/aidl/dev/dergoogler/mmrl/compat/stub/IPowerManager.aidl @@ -0,0 +1,5 @@ +package dev.dergoogler.mmrl.compat.stub; + +interface IPowerManager { + void reboot(boolean confirm, String reason, boolean wait); +} \ No newline at end of file diff --git a/compat/src/main/aidl/dev/dergoogler/mmrl/compat/stub/IServiceManager.aidl b/compat/src/main/aidl/dev/dergoogler/mmrl/compat/stub/IServiceManager.aidl new file mode 100644 index 00000000..c7686cb1 --- /dev/null +++ b/compat/src/main/aidl/dev/dergoogler/mmrl/compat/stub/IServiceManager.aidl @@ -0,0 +1,17 @@ +package dev.dergoogler.mmrl.compat.stub; + +import dev.dergoogler.mmrl.compat.stub.IFileManager; +import dev.dergoogler.mmrl.compat.stub.IModuleManager; +import dev.dergoogler.mmrl.compat.stub.IPowerManager; + +interface IServiceManager { + int getUid() = 0; + int getPid() = 1; + String getSELinuxContext() = 2; + String currentPlatform() = 3; + IModuleManager getModuleManager() = 4; + IFileManager getFileManager() = 5; + IPowerManager getPowerManager() = 6; + + void destroy() = 16777114; // Only for Shizuku +} \ No newline at end of file diff --git a/compat/src/main/kotlin/dev/dergoogler/mmrl/compat/BuildCompat.kt b/compat/src/main/kotlin/dev/dergoogler/mmrl/compat/BuildCompat.kt new file mode 100644 index 00000000..db2ea81c --- /dev/null +++ b/compat/src/main/kotlin/dev/dergoogler/mmrl/compat/BuildCompat.kt @@ -0,0 +1,18 @@ +package dev.dergoogler.mmrl.compat + +import android.os.Build +import androidx.annotation.ChecksSdkIntAtLeast + +object BuildCompat { + @get:ChecksSdkIntAtLeast(api = Build.VERSION_CODES.TIRAMISU) + val atLeastT get() = Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU + + @get:ChecksSdkIntAtLeast(api = Build.VERSION_CODES.S) + val atLeastS get() = Build.VERSION.SDK_INT >= Build.VERSION_CODES.S + + @get:ChecksSdkIntAtLeast(api = Build.VERSION_CODES.R) + val atLeastR get() = Build.VERSION.SDK_INT >= Build.VERSION_CODES.R + + @get:ChecksSdkIntAtLeast(api = Build.VERSION_CODES.P) + val atLeastP get() = Build.VERSION.SDK_INT >= Build.VERSION_CODES.P +} \ No newline at end of file diff --git a/compat/src/main/kotlin/dev/dergoogler/mmrl/compat/ServiceManagerCompat.kt b/compat/src/main/kotlin/dev/dergoogler/mmrl/compat/ServiceManagerCompat.kt new file mode 100644 index 00000000..aa643f4e --- /dev/null +++ b/compat/src/main/kotlin/dev/dergoogler/mmrl/compat/ServiceManagerCompat.kt @@ -0,0 +1,191 @@ +package dev.dergoogler.mmrl.compat + +import android.content.ComponentName +import android.content.Context +import android.content.Intent +import android.content.ServiceConnection +import android.content.pm.PackageManager +import android.os.IBinder +import com.topjohnwu.superuser.Shell +import com.topjohnwu.superuser.ipc.RootService +import dev.dergoogler.mmrl.compat.delegate.ContextDelegate +import dev.dergoogler.mmrl.compat.impl.ServiceManagerImpl +import dev.dergoogler.mmrl.compat.stub.IServiceManager +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.suspendCancellableCoroutine +import kotlinx.coroutines.withContext +import kotlinx.coroutines.withTimeout +import org.lsposed.hiddenapibypass.HiddenApiBypass +import rikka.shizuku.Shizuku +import kotlin.coroutines.resume +import kotlin.coroutines.resumeWithException + +object ServiceManagerCompat { + internal const val VERSION_CODE = 1 + + private const val TIMEOUT_MILLIS = 15_000L + + private val context by lazy { ContextDelegate.getContext() } + + fun setHiddenApiExemptions() = when { + BuildCompat.atLeastP -> HiddenApiBypass.addHiddenApiExemptions("") + else -> true + } + + interface IProvider { + val name: String + fun isAvailable(): Boolean + suspend fun isAuthorized(): Boolean + fun bind(connection: ServiceConnection) + fun unbind(connection: ServiceConnection) + } + + private suspend fun get( + provider: IProvider + ) = withTimeout(TIMEOUT_MILLIS) { + suspendCancellableCoroutine { continuation -> + val connection = object : ServiceConnection { + override fun onServiceConnected(name: ComponentName, binder: IBinder) { + val service = IServiceManager.Stub.asInterface(binder) + continuation.resume(service) + } + + override fun onServiceDisconnected(name: ComponentName) { + continuation.resumeWithException( + IllegalStateException("IServiceManager destroyed") + ) + } + + override fun onBindingDied(name: ComponentName?) { + continuation.resumeWithException( + IllegalStateException("IServiceManager destroyed") + ) + } + } + + provider.bind(connection) + continuation.invokeOnCancellation { + provider.unbind(connection) + } + } + } + + suspend fun from(provider: IProvider): IServiceManager = withContext(Dispatchers.Main) { + when { + !provider.isAvailable() -> throw IllegalStateException("${provider.name} not available") + !provider.isAuthorized() -> throw IllegalStateException("${provider.name} not authorized") + else -> get(provider) + } + } + + private class ShizukuService : Shizuku.UserServiceArgs( + ComponentName( + context.packageName, + ServiceManagerImpl::class.java.name + ) + ) { + init { + daemon(false) + debuggable(false) + version(VERSION_CODE) + processNameSuffix("shizuku") + } + } + + private class ShizukuProvider : IProvider { + override val name = "Shizuku" + + override fun isAvailable(): Boolean { + return Shizuku.pingBinder() && Shizuku.getUid() == 0 + } + + override suspend fun isAuthorized() = when { + isGranted -> true + else -> suspendCancellableCoroutine { continuation -> + val listener = object : Shizuku.OnRequestPermissionResultListener { + override fun onRequestPermissionResult( + requestCode: Int, + grantResult: Int + ) { + Shizuku.removeRequestPermissionResultListener(this) + continuation.resume(isGranted) + } + } + + Shizuku.addRequestPermissionResultListener(listener) + continuation.invokeOnCancellation { + Shizuku.removeRequestPermissionResultListener(listener) + } + Shizuku.requestPermission(listener.hashCode()) + } + } + + override fun bind(connection: ServiceConnection) { + Shizuku.bindUserService(ShizukuService(), connection) + } + + override fun unbind(connection: ServiceConnection) { + Shizuku.unbindUserService(ShizukuService(), connection, true) + } + + private val isGranted get() = Shizuku.checkSelfPermission() == PackageManager.PERMISSION_GRANTED + } + + suspend fun fromShizuku() = from(ShizukuProvider()) + + private class SuService : RootService() { + override fun onBind(intent: Intent): IBinder { + return ServiceManagerImpl() + } + + companion object { + val intent get() = Intent().apply { + component = ComponentName( + context.packageName, + SuService::class.java.name + ) + } + } + } + + private class LibSuProvider : IProvider { + override val name = "LibSu" + + init { + Shell.enableVerboseLogging = true + Shell.setDefaultBuilder( + Shell.Builder.create() + .setInitializers(SuShellInitializer::class.java) + .setTimeout(10) + ) + } + + override fun isAvailable() = true + + override suspend fun isAuthorized() = suspendCancellableCoroutine { continuation -> + Shell.EXECUTOR.submit { + runCatching { + Shell.getShell() + }.onSuccess { + continuation.resume(true) + }.onFailure { + continuation.resume(false) + } + } + } + + override fun bind(connection: ServiceConnection) { + RootService.bind(SuService.intent, connection) + } + + override fun unbind(connection: ServiceConnection) { + RootService.stop(SuService.intent) + } + + private class SuShellInitializer : Shell.Initializer() { + override fun onInit(context: Context, shell: Shell) = shell.isRoot + } + } + + suspend fun fromLibSu() = from(LibSuProvider()) +} \ No newline at end of file diff --git a/compat/src/main/kotlin/dev/dergoogler/mmrl/compat/content/LocalModule.kt b/compat/src/main/kotlin/dev/dergoogler/mmrl/compat/content/LocalModule.kt new file mode 100644 index 00000000..0632d794 --- /dev/null +++ b/compat/src/main/kotlin/dev/dergoogler/mmrl/compat/content/LocalModule.kt @@ -0,0 +1,19 @@ +package dev.dergoogler.mmrl.compat.content + +import android.os.Parcelable +import kotlinx.parcelize.Parcelize + +@Parcelize +data class LocalModule( + val id: String, + val name: String, + val version: String, + val versionCode: Int, + val author: String, + val description: String, + val updateJson: String, + val state: State, + val lastUpdated: Long +) : Parcelable { + companion object +} \ No newline at end of file diff --git a/compat/src/main/kotlin/dev/dergoogler/mmrl/compat/content/State.kt b/compat/src/main/kotlin/dev/dergoogler/mmrl/compat/content/State.kt new file mode 100644 index 00000000..88d655e3 --- /dev/null +++ b/compat/src/main/kotlin/dev/dergoogler/mmrl/compat/content/State.kt @@ -0,0 +1,8 @@ +package dev.dergoogler.mmrl.compat.content + +enum class State { + ENABLE, + REMOVE, + DISABLE, + UPDATE +} \ No newline at end of file diff --git a/compat/src/main/kotlin/dev/dergoogler/mmrl/compat/delegate/ContextDelegate.kt b/compat/src/main/kotlin/dev/dergoogler/mmrl/compat/delegate/ContextDelegate.kt new file mode 100644 index 00000000..618caf74 --- /dev/null +++ b/compat/src/main/kotlin/dev/dergoogler/mmrl/compat/delegate/ContextDelegate.kt @@ -0,0 +1,16 @@ +package dev.dergoogler.mmrl.compat.delegate + +import android.app.ActivityThread +import android.content.Context +import android.content.ContextWrapper + +object ContextDelegate { + fun getContext(): Context { + var context: Context = ActivityThread.currentApplication() + while (context is ContextWrapper) { + context = context.baseContext + } + + return context + } +} \ No newline at end of file diff --git a/compat/src/main/kotlin/dev/dergoogler/mmrl/compat/delegate/PowerManagerDelegate.kt b/compat/src/main/kotlin/dev/dergoogler/mmrl/compat/delegate/PowerManagerDelegate.kt new file mode 100644 index 00000000..ac1d5a95 --- /dev/null +++ b/compat/src/main/kotlin/dev/dergoogler/mmrl/compat/delegate/PowerManagerDelegate.kt @@ -0,0 +1,39 @@ +package dev.dergoogler.mmrl.compat.delegate + +import android.os.Build +import android.os.PowerManagerHidden +import androidx.annotation.RequiresApi +import dev.dergoogler.mmrl.compat.BuildCompat +import dev.dergoogler.mmrl.compat.stub.IPowerManager + +class PowerManagerDelegate( + private val powerManager: IPowerManager +) { + fun reboot(reason: Reason = Reason.UserRequested) { + powerManager.reboot(false, reason.reason, true) + } + + enum class Reason( + internal val reason: String + ) { + UserRequested(SHUTDOWN_USER_REQUESTED), + @RequiresApi(Build.VERSION_CODES.R) + Userspace(REBOOT_USERSPACE), + Recovery(REBOOT_RECOVERY), + Bootloader(REBOOT_BOOTLOADER) + } + + companion object { + const val SHUTDOWN_USER_REQUESTED = "userrequested" + + @RequiresApi(Build.VERSION_CODES.R) + const val REBOOT_USERSPACE = "userspace" + + const val REBOOT_RECOVERY = "recovery" + + const val REBOOT_BOOTLOADER = "bootloader" + + fun isRebootingUserspaceSupported() = + BuildCompat.atLeastR && PowerManagerHidden.isRebootingUserspaceSupportedImpl() + } +} \ No newline at end of file diff --git a/compat/src/main/kotlin/dev/dergoogler/mmrl/compat/impl/APatchModuleManagerImpl.kt b/compat/src/main/kotlin/dev/dergoogler/mmrl/compat/impl/APatchModuleManagerImpl.kt new file mode 100644 index 00000000..dd0a7f1a --- /dev/null +++ b/compat/src/main/kotlin/dev/dergoogler/mmrl/compat/impl/APatchModuleManagerImpl.kt @@ -0,0 +1,56 @@ +package dev.dergoogler.mmrl.compat.impl + +import com.topjohnwu.superuser.Shell +import dev.dergoogler.mmrl.compat.stub.IInstallCallback +import dev.dergoogler.mmrl.compat.stub.IModuleOpsCallback + +internal class APatchModuleManagerImpl( + private val shell: Shell, +) : BaseModuleManagerImpl(shell) { + override fun enable(id: String, callback: IModuleOpsCallback) { + val dir = modulesDir.resolve(id) + if (!dir.exists()) callback.onFailure(id, null) + + "apd module enable $id".submit { + if (it.isSuccess) { + callback.onSuccess(id) + } else { + callback.onFailure(id, it.out.joinToString()) + } + } + } + + override fun disable(id: String, callback: IModuleOpsCallback) { + val dir = modulesDir.resolve(id) + if (!dir.exists()) return callback.onFailure(id, null) + + "apd module disable $id".submit { + if (it.isSuccess) { + callback.onSuccess(id) + } else { + callback.onFailure(id, it.out.joinToString()) + } + } + } + + override fun remove(id: String, callback: IModuleOpsCallback) { + val dir = modulesDir.resolve(id) + if (!dir.exists()) return callback.onFailure(id, null) + + "apd module uninstall $id".submit { + if (it.isSuccess) { + callback.onSuccess(id) + } else { + callback.onFailure(id, it.out.joinToString()) + } + } + } + + override fun install(path: String, callback: IInstallCallback) { + install( + cmd = "apd module install '${path}'", + path = path, + callback = callback + ) + } +} \ No newline at end of file diff --git a/compat/src/main/kotlin/dev/dergoogler/mmrl/compat/impl/BaseModuleManagerImpl.kt b/compat/src/main/kotlin/dev/dergoogler/mmrl/compat/impl/BaseModuleManagerImpl.kt new file mode 100644 index 00000000..905967a2 --- /dev/null +++ b/compat/src/main/kotlin/dev/dergoogler/mmrl/compat/impl/BaseModuleManagerImpl.kt @@ -0,0 +1,172 @@ +package dev.dergoogler.mmrl.compat.impl + +import com.topjohnwu.superuser.CallbackList +import com.topjohnwu.superuser.Shell +import com.topjohnwu.superuser.ShellUtils +import dev.dergoogler.mmrl.compat.content.LocalModule +import dev.dergoogler.mmrl.compat.content.State +import dev.dergoogler.mmrl.compat.stub.IInstallCallback +import dev.dergoogler.mmrl.compat.stub.IModuleManager +import java.io.File +import java.util.zip.ZipFile + +internal abstract class BaseModuleManagerImpl( + private val shell: Shell +) : IModuleManager.Stub() { + internal val modulesDir = File(MODULES_PATH) + + private val mVersion by lazy { + runCatching { + "su -v".exec() + }.getOrDefault("unknown") + } + + private val mVersionCode by lazy { + runCatching { + "su -V".exec().toInt() + }.getOrDefault(-1) + } + + override fun getVersion(): String { + return mVersion + } + + override fun getVersionCode(): Int { + return mVersionCode + } + + override fun getModules() = modulesDir.listFiles() + .orEmpty() + .mapNotNull { dir -> + readProps(dir)?.toModule(dir) + } + + override fun getModuleById(id: String): LocalModule? { + val dir = modulesDir.resolve(id) + return readProps(dir)?.toModule(dir) + } + + override fun getModuleInfo(zipPath: String): LocalModule? { + val zipFile = ZipFile(zipPath) + val entry = zipFile.getEntry(PROP_FILE) ?: return null + + return zipFile.getInputStream(entry).use { + it.bufferedReader() + .readText() + .let(::readProps) + .toModule() + } + } + + private fun readProps(props: String) = props.lines() + .associate { line -> + val items = line.split("=", limit = 2).map { it.trim() } + if (items.size != 2) { + "" to "" + } else { + items[0] to items[1] + } + } + + private fun readProps(moduleDir: File) = + moduleDir.resolve(PROP_FILE).let { + when { + it.exists() -> readProps(it.readText()) + else -> null + } + } + + private fun readState(moduleDir: File): State { + moduleDir.resolve("remove").apply { + if (exists()) return State.REMOVE + } + + moduleDir.resolve("disable").apply { + if (exists()) return State.DISABLE + } + + moduleDir.resolve("update").apply { + if (exists()) return State.UPDATE + } + + return State.ENABLE + } + + private fun readLastUpdated(moduleDir: File): Long { + MODULE_FILES.forEach { filename -> + val file = moduleDir.resolve(filename) + if (file.exists()) { + return file.lastModified() + } + } + + return 0L + } + + private fun Map.toModule( + dir: File + ) = toModule( + path = dir.name, + state = readState(dir), + lastUpdated = readLastUpdated(dir) + ) + + private fun Map.toModule( + path: String = "unknown", + state: State = State.ENABLE, + lastUpdated: Long = 0L + ) = LocalModule( + id = getOrDefault("id", path), + name = getOrDefault("name", path), + version = getOrDefault("version", ""), + versionCode = getOrDefault("versionCode", "-1").toIntOr(-1), + author = getOrDefault("author", ""), + description = getOrDefault("description", ""), + updateJson = getOrDefault("updateJson", ""), + state = state, + lastUpdated = lastUpdated + ) + + private fun String.toIntOr(defaultValue: Int) = + runCatching { + toInt() + }.getOrDefault(defaultValue) + + private fun String.exec() = ShellUtils.fastCmd(shell, this) + + internal fun install(cmd: String, path: String, callback: IInstallCallback) { + val stdout = object : CallbackList() { + override fun onAddElement(msg: String?) { + msg?.let(callback::onStdout) + } + } + + val stderr = object : CallbackList() { + override fun onAddElement(msg: String?) { + msg?.let(callback::onStderr) + } + } + + val result = shell.newJob().add(cmd).to(stdout, stderr).exec() + if (result.isSuccess) { + val module = getModuleInfo(path) + callback.onSuccess(module) + } else { + callback.onFailure() + } + } + + internal fun String.submit(cb: Shell.ResultCallback) = shell + .newJob().add(this).to(ArrayList(), null) + .submit(cb) + + companion object { + const val PROP_FILE = "module.prop" + const val MODULES_PATH = "/data/adb/modules" + + val MODULE_FILES = listOf( + "post-fs-data.sh", "service.sh", "uninstall.sh", + "system", "system.prop", "module.prop" + ) + } +} \ No newline at end of file diff --git a/compat/src/main/kotlin/dev/dergoogler/mmrl/compat/impl/FileManagerImpl.kt b/compat/src/main/kotlin/dev/dergoogler/mmrl/compat/impl/FileManagerImpl.kt new file mode 100644 index 00000000..9ebe712f --- /dev/null +++ b/compat/src/main/kotlin/dev/dergoogler/mmrl/compat/impl/FileManagerImpl.kt @@ -0,0 +1,15 @@ +package dev.dergoogler.mmrl.compat.impl + +import dev.dergoogler.mmrl.compat.stub.IFileManager +import java.io.File + +internal class FileManagerImpl : IFileManager.Stub() { + override fun deleteOnExit(path: String) = with(File(path)) { + when { + !exists() -> false + isFile -> delete() + isDirectory -> deleteRecursively() + else -> false + } + } +} \ No newline at end of file diff --git a/compat/src/main/kotlin/dev/dergoogler/mmrl/compat/impl/KernelSUModuleManagerImpl.kt b/compat/src/main/kotlin/dev/dergoogler/mmrl/compat/impl/KernelSUModuleManagerImpl.kt new file mode 100644 index 00000000..ed04c213 --- /dev/null +++ b/compat/src/main/kotlin/dev/dergoogler/mmrl/compat/impl/KernelSUModuleManagerImpl.kt @@ -0,0 +1,56 @@ +package dev.dergoogler.mmrl.compat.impl + +import com.topjohnwu.superuser.Shell +import dev.dergoogler.mmrl.compat.stub.IInstallCallback +import dev.dergoogler.mmrl.compat.stub.IModuleOpsCallback + +internal class KernelSUModuleManagerImpl( + private val shell: Shell, +) : BaseModuleManagerImpl(shell) { + override fun enable(id: String, callback: IModuleOpsCallback) { + val dir = modulesDir.resolve(id) + if (!dir.exists()) callback.onFailure(id, null) + + "ksud module enable $id".submit { + if (it.isSuccess) { + callback.onSuccess(id) + } else { + callback.onFailure(id, it.out.joinToString()) + } + } + } + + override fun disable(id: String, callback: IModuleOpsCallback) { + val dir = modulesDir.resolve(id) + if (!dir.exists()) return callback.onFailure(id, null) + + "ksud module disable $id".submit { + if (it.isSuccess) { + callback.onSuccess(id) + } else { + callback.onFailure(id, it.out.joinToString()) + } + } + } + + override fun remove(id: String, callback: IModuleOpsCallback) { + val dir = modulesDir.resolve(id) + if (!dir.exists()) return callback.onFailure(id, null) + + "ksud module uninstall $id".submit { + if (it.isSuccess) { + callback.onSuccess(id) + } else { + callback.onFailure(id, it.out.joinToString()) + } + } + } + + override fun install(path: String, callback: IInstallCallback) { + install( + cmd = "ksud module install '${path}'", + path = path, + callback = callback + ) + } +} \ No newline at end of file diff --git a/compat/src/main/kotlin/dev/dergoogler/mmrl/compat/impl/MagiskModuleManagerImpl.kt b/compat/src/main/kotlin/dev/dergoogler/mmrl/compat/impl/MagiskModuleManagerImpl.kt new file mode 100644 index 00000000..aad48e4a --- /dev/null +++ b/compat/src/main/kotlin/dev/dergoogler/mmrl/compat/impl/MagiskModuleManagerImpl.kt @@ -0,0 +1,59 @@ +package dev.dergoogler.mmrl.compat.impl + +import com.topjohnwu.superuser.Shell +import dev.dergoogler.mmrl.compat.stub.IInstallCallback +import dev.dergoogler.mmrl.compat.stub.IModuleOpsCallback + +internal class MagiskModuleManagerImpl( + private val shell: Shell +) : BaseModuleManagerImpl(shell) { + override fun enable(id: String, callback: IModuleOpsCallback) { + val dir = modulesDir.resolve(id) + if (!dir.exists()) callback.onFailure(id, null) + + runCatching { + dir.resolve("remove").apply { if (exists()) delete() } + dir.resolve("disable").apply { if (exists()) delete() } + }.onSuccess { + callback.onSuccess(id) + }.onFailure { + callback.onFailure(id, it.message) + } + } + + override fun disable(id: String, callback: IModuleOpsCallback) { + val dir = modulesDir.resolve(id) + if (!dir.exists()) return callback.onFailure(id, null) + + runCatching { + dir.resolve("remove").apply { if (exists()) delete() } + dir.resolve("disable").createNewFile() + }.onSuccess { + callback.onSuccess(id) + }.onFailure { + callback.onFailure(id, it.message) + } + } + + override fun remove(id: String, callback: IModuleOpsCallback) { + val dir = modulesDir.resolve(id) + if (!dir.exists()) return callback.onFailure(id, null) + + runCatching { + dir.resolve("disable").apply { if (exists()) delete() } + dir.resolve("remove").createNewFile() + }.onSuccess { + callback.onSuccess(id) + }.onFailure { + callback.onFailure(id, it.message) + } + } + + override fun install(path: String, callback: IInstallCallback) { + install( + cmd = "magisk --install-module '${path}'", + path = path, + callback = callback + ) + } +} \ No newline at end of file diff --git a/compat/src/main/kotlin/dev/dergoogler/mmrl/compat/impl/Platform.kt b/compat/src/main/kotlin/dev/dergoogler/mmrl/compat/impl/Platform.kt new file mode 100644 index 00000000..65799d26 --- /dev/null +++ b/compat/src/main/kotlin/dev/dergoogler/mmrl/compat/impl/Platform.kt @@ -0,0 +1,7 @@ +package dev.dergoogler.mmrl.compat.impl + +enum class Platform { + Magisk, + KernelSU, + APatch +} \ No newline at end of file diff --git a/compat/src/main/kotlin/dev/dergoogler/mmrl/compat/impl/PowerManagerImpl.kt b/compat/src/main/kotlin/dev/dergoogler/mmrl/compat/impl/PowerManagerImpl.kt new file mode 100644 index 00000000..dc967c3d --- /dev/null +++ b/compat/src/main/kotlin/dev/dergoogler/mmrl/compat/impl/PowerManagerImpl.kt @@ -0,0 +1,11 @@ +package dev.dergoogler.mmrl.compat.impl + +import dev.dergoogler.mmrl.compat.stub.IPowerManager + +internal class PowerManagerImpl( + private val original: IPowerManager +) : IPowerManager.Stub() { + override fun reboot(confirm: Boolean, reason: String?, wait: Boolean) { + original.reboot(confirm, reason, wait) + } +} \ No newline at end of file diff --git a/compat/src/main/kotlin/dev/dergoogler/mmrl/compat/impl/ServiceManagerImpl.kt b/compat/src/main/kotlin/dev/dergoogler/mmrl/compat/impl/ServiceManagerImpl.kt new file mode 100644 index 00000000..7a0244d3 --- /dev/null +++ b/compat/src/main/kotlin/dev/dergoogler/mmrl/compat/impl/ServiceManagerImpl.kt @@ -0,0 +1,84 @@ +package dev.dergoogler.mmrl.compat.impl + +import android.content.Context +//import android.os.IPowerManager // what huh? +import android.os.SELinux +import android.os.ServiceManager +import android.system.Os +import com.topjohnwu.superuser.Shell +import com.topjohnwu.superuser.ShellUtils +import dev.dergoogler.mmrl.compat.stub.IFileManager +import dev.dergoogler.mmrl.compat.stub.IModuleManager +import dev.dergoogler.mmrl.compat.stub.IPowerManager +import dev.dergoogler.mmrl.compat.stub.IServiceManager +import kotlin.system.exitProcess + +internal class ServiceManagerImpl : IServiceManager.Stub() { + private val main by lazy { + Shell.Builder.create() + .build("sh") + } + + private val platform by lazy { + when { + "which magisk".execResult() -> Platform.Magisk + "which ksud".execResult() -> Platform.KernelSU + "which apd".execResult() -> Platform.APatch + else -> throw IllegalArgumentException("unsupported platform: $seLinuxContext") + } + } + + private val moduleManager by lazy { + when (platform) { + Platform.Magisk -> MagiskModuleManagerImpl(main) + Platform.KernelSU -> KernelSUModuleManagerImpl(main) + Platform.APatch -> APatchModuleManagerImpl(main) + } + } + + private val fileManager by lazy { + FileManagerImpl() + } + + private val powerManager by lazy { + PowerManagerImpl( + IPowerManager.Stub.asInterface( + ServiceManager.getService(Context.POWER_SERVICE) + ) + ) + } + + override fun getUid(): Int { + return Os.getuid() + } + + override fun getPid(): Int { + return Os.getpid() + } + + override fun getSELinuxContext(): String { + return SELinux.getContext() + } + + override fun currentPlatform(): String { + return platform.name + } + + override fun getModuleManager(): IModuleManager { + return moduleManager + } + + override fun getFileManager(): IFileManager { + return fileManager + } + + override fun getPowerManager(): IPowerManager { + return powerManager + } + + override fun destroy() { + exitProcess(0) + } + + private fun String.execResult() = ShellUtils.fastCmdResult(main, this) +} \ No newline at end of file diff --git a/deploy.js b/deploy.js deleted file mode 100644 index f0227ad4..00000000 --- a/deploy.js +++ /dev/null @@ -1,114 +0,0 @@ -const { publish } = require("gh-pages"); -const { config } = require("./package.json"); -const { readFileSync, statSync, readdirSync } = require("fs"); -const { resolve, basename, extname } = require("path"); -require("dotenv").config({ path: resolve(__dirname, "local.properties") }); -const { program } = require("commander"); -const { spawn } = require("child_process"); - -program - .command("website") - .description("Publishes the app to the provided repo") - .option("-p, --prerelease", "Publish the app as a prerelease") - .option("-r, --remote [REMOTE]", "Use another remote", "mmrl-web") - .option("-b, --branch [BRANCH]", "Use another branch", "master") - .option("-o, --owner [OWNER]", "Use repo owner", "DerGoogler") - .option("-n, --name [NAME]", "Use repo name", "mmrl-web") - .option("-a, --add", "Only add, and never remove existing files.") - .option("-l, --blog ", "Add a blog link to the release (Placeholder)") - .action((opt) => { - const __dir = ["app/src/main/assets/www"]; - publish(resolve(__dirname, ...__dir), { - branch: opt.branch, - repo: `https://github.com/${opt.owner}/${opt.name}.git`, - dest: opt.prerelease ? "prerelease" : ".", - nojekyll: true, - cname: config.cname, - remote: opt.remote, - message: "CLI Auto-generated MMRL Web Update", - user: { - name: "github-actions[bot]", - email: "github-actions[bot]@users.noreply.github.com", - }, - add: false, - }); - }); - -program - .command("github") - .description("Publishes the app to GitHub releases") - .option("-p, --prerelease", "Publish the app as a prerelease") - .option("-o, --owner [OWNER]", "Use repo owner", "DerGoogler") - .option("-n, --name [NAME]", "Use repo name", "MMRL") - .option("-l, --blog ", "Add a blog link to the release") - .option("-v, --verTag [VERTAG]", "Use a custom version tag", "v{tag}") - .option("-t, --token [TOKEN]", "Use another GitHub token", process.env.GITHUB_TOKEN) - .action(async (opt) => { - const versionTag = opt.verTag.replace(/{tag}/gm, config.version_name); - - const { Octokit } = await import("@octokit/rest"); - - const octokit = new Octokit({ - auth: opt.token, - }); - - const uploadAPKFiles = async (releaseId, apkFiles) => { - for (const apkFile of apkFiles) { - const filePath = resolve(apkFile); - const fileName = basename(filePath); - const fileData = readFileSync(filePath); - const fileSize = statSync(filePath).size; - - try { - await octokit.repos.uploadReleaseAsset({ - owner: opt.owner, - repo: opt.name, - release_id: releaseId, - name: fileName, - data: fileData, - headers: { - "content-type": "application/vnd.android.package-archive", - "content-length": fileSize, - }, - }); - console.log(`Uploaded ${fileName} successfully.`); - } catch (error) { - console.error(`Failed to upload ${fileName}:`, error); - } - } - }; - - const createReleaseAndUploadAPKFiles = async () => { - try { - // Create a new release - const release = await octokit.repos.createRelease({ - owner: opt.owner, - repo: opt.name, - tag_name: versionTag, - name: versionTag, - body: `Changelogs are readable [here](https://github.com/DerGoogler/MMRL/wiki/Changelog) or directly in the app.\n\nWeb: https://mmrl.dergoogler.com\nBlog: ${ - opt.blog ? opt.blog : "-" - }`, - draft: false, - prerelease: opt.prerelease, - }); - - const releaseId = release.data.id; - console.log(`Created release with ID: ${releaseId}`); - - // Specify your APK files here - const __dir = ["app/default/release"]; - const apkFiles = readdirSync(resolve(__dirname, ...__dir)) - .flatMap((file) => resolve(__dirname, ...__dir, file)) - .filter((fns) => extname(fns) === ".apk"); - - await uploadAPKFiles(releaseId, apkFiles); - } catch (error) { - console.error("Failed to create release or upload APK files:", error); - } - }; - - createReleaseAndUploadAPKFiles(); - }); - -program.parse(); diff --git a/docs/Installer/README.md b/docs/Installer/README.md deleted file mode 100644 index 0f74c365..00000000 --- a/docs/Installer/README.md +++ /dev/null @@ -1,87 +0,0 @@ -# Installer V3 - -Implantation - -```shell -mmrl_exec() { - if [ "$MMRL_INTR" = "true" ]; then - local command=$1 - shift - local args=$(printf "|%s" "$@") - args=${args:1} - ui_print "#!mmrl:<$command=($args)>" - fi -} -``` - -## Internal commands - -Every internal command starts with `mmrl_exec`! - -### Clear terminal - -Clears everything from the terminal - -```shell -mmrl_exec clearTerminal -``` - -| Args | Description | -| ---- | ---------------- | -| No | | -| | Returns no value | - -### Replace last line - -You can replace the last placed line, even the last line is a button - -```shell -mmrl_exec setLastLine "This is a cool log" -``` - -| Args | Description | -| ---- | -------------------------------------- | -| `$1` | Text that should replace the last line | -| | Returns no value | - -### Remove last line - -This command just removes the last line - -```shell -mmrl_exec removeLastLine -``` - -| Args | Description | -| ---- | ---------------- | -| No | | -| | Returns no value | - - - -## Making colored text easir! - -When you implant the API you can start using `gui_print`. - -```shell -mmrl_exec color "This is MMRL!" -``` - -| Args | Description | -| ---- | --------------- | -| `$1` | Text | -| | Returns a value | - -> [!NOTE] -> Other installer will return `This is MMRL!` because it's a MMRL only syntax diff --git a/docs/Installer/V2.md b/docs/Installer/V2.md deleted file mode 100644 index 042d3379..00000000 --- a/docs/Installer/V2.md +++ /dev/null @@ -1,81 +0,0 @@ -# Installer V2 - -Implantation - -```shell -if [ "$MMRL_INTR" = "true" ]; then - mmrl_exec() { ui_print "#!mmrl:$*"; } - gui_print() { mmrl_exec color "\"$@\""; } - mmrl_setLastLine() { mmrl_exec setLastLine "\"$@\""; } - gui_image() { mmrl_exec addImage "$*"; } -else - mmrl_exec() { true; } - gui_print() { ui_print "$@" | sed 's/<[A-Z.]*>//g'; } - mmrl_setLastLine() { true; } - gui_image() { true; } -fi -``` - -## Internal commands - -Every internal command starts with `mmrl_exec`! - -### Clear terminal - -Clears everything from the terminal - -```shell -mmrl_exec clearTerminal -``` - -| Args | Description | -| ---- | ----------- | -| No | | - -### Replace last line - -You can replace the last placed line, even the last line is a button - -```shell -mmrl_exec setLastLine "This is a cool log" -``` - -| Args | Description | -| --------- | -------------------------------------- | -| `args[0]` | Text that should replace the last line | - -### Remove last line - -This command just removes the last line - -```shell -mmrl_exec removeLastLine -``` - -| Args | Description | -| ---- | ----------- | -| No | | - -### Add a button - -This command can a little bit more but it has less functionality because you can't a click event - -```shell -mmrl_exec addButton "Button text here" --variant "contained or outlined" -``` - -| Args | Description | -| ----------- | ----------------------------------------- | -| `args[0]` | Button text | -| `--variant` | Choose between `contained` and `outlined` | - -## Making colored text easir! - -When you implant the API you can start using `gui_print`. - -```shell -gui_print "This is MMRL!" -``` - -> [!NOTE] -> Other installer will return `This is MMRL!` because it's a MMRL only syntax diff --git a/docs/Installer/V2_az.md b/docs/Installer/V2_az.md deleted file mode 100644 index 22770295..00000000 --- a/docs/Installer/V2_az.md +++ /dev/null @@ -1,79 +0,0 @@ -# Quraşdırıcı V2 - -İmplantasiya - -```shell -if [ "$MMRL_INTR" = "true" ]; then - mmrl_exec() { ui_print "#!mmrl:$*"; } - gui_print() { mmrl_exec color "\"$@\""; } - mmrl_setLastLine() { mmrl_exec setLastLine "\"$@\""; } -else - mmrl_exec() { true; } - gui_print() { ui_print "$@" | sed 's/<[A-Z.]*>//g'; } - mmrl_setLastLine() { true; } -fi -``` - -## Daxili əmr - -Hər bir daxili əmr `mmrl_exec' ilə başlayır! - -### Terminalı təmizləyin - -Terminaldan hər şeyi təmizləyir - -```shell -mmrl_exec clearTerminal -``` - -| Arqlər | Təsvir | -| ------ | ------ | -| Yoxdur | | - -### Son sətri əvəz edin - -Son yerləşdirilmiş sətri əvəz edə bilərsiniz, hətta sonuncu sətir bir düymədir - -```shell -mmrl_exec setLastLine "This is a cool log" -``` - -| Arqlər | Təsvir | -| --------- | ------------------------------- | -| `args[0]` | Son sətri əvəz etməli olan mətn | - -### Son sətri silin - -Bu əmr yalnız sonuncu sətri silir - -```shell -mmrl_exec removeLastLine -``` - -| Arqlər | Təsvir | -| ------ | ------ | -| Yoxdur | | - -### Bir düymə əlavə edin - -Bu əmr bir az daha çox ola bilər, lakin daha az funksionallığa malikdir, çünki bir klik hadisəsi edə bilməzsiniz - -```shell -mmrl_exec addButton "Button text here" --variant "contained or outlined" -``` - -| Arqlər | Təsvir | -| ----------- | --------------------------------------------- | -| `args[0]` | Düymə mətni | -| `--variant` | 'contained' və 'outlined' arasında seçim edin | - -## Rəngli mətni asanlaşdırmaq! - -PPİ-ni implantasiya etdikdə `gui_print` istifadə etməyə başlaya bilərsiniz. - -```shell -gui_print "This is MMRL!" -``` - -> [!QEYD] -> Digər quraşdırıcı 'Bu MMRL!' qaytaracaq, çünki bu, yalnız MMRL sintaksisidir diff --git a/docs/ModConf/README.md b/docs/ModConf/README.md deleted file mode 100644 index 7e121925..00000000 --- a/docs/ModConf/README.md +++ /dev/null @@ -1,142 +0,0 @@ -# What is ModConf? - -ModConf stands for **Module Configuration**, this feature in MMRL allows you to create dynamic configuration pages for individual modules. Essentially, it provides a way to customize how information about a module is displayed within the MMRL app. - -Think of it like this: By default, MMRL might display basic details about a module, like its name and description. With ModConf, developers of modules can create custom pages that show additional information relevant to their specific module. This could include things like: - -- Screenshots or icons for the module -- Detailed descriptions of what the module does and its functionalities -- Configuration options specific to the module -- Verification details to ensure the module's authenticity -- Dependency information, listing other modules required for this one to function - -While MMRL itself doesn't directly create these ModConf pages, it provides the framework for developers to implement them for their modules. - -## Getting started - -Your `index.jsx` needs to be placed inside `/system/usr/share/mmrl/config//index.jsx`. - -Simple starter: - -```jsx -import React from "react"; -import { Page } from "@mmrl/ui"; - -export default () => { - return Test; -}; -``` - -> [!IMPORTANT] -> Always wrap your content with `` otherwise your page won't start corretly - -## Available libaries - -See [`libs.ts`](https://github.com/DerGoogler/MMRL/blob/master/Website/src/components/ModConfView/libs.ts) to see all usable modules - -## How it works - -With `require` you can import predefined libaries or your files - -```js -const React = require("react"); -// or -import React from "react"; -``` - ----- - -Supported file types: - -- `*.js` -- `*.jsx` -- `*.json` -- `*.yaml` -- `*.yml` -- `*.ini` -- `*.prop` -- `*.properties` - -```js -import { Component } from "Component" // .jsx - -const properties = require(path.resolve(__moddirname, "module.prop")) - -const { id, name, author } = properties; -``` - -## Defaut functions and variables - -There are some default functions and variables that makes the development easier.. - -> These functions make also usage of the ModConf services. - -### `__idname` (former `modid`) - -Types - -```ts -declare const __idname: string; -``` - -### `__modpath` (new) - -Types - -```ts -declare const __modpath: string; -``` - -### `__filename` (new) - -Types - -```ts -declare const __filename: string; -``` - -### `__dirname` (former `modpath`) - -Types - -```ts -declare const __dirname: string; -``` - -# Blacklisted functions - -- `eval()` -- `document.write()` -- `document.writeln()` -- `decodeURI()` -- `decodeURIComponent()` -- `endodeURI()` -- `encodeURIComponent()` -- `atob()` -- `bota()` - -These function will throw a `IsolatedEvalError` and your config will stop immediately after calling the function - -# Code Server - -Make conf development easier with code-server - -## Requirements - -- [Systemless Mkshrc](https://github.com/Magisk-Modules-Alt-Repo/mkshrc) -- [Systemless Node.js](https://github.com/Magisk-Modules-Alt-Repo/node) -- [Code Server](https://github.com/Googlers-Repo/code-server) - -> You can get everthing on MMRL except Code Server - -> ### Install it with [MMRL-CLI](https://github.com/DerGoogler/MMRL-CLI) -> -> ```shell -> mmrl install -y "mkshrc" "node_on_android" "https://github.com/Googlers-Repo/code-server/releases/latest/download/module.zip" -> ``` - -Then you can start code-server after a reboot - -```shell -code-server -``` diff --git a/docs/ModConf/README_az.md b/docs/ModConf/README_az.md deleted file mode 100644 index 208d5969..00000000 --- a/docs/ModConf/README_az.md +++ /dev/null @@ -1,141 +0,0 @@ -# ModConf nədir? - -ModConf **Modul Konfiqurasiyası** deməkdir, MMRL-dəki bu xüsusiyyət fərdi modullar üçün dinamik konfiqurasiya səhifələri yaratmağa imkan verir. Əsasən, o, modul haqqında məlumatın MMRL tətbiqində necə göstərildiyini fərdiləşdirmək üçün bir yol təqdim edir. - -Bunu bu şəkildə düşünün: Varsayılan olaraq, MMRL modulun adı və təsviri kimi əsas təfərrüatlarını göstərə bilər. ModConf ilə modul tərtibatçıları öz modullarına uyğun əlavə məlumatları göstərən xüsusi səhifələr yarada bilərlər. Bu, aşağıdakı kimi şeyləri əhatə edə bilər: - -- Modul üçün ekran görüntüləri və ya simgələr -- Modulun nə etdiyi və onun funksiyalarının ətraflı təsviri -- Modula xüsusi konfiqurasiya seçimləri -- Modulun həqiqiliyini təmin etmək üçün doğrulama detalları -- Asılılıq məlumatları, bunun işləməsi üçün tələb olunan digər modulların siyahılanması - -MMRL özü bu ModConf səhifələrini birbaşa yaratmasa da, tərtibatçılar üçün onları öz modullarına tətbiq edəcəyi çərçivəyi təmin edir. - -## Başlayarkən - -`index.jsx` faylınızın `/system/usr/share/mmrl/config//index.jsx` içinə yerləşdirilməsi lazımdır. - -Sadə başlanğıc: - -```jsx -import React from "react"; -import { Page } from "@mmrl/ui"; - -export default () => { - return Test; -}; -``` - -> [!VACİB] -> Məzmununuzu həmişə `` ilə sarıyın, əks halda səhifəniz düzgün şəkildə başlamayacaq - -## Mövcud kitabxanalar - -Bütün istifadə edilə bilən modulları görmək üçün [`libs.ts`](https://github.com/DerGoogler/MMRL/blob/master/Website/src/components/ModConfView/libs.ts) ünvanına baxın - -## Necə işləyir - -`require` ilə siz əvvəlcədən təyin edilmiş kitabxanaları və ya fayllarınızı idxal edə bilərsiniz - -```js -const React = require("react"); -// or -import React from "react"; -``` - ----- - -Dəstəklənən fayl növləri: - -- `*.js` -- `*.jsx` -- `*.json` -- `*.yaml` -- `*.yml` -- `*.ini` -- `*.prop` -- `*.properties` - -```js -import { Component } from "Component" // .jsx - -const properties = require(path.resolve(__moddirname, "module.prop")) - -const { id, name, author } = properties; -``` - -## Varsayılan funksiyalar və dəyişənlər - -İnkişafı asanlaşdıran bəzi varsayılan funksiyalar və dəyişənlər var.. - -> Bu funksiyalar həmçinin ModConf xidmətlərindən də istifadə edir. - -### `__idname` (former `modid`) - -Types - -```ts -declare const __idname: string; -``` - -### `__modpath` (new) - -Types - -```ts -declare const __modpath: string; -``` - - -### `__filename` (new) - -Types - -```ts -declare const __filename: string; -``` - -### `__dirname` (former `modpath`) - -Types - -```ts -declare const __dirname: string; -``` - -# Qara siyahıya alınmış funksiyalar - -- `eval()` -- `document.write()` -- `document.writeln()` -- `decodeURI()` -- `decodeURIComponent()` -- `endodeURI()` -- `encodeURIComponent()` -- `atob()` -- `bota()` - -Bu funksiya bir 'IsolatedEvalError' xətası verəcək və konfiqurasiyanız funksiyanı çağırdıqdan dərhal sonra dayandırılacaq. - -# Kod Serveri - -Kod-server ilə konfiqurasiya inkişafını asanlaşdırın - -## Tələblər - -- [Systemless Mkshrc](https://github.com/Magisk-Modules-Alt-Repo/mkshrc) -- [Systemless Node.js](https://github.com/Magisk-Modules-Alt-Repo/node) -- [Code Server](https://github.com/Googlers-Repo/code-server) - -> Kod Serverindən başqa hər şeyi MMRL-də tapa bilərsiniz -```shell -# start code server -code-server -``` - -Və ya riskli yolu istifadə edin - -```shell -ln -s `which code-server` /data/adb/service.d/code-server -``` diff --git a/docs/ModConf/STANDALONE.md b/docs/ModConf/STANDALONE.md deleted file mode 100644 index 59f52e87..00000000 --- a/docs/ModConf/STANDALONE.md +++ /dev/null @@ -1,42 +0,0 @@ -# ModConf Standalone - -ModConf Standalone nearly supports everything from the normal ModConf. - -## Getting started - -Create `/data/adb/mmrl/modconf//modconf.json` with the following content - -```json -{ - "id": "", - "name": "Your name", - "description": "your description", - "main": "/src/index.jsx", - "cwd": "/src" -} -``` - -### `modconf.json` fields - -| Key | Required | Description | -| ------------- | ------------------------- | ------------------------------------------------------------------------------------------------------------------------------- | -| `id` | Yes | The `id` should always match the folder name otherwise it won't show up | -| `name` | Optional | Used to display the name | -| `description` | Optional | Use to display the description and that it does | -| `main` | No | Here can you set your own index file. Useful when you have a `src` folder.
ModFS supported
| -| `cwd` | No (yes if `main` is set) | It is required to use `cwd` if `main` is set to ensure the the imports are working
ModFS supported
| - -## Index file - -As above decribed you can set your own index path - -`src/index.jsx` - -```jsx -import React from "react"; -import { Page } from "@mmrl/ui"; - -export default () => { - return Test; -}; -``` diff --git a/docs/ModConf/components/Image.md b/docs/ModConf/components/Image.md deleted file mode 100644 index cb68e748..00000000 --- a/docs/ModConf/components/Image.md +++ /dev/null @@ -1,56 +0,0 @@ -# Image API - -When you want to use images that stored on your device then you can use the `` component to access them - -## Setup - -```js -import { Image } from "@mmrl/ui"; -``` - -## Usage - -```js -import React from "react"; -import { useActivity } from "@mmrl/hooks"; -import { Image, Page, Toolbar } from "@mmrl/ui"; - -function RenderToolbar() { - const { context, extra } = useActivity(); - const { title = "Default" } = extra; - return ( - - - - - {title} - - ); -} - -function App() { - return ( - - -
- -

Disable opening

- -
- ); -} - -export default App; -``` - -## API - -| Attr | Required | Type | -| ----------- | -------- | --------- | -| `src` | Yes | `string` | -| `type` | No | `string` | -| `shadow` | No | `number` | -| `title` | No | `string` | -| `noOpen` | No | `boolean` | -| `modFSAdds` | No | `object` | -| `sx` | No | `SxProps` | diff --git a/docs/ModConf/components/Markdown.md b/docs/ModConf/components/Markdown.md deleted file mode 100644 index b9ad4b19..00000000 --- a/docs/ModConf/components/Markdown.md +++ /dev/null @@ -1,51 +0,0 @@ -# Image API - -Component to display markdown code - -## Setup - -```js -import { Markdown } from "@mmrl/ui"; -``` - -## Usage - -```js -import React from "react"; -import { useActivity } from "@mmrl/hooks"; -import { Markdown, Page, Toolbar } from "@mmrl/ui"; - -function RenderToolbar() { - const { context, extra } = useActivity(); - const { title = "Default" } = extra; - return ( - - - - - {title} - - ); -} - -function App() { - return ( - - {` -# Heading 1 - -Hello, world! - `} - - ); -} - -export default App; -``` - -## API - -| Attr | Required | Type | -| ---------- | -------- | ----------------- | -| `children` | Yes | `React.ReactNode` | -| `fetch` | No | `url\|string` | diff --git a/docs/ModConf/examples/ColoredNativeNavbar.md b/docs/ModConf/examples/ColoredNativeNavbar.md deleted file mode 100644 index ab177f85..00000000 --- a/docs/ModConf/examples/ColoredNativeNavbar.md +++ /dev/null @@ -1,65 +0,0 @@ -# Colored Native Navbar Example - -```js -import React from "react"; -import { Page, Toolbar, BottomToolbar } from "@mmrl/ui"; -import { Box } from "@mui/material"; - -// web handler -const bottomInsets = os.isAndroid ? view.getWindowBottomInsets() : 23; - -const RenderToolbar = () => { - return ( - - Custom Native Navbar - - ); -}; - -const RenderBottomToolbar = () => { - return ( - - ); -}; - -export default () => { - return ( - - - Hello, world! - - - ); -}; -``` diff --git a/docs/ModConf/functions/Chooser.md b/docs/ModConf/functions/Chooser.md deleted file mode 100644 index f9464392..00000000 --- a/docs/ModConf/functions/Chooser.md +++ /dev/null @@ -1,20 +0,0 @@ -# Chooser - -File picker, even mutiple files - -> [!IMPORTANT] -> In MMRL V2.18.17 and above, `Chooser` will be a global part of **ModConf** - -## Usage - -```js -const chooseModule = new Chooser("application/zip"); - -chooseModule.onChose = (files) => { - if (Chooser.isSuccess(files)) { - console.log(files); - } -}; - -chooseModule.getFiles(); -``` diff --git a/docs/ModConf/functions/DOMParser.md b/docs/ModConf/functions/DOMParser.md deleted file mode 100644 index 37d1d71f..00000000 --- a/docs/ModConf/functions/DOMParser.md +++ /dev/null @@ -1,37 +0,0 @@ -# DOMParser - -## Usage - -```js -import React from "react"; -import { Page } from "@mmrl/ui"; -import { List, ListItem, ListItemText } from "@mui/material"; - -const dom = new DOMParser(); - -const configStore = dom.parseFromFile( - "/data/misc/apexdata/com.android.wifi/WifiConfigStoreSoftAp.xml" -); - -const softap = configStore.getElementsByTagName("SoftAp")[0]; - -const ssidName = softap - .querySelector('string[name="WifiSsid"]') - .innerHTML.replace(/"(.+)"/g, "$1"); -const passwd = softap.querySelector('string[name="Passphrase"]').innerHTML; - -export default () => { - return ( - - - - - - - - - - - ); -}; -``` diff --git a/docs/ModConf/functions/Shell.md b/docs/ModConf/functions/Shell.md deleted file mode 100644 index 90023024..00000000 --- a/docs/ModConf/functions/Shell.md +++ /dev/null @@ -1,49 +0,0 @@ -# Shell - -Finally the `Shell` API is public! - -> [!NOTE] -> `Shell` is a global part in ModConf. No need to import it. -> -> The `Shell` API v2 is currently in the beta - -## Execute a normal command - -```js -const ls = new Shell("ls"); - -ls.exec(); -``` - -## Execute with result - -```js -const ls = new Shell("ls"); - -console.log(ls.result()); -``` - -## Get code - -```js -const ls = new Shell("ls"); - -console.log(ls.getCode()); -``` - -## Check if it is successful - -```js -const ls = new Shell("ls"); - -console.log(ls.isSuccess()); -``` - -## Lagecy API - -```js -Shell.cmd("ls").exec(); -Shell.cmd("ls").result(); -Shell.cmd("ls").getCode(); -Shell.cmd("ls").isSuccess(); -``` diff --git a/docs/ModConf/functions/SuFile.md b/docs/ModConf/functions/SuFile.md deleted file mode 100644 index 0dcafaab..00000000 --- a/docs/ModConf/functions/SuFile.md +++ /dev/null @@ -1,105 +0,0 @@ -# SuFile - -Here will you learn, how `SuFile` works in MMRL. - -> [!NOTE] -> `SuFile` is a global part in ModConf. No need to import it. - -## Reading files - -Files can easily be read within MMRL, it returns a empty string if the file does not exists. - -```js -const mkprop = new SuFile("/data/adb/modules/mkshrc/module.prop"); -console.log(mkprop.read()); -``` - -## Check for existing - -It you want to make sure that your file exists - -```js -const mkprop = new SuFile("/data/adb/modules/mkshrc/module.prop"); - -if (mkprop.exist()) { - console.log("File exist!"); -} else { - // stops ModConf - throw new Error("File does not exist!"); -} -``` - -## Writing files - -Be careful with this function, changes that are made can't be restored. - -```js -SuFile.write("/sdcard/file_test.txt", "foo"); -// new SuFile("/sdcard/file_test.txt").write("foo") -``` - -## Listing files - -You also able to list whole folders. - -```js -const modules = new SuFile("/data/adb/modules"); - -if (modules.exist()) { - console.log(modules.list()); -} else { - throw new Error("Modules folder does not exist"); -} -``` - -It also possible to change the delimiter - -```js -modules.list(";"); // other than "," -``` - -## Get last modified date - -```js -const mkprop = new SuFile("/data/adb/modules/mkshrc/module.prop"); -console.log(mkprop.lastModified()); -``` - -## Deleting files - -```js -const file = new SuFile("/sdcard/file_test.txt"); - -if (file.delete()) { - console.log("Successful deleted"); -} else { - console.log("Something went wrong"); -} - -// deleting recursive -file.deleteRecursive(); // void -``` - -## Creating files and folders - -This method can multiple things like creating files, folders and parent folders - -```js -const hello = new SuFile("/sdcard/hello.txt"); - -// create a file (default) -hello.create(); // hello.create(SuFile.NEW_FILE) - -// create a folder -hello.create(SuFile.NEW_FOLDER); - -// create folder with parent folders -hello.create(SuFile.NEW_FOLDERS); -``` - -## Access native methods - -```js -const native = new SuFile("/sdcard/hello.txt"); -native.interface -``` diff --git a/docs/ModConf/functions/SuZip.md b/docs/ModConf/functions/SuZip.md deleted file mode 100644 index b43bce32..00000000 --- a/docs/ModConf/functions/SuZip.md +++ /dev/null @@ -1,48 +0,0 @@ -# SuZip - -Here will you learn, how `SuZip` works in MMRL. It has the same style as `SuFile` - -> [!NOTE] -> `SuZip` is a global part in ModConf. No need to import it. -> -> Unstable class! - -## Reading files - -```js -const mkprop = new SuZip("file.zip", "path/to/file"); -console.log(mkprop.read()); -``` - -## Check for existing - -```js -const mkprop = new SuZip("file.zip", "path/to/file"); - -if (mkprop.exist()) { - console.log("File exist!"); -} else { - // stops ModConf - throw new Error("File does not exist!"); -} -``` - -## Listing files - -```js -const modules = new SuZip("file.zip", "path/to/file"); - -if (modules.exist()) { - // path is ignored here - console.log(modules.list()); -} else { - throw new Error("Modules folder does not exist"); -} -``` - -## Access native methods - -```js -const native = new SuZip("file.zip", "path/to/file"); -native.interface; -``` diff --git a/docs/ModConf/functions/Terminal.md b/docs/ModConf/functions/Terminal.md deleted file mode 100644 index e62821bb..00000000 --- a/docs/ModConf/functions/Terminal.md +++ /dev/null @@ -1,41 +0,0 @@ -# Terminal - -With this class you can inplant your own terminal like MMRL's installer - -> [!IMPORTANT] -> In MMRL V2.18.17 and above, `Terminal` will be a global part of **ModConf** - -## Usage - -```js -const myTerminal = new Terminal({ - // working dir - cwd: "/data/local", -}); - -// set env vars -myTerminal.env = { - TMPDIR: "/data/local/tmp", -}; - -// get results -myTerminal.onLine = (line) => { - console.log(line); -}; - -// get results -myTerminal.onError = (line) => { - console.error(line); -}; - - -// get exit code -myTerminal.onExit = (code) => { - console.log("Exit code:", code); -}; - -// start terminal -myTerminal.exec('echo "Hello, world!"'); -``` - -> Check out [InstallTerminalV2Activity.tsx](https://github.com/DerGoogler/MMRL/blob/master/Website/src/activitys/InstallTerminalV2Activity.tsx) for advanced usage diff --git a/docs/ModConf/functions/Window.md b/docs/ModConf/functions/Window.md deleted file mode 100644 index 4d2da052..00000000 --- a/docs/ModConf/functions/Window.md +++ /dev/null @@ -1,23 +0,0 @@ -# Window - -Here is the window API documentated, restricted. - -## `.open` - -This is currently the only method avaiable here - -```js -const windowFeatures = "left=100,top=100,width=320,height=320"; -window.open( - "https://www.mozilla.org/", - "mozillaWindow", - windowFeatures -); -``` - -Android supports a additional feature `color` - -```js -const windowFeatures = "color=#ffffff"; -window.open("https://www.mozilla.org/", "mozillaWindow", windowFeatures); -``` diff --git a/docs/ModConf/hoc/withRequireNewVersion.md b/docs/ModConf/hoc/withRequireNewVersion.md deleted file mode 100644 index 3dc3c986..00000000 --- a/docs/ModConf/hoc/withRequireNewVersion.md +++ /dev/null @@ -1,55 +0,0 @@ -# Export with a new version requirement - -Only shows the page if the requirement is met - -## Setup - -```js -import { withRequireNewVersion } from "@mmrl/hoc"; -``` - -## Usage - -```jsx -import React from "react"; -import { Page, Toolbar } from "@mmrl/ui"; -import { useActivity } from "@mmrl/hooks"; -import { withRequireNewVersion } from "@mmrl/hoc"; -import { StringsProvider } from "@mmrl/providers"; - -function RenderToolbar() { - const { context, extra } = useActivity(); - const { title = "Default" } = extra; - return ( - - - - - {title} - - ); -} - -function App() { - return Lol; -} - -export default withRequireNewVersion({ - versionCode: 21510, - component: () => { - return ( - - - - ); - }, - // text: "Custom text", - // title: "Custom title" -}); -``` - diff --git a/docs/ModConf/hooks/useActivity.md b/docs/ModConf/hooks/useActivity.md deleted file mode 100644 index d76b20de..00000000 --- a/docs/ModConf/hooks/useActivity.md +++ /dev/null @@ -1,77 +0,0 @@ -# Activity Managment - -Simple manage multiple activities with this hook. - -## Setup - -```js -import { useActivity } from "@mmrl/hooks"; -``` - -## Usage - -```js -import React from "react"; -import { useActivity } from "@mmrl/hooks"; -import { Page, Toolbar } from "@mmrl/ui"; -import { Button } from "@mui/material"; - -function RenderToolbar() { - const { context, extra } = useActivity(); - const { title = "Default" } = extra; - return ( - - - - - {title} - - ); -} - -function App() { - const { context } = useActivity(); - - const handleOpen = () => { - context.pushPage({ - component: NewPage, - // don't forget this! - key: "MyNewPage", - // if your page has props - props: {}, - // push any object here - extra: { - title: "Hello", - }, - }); - }; - - return ( - - - - ); -} - -const allowBack = false; - -function NewPage() { - return ( - { - if (allowBack) { - e.callParentHandler(); - } - }} - renderToolbar={RenderToolbar} - > - Try to use your back button - - ); -} - -export default App; -``` diff --git a/docs/ModConf/hooks/useStrings.md b/docs/ModConf/hooks/useStrings.md deleted file mode 100644 index d56c3b6d..00000000 --- a/docs/ModConf/hooks/useStrings.md +++ /dev/null @@ -1,49 +0,0 @@ -# Config Localization - -## Setup - -```js -import { useStrings } from "@mmrl/hooks"; -import { StringsProvider } from "@mmrl/providers"; -``` - -## Usage - -```jsx -import React from "react"; -import { Page, Toolbar } from "@mmrl/ui"; -import { useStrings, useActivity } from "@mmrl/hooks"; -import { StringsProvider } from "@mmrl/providers"; - -function RenderToolbar() { - const { context, extra } = useActivity(); - const { title = "Default" } = extra; - return ( - - - - - {title} - - ); -} - -function App() { - const { strings } = useStrings(); - - return {strings("hello")}; -} - -export default () => { - return ( - - - - ); -}; -``` diff --git a/docs/ModFS.md b/docs/ModFS.md deleted file mode 100644 index 8a377283..00000000 --- a/docs/ModFS.md +++ /dev/null @@ -1,52 +0,0 @@ -# Module File System (ModFS) - -**ModFS** is a core component that provides a flexible and customizable filesystem for managing modules on Android devices. It's designed to streamline module installation, updates, and removal while offering granular control over module structure. - - - -## Types and functions - -### findBinary - -> [!NOTE] -> This is not a global ModFS part and only avaiable above v3.24.31. - -``` - -``` - -Returns the full paths of the first found binary diff --git a/docs/ModFS_az.md b/docs/ModFS_az.md deleted file mode 100644 index bf7986d1..00000000 --- a/docs/ModFS_az.md +++ /dev/null @@ -1,47 +0,0 @@ -# Modul Fayl Sistemi (ModFS) - -ETMƏK - -## Həll yolları - -Bəzi konfiqurasiyaların qarşısını almaq üçün bəzi nümunələr var - -## MMRL Quraşdırma Alətlərindən çəkinin - -Bu həll yolu digər modulları quraşdırmaq üçün əlavə modul quraşdırmaq istəməyən insanlar üçündür - -Seçdiyiniz kök metoduna əsasən - -| Kök metodu | ƏXİ | Busybox | -| ----------- | ---------- | ---------- | -| Magisk | `` | `` | -| KernelSU | `` | `` | -| APatch | `` | `` | - -### Lokal quraşdırma skripti - -```shell - --install-module "" -``` - -> Biz nümunə olaraq magisk istifadə edirik. KernelSU və ya APatch ` module install ""`-dır. - -### Quraşdırma skriptini araşdırın - -```shell -FILE="/data/local/tmp/.zip"; wget "" -O $FILE; --install-module $FILE; -``` - -# Modulunuza lokal bir örtük əlavə edin - -> [!VACIB] -> Örtük yolunuzu sərt kodlamayın - -```properties -id=mkshrc -# ... - -cover=/system/usr/share/mmrl/covers/cover.png -# ModConf ciq-da saxlanılırsa -# cover=/assets/cover.png -``` diff --git a/docs/README_PT.md b/docs/README_PT.md deleted file mode 100644 index 569c155b..00000000 --- a/docs/README_PT.md +++ /dev/null @@ -1,81 +0,0 @@ -[English](README.md) || **Português (Brasil)** || [Azərbaycan](README_az.md) - -

- -
Seu gerenciador de módulos altamente personalizável -

- -

- - Get it on Google Play - Get it on GitHub - -
- - Get it on F-Droid - Get it on GitHub - -

- -

- Build with Webpack - Generate APK Debug - GitHub all releases -

- -

- FAQ • - ModConf • - ModFS • - Installer -

- -# Sobre - -Apresentando Magisk Module Repo Loader (MMRL) - O gerenciador de módulos definitivo para Magisk, KernelSU e APatch no Android. Este aplicativo altamente configurável permite aos usuários gerenciar módulos sem esforço, ao mesmo tempo que é completamente livre de anúncios. - -# Requisitos - -- Android 8.0 ou Superior -- [MMRL-CLI](https://github.com/DerGoogler/MMRL-CLI) -- 4-5 GB RAM (Menor pode ser possível) - - -# Características - -- Basico: Literalmente básico -- [ModFS](https://github.com/DerGoogler/MMRL/tree/master/docs/ModFS.md): Sistema de arquivos de módulo personalizável -- [ModConf](https://github.com/DerGoogler/MMRL/blob/master/docs/ModConf/README.md): Fornece páginas criadas dinâmicas para módulos -- Repos personalizados: carregue qualquer repositório que use o MRepo ou o GR Fork - -> Leia a [documentação](https://github.com/DerGoogler/MMRL/tree/master/docs) para explorar mais de nossas funções como `Shell`, `SuFile` e muito mais - -### Gerenciadores root compatíveis - -- [x] [Magisk](https://github.com/topjohnwu/Magisk) -- [x] [Magisk Delta](https://github.com/HuskyDG/magisk-files) -- [x] [KernelSU](https://github.com/tiann/KernelSU) -- [x] [APatch](https://github.com/bmax121/APatch) - -# Capturas de Tela - -

- Screenshot 1 of MMRL - Screenshot 2 of MMRL - Screenshot 3 of MMRL - Screenshot 4 of MMRL - Screenshot 5 of MMRL - Screenshot 6 of MMRL - Screenshot 7 of MMRL - Screenshot 8 of MMRL -

- -# Créditos e Agradecimentos - -- [tabler/tabler-icons](https://github.com/tabler/tabler-icons.git) -- [Googlers-Repo/node-native](https://github.com/Googlers-Repo/node-native) -- [topjohnwu/libsu](https://github.com/topjohnwu/libsu) -- [Fox2Code/FoxMagiskModuleManager](https://github.com/Fox2Code/FoxMagiskModuleManager) -- [DerGoogler/dgm-cms](https://github.com/DerGoogler/dgm-cms) -- [Hentai-Web/Core](https://github.com/Hentai-Web/Core) -- [Hentai-Web/Android](https://github.com/Hentai-Web/Android) diff --git a/docs/README_az.md b/docs/README_az.md deleted file mode 100644 index 5cc039a9..00000000 --- a/docs/README_az.md +++ /dev/null @@ -1,81 +0,0 @@ -[English](README.md) || [Português](README_PT.md) || **Azərbaycan** - -

- -
Son dərəcə fərdiləşdirilə bilən modul meneceriniz -

- -

- - Get it on Google Play - Get it on GitHub - -
- - Get it on F-Droid - Get it on GitHub - -

- -

- Build with Webpack - Generate APK Debug - GitHub all releases -

- -

- TSS • - ModConf • - ModFS • - Installer -

- -# Xülasə - -Magisk Module Repo Loader (MMRL) ilə tanış olun - Android-də Magisk, KernelSU ve APatch üçün son modul meneceri. Bu son derəcə konfiqurasiya edilə bilən proqram istifadəçilərə modulları asanlıqla idarə etməyə imkan verir, eyni zamanda reklamlardan tamamilə azaddır. - -# Tələblər - -- Android 8.0 və ya sonrakı versiyalar -- [MMRL-CLI](https://github.com/DerGoogler/MMRL-CLI) -- 4-5 GB RAM (daha aşağı mümkün ola bilər) - - -# Xüsusiyyətləri - -- Əsas biliklər: Sözün əsl mənasında əsas biliklər -- [ModFS](https://github.com/DerGoogler/MMRL/tree/master/docs/ModFS.md): Fərdiləşdirilə Bilən Modul Fayl Sistemi -- [ModConf](https://github.com/DerGoogler/MMRL/blob/master/docs/ModConf/README.md): Modullar üçün dinamik yaradılmış səhifələr təmin edin -- Xüsusi Anbarlar: MRepo və ya GR Fork istifadə edən istənilən anbarı yükləyin - -> Shell, SuFile və daha çoxu kimi daxa çox funksiyalarımızı kəşf etmək üçün [sənədləri](https://github.com/DerGoogler/MMRL/tree/master/docs) oxuyun - -### Dəstəklənən kök meneceri - -- [x] [Magisk](https://github.com/topjohnwu/Magisk) -- [x] [Magisk Delta](https://github.com/HuskyDG/magisk-files) -- [x] [KernelSU](https://github.com/tiann/KernelSU) -- [x] [APatch](https://github.com/bmax121/APatch) - -# Ekran görüntüləri - -

- Screenshot 1 of MMRL - Screenshot 2 of MMRL - Screenshot 3 of MMRL - Screenshot 4 of MMRL - Screenshot 5 of MMRL - Screenshot 6 of MMRL - Screenshot 7 of MMRL - Screenshot 8 of MMRL -

- -# Kredit və Təşəkkürlər - -- [tabler/tabler-icons](https://github.com/tabler/tabler-icons.git) -- [Googlers-Repo/node-native](https://github.com/Googlers-Repo/node-native) -- [topjohnwu/libsu](https://github.com/topjohnwu/libsu) -- [Fox2Code/FoxMagiskModuleManager](https://github.com/Fox2Code/FoxMagiskModuleManager) -- [DerGoogler/dgm-cms](https://github.com/DerGoogler/dgm-cms) -- [Hentai-Web/Core](https://github.com/Hentai-Web/Core) -- [Hentai-Web/Android](https://github.com/Hentai-Web/Android) diff --git a/docs/faq.md b/docs/faq.md deleted file mode 100644 index 83058bd8..00000000 --- a/docs/faq.md +++ /dev/null @@ -1,22 +0,0 @@ -[googleplay-release]: https://play.google.com/store/apps/details?id=com.dergoogler.mmrl -[github-release]: https://github.com/DerGoogler/MMRL -[mmrlini]: https://github.com/DerGoogler/mmrl_install_tools -[mmrlini-release]: https://github.com/DerGoogler/mmrl_install_tools/releases - -# **F**requently **A**sked **Q**uestions - -> We do not recommend to delete the entire `/data/adb/mmrl` folder. This folder contains your saved ModConf Playground file and other config files like from [MMRL Install Tools][mmrlini-release] - -## MMRL does not load correctly or shows a empty screen - -This my due internal config changes or invalid config files - -To solve this, try the following options: - -- reinstall the app from [GitHub Releases][github-release] or the [Google Play Store][googleplay-release] -- clean app data and cache -- removing `/data/adb/mmrl/settings.v*.json` or `/data/adb/mmrl` - -## Cannot install or update modules - -Try updating [MMRL Install Tools][mmrlini-release] or remove the `/data/adb/mmrl/mmrlini.v*.ini` config file diff --git a/docs/faq_az.md b/docs/faq_az.md deleted file mode 100644 index d5191191..00000000 --- a/docs/faq_az.md +++ /dev/null @@ -1,22 +0,0 @@ -[googleplay-release]: https://play.google.com/store/apps/details?id=com.dergoogler.mmrl -[github-release]: https://github.com/DerGoogler/MMRL -[mmrlini]: https://github.com/DerGoogler/mmrl_install_tools -[mmrlini-release]: https://github.com/DerGoogler/mmrl_install_tools/releases - -# **T**ez-Tez **S**oruşulan **S**uallar - -> Biz bütün `/data/adb/mmrl` qovluğunu silməyi tövsiyə etmirik. Bu qovluğa saxladığınız ModConf OyunMeydanı faylı və [MMRL Quraşdırma Alətləri][mmrlini-release] kimi digər konfiqurasiya faylları ehtiva edir. - -## MMRL düzgün şəkildə yüklənmir və ya boş bir ekran göstərir - -Bu, mənim daxili konfiqurasiya dəyişikliklərim və ya etibarsız konfiqurasiya fayllarıma görədir - -Bunu həll etmək üçün aşağıdakı seçimləri sınayın: - -- proqramı [GitHub Releases][github-release] və ya [Google Play Market][googleplay-release] ünvanından yenidən quraşdırın -- proqram məlumatını və keşi təmizləyin -- `/data/adb/mmrl/settings.v*.json` və ya `/data/adb/mmrl` silin - -## Modullar yüklənə və ya yenilənə bilmir - -[MMRL Quraşdırma Alətlərini][mmrlini-release] yeniləməyə cəhd edin və ya `/data/adb/mmrl/mmrlini.v*.ini` konfiqurasiya faylını silin diff --git a/fastlane/metadata/android/ar/full_description.txt b/fastlane/metadata/android/ar/full_description.txt new file mode 100644 index 00000000..5db6bcf0 --- /dev/null +++ b/fastlane/metadata/android/ar/full_description.txt @@ -0,0 +1,9 @@ +MMRL هو تطبيق Android يساعد في إدارة مستودع الإضافت النمطية الخاص بك. + + المميزات: + - إدارة مستودع الإضافات الخاصة بك + - دعم مستودعات متعددة + - دعم Magisk وKernelSU + - إنشاء Jetpack وتصميم Material 3 + + https://github.com/DerGoogler/MMRL diff --git a/fastlane/metadata/android/ar/short_description.txt b/fastlane/metadata/android/ar/short_description.txt new file mode 100644 index 00000000..f3992635 --- /dev/null +++ b/fastlane/metadata/android/ar/short_description.txt @@ -0,0 +1 @@ +مدير إضافات لـMagisk وKernelSU diff --git a/fastlane/metadata/android/ar/title.txt b/fastlane/metadata/android/ar/title.txt new file mode 100644 index 00000000..7cfd0c2a --- /dev/null +++ b/fastlane/metadata/android/ar/title.txt @@ -0,0 +1 @@ +MMRL diff --git a/fastlane/metadata/android/en-US/full_description.txt b/fastlane/metadata/android/en-US/full_description.txt index 1fcc4754..e50f7e30 100644 --- a/fastlane/metadata/android/en-US/full_description.txt +++ b/fastlane/metadata/android/en-US/full_description.txt @@ -1,23 +1,9 @@ -MMRL is a highly configurable app allows you to manage modules effortlessly, all while being completely free of ads. +MMRL is an Android app that helps manage your own modules repository. +Features: +- Manage your modules repository +- Support multiple repositories +- Support Magisk and KernelSU +- Jetpack Compose & Material Design 3 -

What root managers support MMRL?

- ✅ Magisk - ✅ KernelSU - ✅ APatch - - -

Which features does MMRL offer?

- 🫂 User friendly UI - ⚙️ Configurable module pages (ModConf) - 🗂️ Customizing all paths (such as "/data/adb/modules", if your root uses an other path) - ✏️ ModConf Playground - 📚 Viewing installed and updatable modules - 📈 Adding covers, screenshots, verification, dependencies and more (repo based) - 🔍 Search functionality - 📄 Adding up to 5 repositories - -Requirements: -- A rooted Android phone -- A installed WebView engine -- MMRL-CLI to use the full functionality \ No newline at end of file +https://github.com/DerGoogler/MMRL \ No newline at end of file diff --git a/fastlane/metadata/android/en-US/images/icon.png b/fastlane/metadata/android/en-US/images/icon.png index 074aab12..e29b65f7 100644 Binary files a/fastlane/metadata/android/en-US/images/icon.png and b/fastlane/metadata/android/en-US/images/icon.png differ diff --git a/fastlane/metadata/android/en-US/images/phoneScreenshots/1.png b/fastlane/metadata/android/en-US/images/phoneScreenshots/1.png index 78476fd9..2f8fabec 100644 Binary files a/fastlane/metadata/android/en-US/images/phoneScreenshots/1.png and b/fastlane/metadata/android/en-US/images/phoneScreenshots/1.png differ diff --git a/fastlane/metadata/android/en-US/images/phoneScreenshots/2.png b/fastlane/metadata/android/en-US/images/phoneScreenshots/2.png index bfad7d22..867f0260 100644 Binary files a/fastlane/metadata/android/en-US/images/phoneScreenshots/2.png and b/fastlane/metadata/android/en-US/images/phoneScreenshots/2.png differ diff --git a/fastlane/metadata/android/en-US/images/phoneScreenshots/3.png b/fastlane/metadata/android/en-US/images/phoneScreenshots/3.png index 884d3072..0bbe348b 100644 Binary files a/fastlane/metadata/android/en-US/images/phoneScreenshots/3.png and b/fastlane/metadata/android/en-US/images/phoneScreenshots/3.png differ diff --git a/fastlane/metadata/android/en-US/images/phoneScreenshots/4.png b/fastlane/metadata/android/en-US/images/phoneScreenshots/4.png index c755e8d9..d8736c09 100644 Binary files a/fastlane/metadata/android/en-US/images/phoneScreenshots/4.png and b/fastlane/metadata/android/en-US/images/phoneScreenshots/4.png differ diff --git a/fastlane/metadata/android/en-US/images/phoneScreenshots/5.png b/fastlane/metadata/android/en-US/images/phoneScreenshots/5.png index 3497baf7..53e83fa4 100644 Binary files a/fastlane/metadata/android/en-US/images/phoneScreenshots/5.png and b/fastlane/metadata/android/en-US/images/phoneScreenshots/5.png differ diff --git a/fastlane/metadata/android/en-US/images/phoneScreenshots/6.png b/fastlane/metadata/android/en-US/images/phoneScreenshots/6.png index 9b932635..0c8d7f00 100644 Binary files a/fastlane/metadata/android/en-US/images/phoneScreenshots/6.png and b/fastlane/metadata/android/en-US/images/phoneScreenshots/6.png differ diff --git a/fastlane/metadata/android/en-US/images/phoneScreenshots/7.png b/fastlane/metadata/android/en-US/images/phoneScreenshots/7.png deleted file mode 100644 index f3916239..00000000 Binary files a/fastlane/metadata/android/en-US/images/phoneScreenshots/7.png and /dev/null differ diff --git a/fastlane/metadata/android/en-US/short_description.txt b/fastlane/metadata/android/en-US/short_description.txt index 6af6f856..7ed72ef2 100644 --- a/fastlane/metadata/android/en-US/short_description.txt +++ b/fastlane/metadata/android/en-US/short_description.txt @@ -1 +1 @@ -Introducing MMRL - the ultimate manager for Magisk, KernelSU, and APatch. \ No newline at end of file +A modules manager for Magisk & KernelSU diff --git a/fastlane/metadata/android/en-US/title.txt b/fastlane/metadata/android/en-US/title.txt new file mode 100644 index 00000000..4ccb1641 --- /dev/null +++ b/fastlane/metadata/android/en-US/title.txt @@ -0,0 +1 @@ +MMRL \ No newline at end of file diff --git a/fastlane/metadata/android/ru-RU/full_description.txt b/fastlane/metadata/android/ru-RU/full_description.txt new file mode 100644 index 00000000..dcb31c87 --- /dev/null +++ b/fastlane/metadata/android/ru-RU/full_description.txt @@ -0,0 +1,9 @@ +Это приложение помогает управлять вашим собственным репозиторием модулей. + +Возможности: +- Управление репозиторием модулей +- Поддержка нескольких репозиториев +- Поддержка Magisk и KernelSU +- JetPack Compose и Материальный Дизайн 3 + +https://github.com/mrepoapp/mrepo diff --git a/fastlane/metadata/android/ru-RU/short_description.txt b/fastlane/metadata/android/ru-RU/short_description.txt new file mode 100644 index 00000000..9ff5f092 --- /dev/null +++ b/fastlane/metadata/android/ru-RU/short_description.txt @@ -0,0 +1 @@ +Менеджер модулей для Magisk и KernelSU diff --git a/fastlane/metadata/android/ru-RU/title.txt b/fastlane/metadata/android/ru-RU/title.txt new file mode 100644 index 00000000..7cfd0c2a --- /dev/null +++ b/fastlane/metadata/android/ru-RU/title.txt @@ -0,0 +1 @@ +MMRL diff --git a/gradle.properties b/gradle.properties index 81cf2daa..e0dbbdbe 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,22 +1,9 @@ -# Project-wide Gradle settings. +org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 +org.gradle.caching=true +org.gradle.configuration-cache=true -# IDE (e.g. Android Studio) users: -# Gradle settings configured through the IDE *will override* -# any settings specified in this file. - -# For more details on how to configure your build environment visit -# http://www.gradle.org/docs/current/userguide/build_environment.html - -# Specifies the JVM arguments used for the daemon process. -# The setting is particularly useful for tweaking memory settings. -# Default value: -Xmx10248m -XX:MaxPermSize=256m -# org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8 - -# When configured, Gradle will run in incubating parallel mode. -# This option should only be used with decoupled projects. More details, visit -# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects -# org.gradle.parallel=true android.useAndroidX=true -android.enableJetifier=true -org.gradle.daemon=false -android.suppressUnsupportedCompileSdk=34 +android.nonTransitiveRClass=true + +kapt.include.compile.classpath=false +kotlin.code.style=official \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml new file mode 100644 index 00000000..f0f63b2c --- /dev/null +++ b/gradle/libs.versions.toml @@ -0,0 +1,105 @@ +[versions] +androidGradlePlugin = "8.4.1" +androidxActivity = "1.9.0" +androidxAnnotation = "1.8.0" +androidxAppCompat = "1.6.1" +androidxCompose = "1.7.0-beta01" +androidxComposeMaterial3 = "1.2.1" +androidxCore = "1.13.1" +androidxCoreSplashscreen = "1.0.1" +androidxDataStore = "1.1.1" +androidxDocumentFile = "1.0.1" +androidxHiltNavigationCompose = "1.2.0" +androidxLifecycle = "2.8.0" +androidxNavigation = "2.7.7" +androidxRoom = "2.6.1" +coilCompose = "2.1.0" +hiddenApiRefine = "4.4.0" +hilt = "2.51.1" +kotlin = "2.0.0" +kotlinReflect = "1.9.24" +kotlinxCoroutines = "1.8.1" +kotlinxDatetime = "0.6.0" +ksp = "2.0.0-1.0.21" +libsu = "5.2.2" +protobuf = "4.27.0" +protobufPlugin = "0.9.4" +semver = "2.0.0" +shizuku = "13.1.5" +squareRetrofit = "2.11.0" +squareOkhttp = "4.12.0" +squareMoshi = "1.15.1" + +[libraries] +androidx-activity-compose = { group = "androidx.activity", name = "activity-compose", version.ref = "androidxActivity" } +androidx-annotation = { group = "androidx.annotation", name = "annotation", version.ref = "androidxAnnotation" } +androidx-appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "androidxAppCompat" } +androidx-compose-material3 = { group = "androidx.compose.material3", name = "material3", version.ref = "androidxComposeMaterial3" } +androidx-compose-ui = { group = "androidx.compose.ui", name = "ui", version.ref = "androidxCompose" } +androidx-compose-ui-tooling = { group = "androidx.compose.ui", name = "ui-tooling", version.ref = "androidxCompose" } +androidx-compose-ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-tooling-preview", version.ref = "androidxCompose" } +androidx-compose-ui-util = { group = "androidx.compose.ui", name = "ui-util", version.ref = "androidxCompose" } +androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "androidxCore" } +androidx-core-splashscreen = { group = "androidx.core", name = "core-splashscreen", version.ref = "androidxCoreSplashscreen" } +androidx-datastore-core = { group = "androidx.datastore", name = "datastore", version.ref = "androidxDataStore" } +androidx-documentfile = { group = "androidx.documentfile", name = "documentfile", version.ref = "androidxDocumentFile" } +androidx-hilt-navigation-compose = { group = "androidx.hilt", name = "hilt-navigation-compose", version.ref = "androidxHiltNavigationCompose" } +androidx-lifecycle-livedata-ktx = { group = "androidx.lifecycle", name = "lifecycle-livedata-ktx", version.ref = "androidxLifecycle" } +androidx-lifecycle-runtime-compose = { group = "androidx.lifecycle", name = "lifecycle-runtime-compose", version.ref = "androidxLifecycle" } +androidx-lifecycle-service = { group = "androidx.lifecycle", name = "lifecycle-service", version.ref = "androidxLifecycle" } +androidx-lifecycle-viewModel-compose = { group = "androidx.lifecycle", name = "lifecycle-viewmodel-compose", version.ref = "androidxLifecycle" } +androidx-navigation-compose = { group = "androidx.navigation", name = "navigation-compose", version.ref = "androidxNavigation" } +androidx-room-compiler = { group = "androidx.room", name = "room-compiler", version.ref = "androidxRoom" } +androidx-room-ktx = { group = "androidx.room", name = "room-ktx", version.ref = "androidxRoom" } +androidx-room-runtime = { group = "androidx.room", name = "room-runtime", version.ref = "androidxRoom" } +coil-compose = { module = "io.coil-kt:coil-compose", version.ref = "coilCompose" } +hilt-android = { group = "com.google.dagger", name = "hilt-android", version.ref = "hilt" } +hilt-compiler = { group = "com.google.dagger", name = "hilt-android-compiler", version.ref = "hilt" } +kotlin-reflect = { module = "org.jetbrains.kotlin:kotlin-reflect", version.ref = "kotlinReflect" } +kotlinx-coroutines-android = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-android", version.ref = "kotlinxCoroutines" } +kotlinx-datetime = { group = "org.jetbrains.kotlinx", name = "kotlinx-datetime", version.ref = "kotlinxDatetime" } +libsu-core = { group = "com.github.topjohnwu.libsu", name = "core", version.ref = "libsu" } +libsu-service = { group = "com.github.topjohnwu.libsu", name = "service", version.ref = "libsu" } +protobuf-kotlin-lite = { group = "com.google.protobuf", name = "protobuf-kotlin-lite", version.ref = "protobuf" } +protobuf-protoc = { group = "com.google.protobuf", name = "protoc", version.ref = "protobuf" } +rikka-refine-annotation = { module = "dev.rikka.tools.refine:annotation", version.ref = "hiddenApiRefine" } +rikka-refine-compiler = { module = "dev.rikka.tools.refine:annotation-processor", version.ref = "hiddenApiRefine" } +rikka-refine-runtime = { module = "dev.rikka.tools.refine:runtime", version.ref = "hiddenApiRefine" } +rikka-shizuku-api = { module = "dev.rikka.shizuku:api", version.ref = "shizuku" } +rikka-shizuku-provider = { module = "dev.rikka.shizuku:provider", version.ref = "shizuku" } +semver = { module = "io.github.z4kn4fein:semver", version.ref = "semver" } +square-retrofit = { group = "com.squareup.retrofit2", name = "retrofit", version.ref = "squareRetrofit" } +square-retrofit-moshi = { group = "com.squareup.retrofit2", name = "converter-moshi", version.ref = "squareRetrofit" } +square-okhttp = { group = "com.squareup.okhttp3", name = "okhttp", version.ref = "squareOkhttp" } +square-okhttp-dnsoverhttps = { group = "com.squareup.okhttp3", name = "okhttp-dnsoverhttps", version.ref = "squareOkhttp" } +square-logging-interceptor = { group = "com.squareup.okhttp3", name = "logging-interceptor", version.ref = "squareOkhttp" } +square-moshi = { group = "com.squareup.moshi", name = "moshi", version.ref = "squareMoshi" } +square-moshi-kotlin = { group = "com.squareup.moshi", name = "moshi-kotlin-codegen", version.ref = "squareMoshi" } + +markwon-core = "io.noties.markwon:core:4.6.2" +hiddenApiBypass = "org.lsposed.hiddenapibypass:hiddenapibypass:4.3" +timber = "com.jakewharton.timber:timber:5.0.1" + +# Dependencies of the included build-logic +android-gradle = { group = "com.android.tools.build", name = "gradle", version.ref = "androidGradlePlugin" } +compose-gradle= { module = "org.jetbrains.kotlin:compose-compiler-gradle-plugin", version.ref = "kotlin" } +kotlin-gradle = { group = "org.jetbrains.kotlin", name = "kotlin-gradle-plugin", version.ref = "kotlin" } +ksp-gradle = { group = "com.google.devtools.ksp", name = "com.google.devtools.ksp.gradle.plugin", version.ref = "ksp" } + +[plugins] +android-application = { id = "com.android.application", version.ref = "androidGradlePlugin" } +android-library = { id = "com.android.library", version.ref = "androidGradlePlugin" } +compose-compiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" } +hilt = { id = "com.google.dagger.hilt.android", version.ref = "hilt" } +kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" } +kotlin-parcelize = { id = "org.jetbrains.kotlin.plugin.parcelize", version.ref = "kotlin" } +ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" } +rikka-refine = { id = "dev.rikka.tools.refine", version.ref = "hiddenApiRefine" } +protobuf = { id = "com.google.protobuf", version.ref = "protobufPlugin" } + +# Plugins defined by this project +self-application = { id = "self.application", version = "unspecified" } +self-library = { id = "self.library", version = "unspecified" } +self-compose = { id = "self.compose", version = "unspecified" } +self-hilt = { id = "self.hilt", version = "unspecified" } +self-room = { id = "self.room", version = "unspecified" } diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index 8c0fb64a..d64cd491 100644 Binary files a/gradle/wrapper/gradle-wrapper.jar and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 59968b76..a000404b 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ -#Thu Aug 15 17:04:39 CEST 2024 -distributionBase=GRADLE_USER_HOME -distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.10-bin.zip -zipStoreBase=GRADLE_USER_HOME -zipStorePath=wrapper/dists +#Tue Sep 24 18:52:42 CEST 2024 +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-bin.zip +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew index 91a7e269..1aa94a42 100644 --- a/gradlew +++ b/gradlew @@ -1,79 +1,127 @@ -#!/usr/bin/env bash +#!/bin/sh + +# +# Copyright © 2015-2021 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ############################################################################## -## -## Gradle start up script for UN*X -## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# ############################################################################## -# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -DEFAULT_JVM_OPTS="" +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done -APP_NAME="Gradle" -APP_BASE_NAME=`basename "$0"` +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit # Use the maximum available, or set MAX_FD != -1 to use that value. -MAX_FD="maximum" +MAX_FD=maximum -warn ( ) { +warn () { echo "$*" -} +} >&2 -die ( ) { +die () { echo echo "$*" echo exit 1 -} +} >&2 # OS specific support (must be 'true' or 'false'). cygwin=false msys=false darwin=false -case "`uname`" in - CYGWIN* ) - cygwin=true - ;; - Darwin* ) - darwin=true - ;; - MINGW* ) - msys=true - ;; +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; esac -# For Cygwin, ensure paths are in UNIX format before anything is touched. -if $cygwin ; then - [ -n "$JAVA_HOME" ] && JAVA_HOME=`cygpath --unix "$JAVA_HOME"` -fi - -# Attempt to set APP_HOME -# Resolve links: $0 may be a link -PRG="$0" -# Need this for relative symlinks. -while [ -h "$PRG" ] ; do - ls=`ls -ld "$PRG"` - link=`expr "$ls" : '.*-> \(.*\)$'` - if expr "$link" : '/.*' > /dev/null; then - PRG="$link" - else - PRG=`dirname "$PRG"`"/$link" - fi -done -SAVED="`pwd`" -cd "`dirname \"$PRG\"`/" >&- -APP_HOME="`pwd -P`" -cd "$SAVED" >&- - CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + # Determine the Java command to use to start the JVM. if [ -n "$JAVA_HOME" ] ; then if [ -x "$JAVA_HOME/jre/sh/java" ] ; then # IBM's JDK on AIX uses strange locations for the executables - JAVACMD="$JAVA_HOME/jre/sh/java" + JAVACMD=$JAVA_HOME/jre/sh/java else - JAVACMD="$JAVA_HOME/bin/java" + JAVACMD=$JAVA_HOME/bin/java fi if [ ! -x "$JAVACMD" ] ; then die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME @@ -82,83 +130,120 @@ Please set the JAVA_HOME variable in your environment to match the location of your Java installation." fi else - JAVACMD="java" - which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + JAVACMD=java + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. Please set the JAVA_HOME variable in your environment to match the location of your Java installation." + fi fi # Increase the maximum file descriptors if we can. -if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then - MAX_FD_LIMIT=`ulimit -H -n` - if [ $? -eq 0 ] ; then - if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then - MAX_FD="$MAX_FD_LIMIT" - fi - ulimit -n $MAX_FD - if [ $? -ne 0 ] ; then - warn "Could not set maximum file descriptor limit: $MAX_FD" - fi - else - warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" - fi +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=SC2039,SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac fi -# For Darwin, add options to specify how the application appears in the dock -if $darwin; then - GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" -fi +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) -# For Cygwin, switch paths to Windows format before running java -if $cygwin ; then - APP_HOME=`cygpath --path --mixed "$APP_HOME"` - CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` - - # We build the pattern for arguments to be converted via cygpath - ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` - SEP="" - for dir in $ROOTDIRSRAW ; do - ROOTDIRS="$ROOTDIRS$SEP$dir" - SEP="|" - done - OURCYGPATTERN="(^($ROOTDIRS))" - # Add a user-defined pattern to the cygpath arguments - if [ "$GRADLE_CYGPATTERN" != "" ] ; then - OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" - fi # Now convert the arguments - kludge to limit ourselves to /bin/sh - i=0 - for arg in "$@" ; do - CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` - CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option - - if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition - eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` - else - eval `echo args$i`="\"$arg\"" + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) fi - i=$((i+1)) + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg done - case $i in - (0) set -- ;; - (1) set -- "$args0" ;; - (2) set -- "$args0" "$args1" ;; - (3) set -- "$args0" "$args1" "$args2" ;; - (4) set -- "$args0" "$args1" "$args2" "$args3" ;; - (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; - (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; - (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; - (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; - (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; - esac fi -# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules -function splitJvmOpts() { - JVM_OPTS=("$@") -} -eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS -JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" -exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" +# 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, 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" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat index 8a0b282a..7101f8e4 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -1,90 +1,92 @@ -@if "%DEBUG%" == "" @echo off -@rem ########################################################################## -@rem -@rem Gradle startup script for Windows -@rem -@rem ########################################################################## - -@rem Set local scope for the variables with windows NT shell -if "%OS%"=="Windows_NT" setlocal - -@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -set DEFAULT_JVM_OPTS= - -set DIRNAME=%~dp0 -if "%DIRNAME%" == "" set DIRNAME=. -set APP_BASE_NAME=%~n0 -set APP_HOME=%DIRNAME% - -@rem Find java.exe -if defined JAVA_HOME goto findJavaFromJavaHome - -set JAVA_EXE=java.exe -%JAVA_EXE% -version >NUL 2>&1 -if "%ERRORLEVEL%" == "0" goto init - -echo. -echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. - -goto fail - -:findJavaFromJavaHome -set JAVA_HOME=%JAVA_HOME:"=% -set JAVA_EXE=%JAVA_HOME%/bin/java.exe - -if exist "%JAVA_EXE%" goto init - -echo. -echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. - -goto fail - -:init -@rem Get command-line arguments, handling Windowz variants - -if not "%OS%" == "Windows_NT" goto win9xME_args -if "%@eval[2+2]" == "4" goto 4NT_args - -:win9xME_args -@rem Slurp the command line arguments. -set CMD_LINE_ARGS= -set _SKIP=2 - -:win9xME_args_slurp -if "x%~1" == "x" goto execute - -set CMD_LINE_ARGS=%* -goto execute - -:4NT_args -@rem Get arguments from the 4NT Shell from JP Software -set CMD_LINE_ARGS=%$ - -:execute -@rem Setup the command line - -set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar - -@rem Execute Gradle -"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% - -:end -@rem End local scope for the variables with windows NT shell -if "%ERRORLEVEL%"=="0" goto mainEnd - -:fail -rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of -rem the _cmd.exe /c_ return code! -if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 -exit /b 1 - -:mainEnd -if "%OS%"=="Windows_NT" endlocal - -:omega +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if %ERRORLEVEL% equ 0 goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/hidden-api/build.gradle.kts b/hidden-api/build.gradle.kts new file mode 100644 index 00000000..a0286637 --- /dev/null +++ b/hidden-api/build.gradle.kts @@ -0,0 +1,13 @@ +plugins { + alias(libs.plugins.self.library) +} + +android { + namespace = "com.dergoogler.mmrl.hidden_api" +} + +dependencies { + annotationProcessor(libs.rikka.refine.compiler) + compileOnly(libs.rikka.refine.annotation) + compileOnly(libs.androidx.annotation) +} \ No newline at end of file diff --git a/hidden-api/src/main/java/android/app/ActivityThread.java b/hidden-api/src/main/java/android/app/ActivityThread.java new file mode 100644 index 00000000..c1b50074 --- /dev/null +++ b/hidden-api/src/main/java/android/app/ActivityThread.java @@ -0,0 +1,7 @@ +package android.app; + +public class ActivityThread { + public static Application currentApplication() { + throw new RuntimeException("Stub!"); + } +} \ No newline at end of file diff --git a/hidden-api/src/main/java/android/os/IPowerManager.java b/hidden-api/src/main/java/android/os/IPowerManager.java new file mode 100644 index 00000000..e6f23a29 --- /dev/null +++ b/hidden-api/src/main/java/android/os/IPowerManager.java @@ -0,0 +1,12 @@ +package android.os; + +public interface IPowerManager extends IInterface { + void reboot(boolean confirm, String reason, boolean wait) throws RemoteException; + + abstract class Stub extends Binder implements IPowerManager { + + public static IPowerManager asInterface(IBinder binder) { + throw new RuntimeException("Stub!"); + } + } +} diff --git a/hidden-api/src/main/java/android/os/PowerManagerHidden.java b/hidden-api/src/main/java/android/os/PowerManagerHidden.java new file mode 100644 index 00000000..6b8ec379 --- /dev/null +++ b/hidden-api/src/main/java/android/os/PowerManagerHidden.java @@ -0,0 +1,13 @@ +package android.os; + +import androidx.annotation.RequiresApi; + +import dev.rikka.tools.refine.RefineAs; + +@RefineAs(PowerManager.class) +public class PowerManagerHidden { + @RequiresApi(30) + public static boolean isRebootingUserspaceSupportedImpl() { + throw new RuntimeException("Stub!"); + } +} diff --git a/hidden-api/src/main/java/android/os/SELinux.java b/hidden-api/src/main/java/android/os/SELinux.java new file mode 100644 index 00000000..79c66bff --- /dev/null +++ b/hidden-api/src/main/java/android/os/SELinux.java @@ -0,0 +1,6 @@ +package android.os; + +public class SELinux { + + public static native String getContext(); +} \ No newline at end of file diff --git a/hidden-api/src/main/java/android/os/ServiceManager.java b/hidden-api/src/main/java/android/os/ServiceManager.java new file mode 100644 index 00000000..6718fb30 --- /dev/null +++ b/hidden-api/src/main/java/android/os/ServiceManager.java @@ -0,0 +1,7 @@ +package android.os; + +public class ServiceManager { + public static IBinder getService(String name) { + throw new RuntimeException("Stub!"); + } +} \ No newline at end of file diff --git a/licensefix.js b/licensefix.js deleted file mode 100644 index 38ce641a..00000000 --- a/licensefix.js +++ /dev/null @@ -1,40 +0,0 @@ -const fs = require("fs"); -const path = require("path"); -const package = require(path.resolve(__dirname, "package.json")); - -const NODE_MODULES_PATH = path.resolve(__dirname, "node_modules"); -const OUTPUT = path.resolve(__dirname, "src", "util", "licenses.json"); -const allDependencies = []; - -Object.keys(package.dependencies).forEach((dependency) => { - const depPackage = require(path.resolve(__dirname, NODE_MODULES_PATH, dependency, "package.json")); - - if (depPackage) { - function getSource() { - if (depPackage.repository) { - return depPackage.repository.url ? depPackage.repository.url : null; - } else { - return null; - } - } - - function getAuthor() { - if (depPackage.author) { - return depPackage.author.name ? depPackage.author.name : null; - } else { - return null; - } - } - - allDependencies.push({ - name: depPackage.name, - author: getAuthor(), - license: depPackage.license, - description: depPackage.description, - version: depPackage.version, - source: `https://www.npmjs.com/package/${depPackage.name}`, - }); - } -}); - -fs.writeFileSync(OUTPUT, JSON.stringify(allDependencies, null, 2)); diff --git a/package.json b/package.json deleted file mode 100644 index 50a1a64f..00000000 --- a/package.json +++ /dev/null @@ -1,171 +0,0 @@ -{ - "name": "com.dergoogler.mmrl.web", - "description": "", - "config": { - "cname": "mmrl.dergoogler.com", - "application_id": "com.dergoogler.mmrl", - "min_sdk": 26, - "target_sdk": 34, - "version_name": "3.24.33", - "version_code": 32433, - "verified_hosts": [ - [ - "mmrl", - "i" - ], - [ - "localhost", - "i" - ], - [ - "mmrl.dergoogler.com", - "i" - ], - [ - "dergoogler.com", - "i" - ], - [ - "dergoogler.github.io", - "i" - ], - [ - "gr.dergoogler.com", - "i" - ], - [ - "googlers-repo.github.io", - "i" - ], - [ - "(localhost|\\b(?:(?:25[0-5]|2[0-4]\\d|[01]?\\d\\d?)\\.){3}(?:25[0-5]|2[0-4]\\d|[01]?\\d\\d?)(?::\\d{0,4})?\\b)", - "g" - ] - ] - }, - "main": "index.tsx", - "keywords": [], - "author": "Der_Googler", - "license": "GPL-3.0", - "scripts": { - "start:dev": "webpack-dev-server --open --config webpack.dev.ts", - "start:prod": "npm run licensefix && webpack-dev-server --open --config webpack.prod.ts", - "web:dev": "webpack --config webpack.dev.ts", - "web:prod": "npm run licensefix && webpack --config webpack.prod.ts", - "web:prod-app": "npm run web:prod", - "licensefix": "node licensefix.js", - "deploy": "node deploy.js" - }, - "resolutions": { - "react": "^18.2.0", - "react-dom": "^18.1.0", - "@types/webpack": "^5.28.0" - }, - "dependencies": { - "@babel/runtime": "^7.23.2", - "@babel/standalone": "^7.24.0", - "@emotion/react": "^11.9.0", - "@emotion/styled": "^11.8.1", - "@giscus/react": "^2.4.0", - "@monaco-editor/react": "^4.6.0", - "@mui/icons-material": "^5.16.5", - "@mui/lab": "^5.0.0-alpha.160", - "@mui/material": "^5.15.2", - "@nyariv/sandboxjs": "^0.8.23", - "@primer/octicons-react": "^19.9.0", - "@zenfs/core": "^0.17.1", - "@zenfs/dom": "^0.2.15", - "ajv": "^8.12.0", - "anser": "^2.1.1", - "axios": "^1.6.2", - "default-composer": "^0.6.0", - "eruda": "^3.0.0", - "escape-carriage": "^1.3.1", - "flatlist-react": "^1.5.14", - "googlers-tools": "^1.2.8", - "highlight.js": "^11.6.0", - "ini": "^4.1.1", - "linkify-it": "^5.0.0", - "localforage": "^1.10.0", - "markdown-to-jsx": "^7.4.0", - "material-icons": "^1.10.8", - "material-ui-confirm": "^3.0.16", - "modfs": "^1.4.2", - "monaco-editor": "^0.48.0", - "monaco-editor-core": "^0.50.0", - "monaco-languageclient": "^6.5.0", - "object-assign": "^4.1.1", - "onsenui": "^2.12.8", - "properties-file": "^3.2.10", - "react": "^18.2.0", - "react-device-detect": "^2.2.3", - "react-disappear": "^1.1.3", - "react-dom": "^18.2.0", - "react-fast-marquee": "^1.6.1", - "react-onsenui": "^1.13.2", - "react-render-tools": "^1.0.1", - "react-syntax-highlighter": "^15.5.0", - "react-transition-group": "^4.4.5", - "react-zoom-pan-pinch": "^3.3.0", - "reflect-metadata": "^0.2.2", - "semver": "^7.6.3", - "underscore": "^1.13.6", - "usehooks-ts": "^3.1.0", - "uuid": "^10.0.0", - "yaml": "^2.3.4" - }, - "devDependencies": { - "@babel/core": "^7.24.0", - "@babel/preset-env": "^7.23.6", - "@babel/preset-react": "^7.23.3", - "@octokit/rest": "^21.0.1", - "@types/babel__core": "^7.20.2", - "@types/babel__standalone": "^7.1.7", - "@types/fs-extra": "^11.0.4", - "@types/gh-pages": "^6.1.0", - "@types/ini": "^4.1.0", - "@types/node": "^18.19.50", - "@types/object-assign": "^4.0.30", - "@types/react": "^18.2.67", - "@types/react-dom": "^18.0.2", - "@types/react-onsenui": "^2.9.17", - "@types/react-syntax-highlighter": "^15.5.2", - "@types/semver": "^7.5.8", - "@types/uglifyjs-webpack-plugin": "^1.1.2", - "@types/underscore": "^1.11.15", - "@types/webpack": "^5.28.5", - "babel-loader": "^9.1.3", - "buffer": "^6.0.3", - "cache-loader": "^4.1.0", - "commander": "^11.0.0", - "css-loader": "^6.8.1", - "css-minimizer-webpack-plugin": "^4.0.0", - "dotenv": "^16.4.5", - "file-loader": "^6.2.0", - "filemanager-webpack-plugin": "^8.0.0", - "fs-extra": "^11.2.0", - "gh-pages": "^6.1.1", - "html-webpack-plugin": "^5.6.0", - "image-webpack-loader": "^8.1.0", - "js-yaml-loader": "^1.2.2", - "license-checker": "^25.0.1", - "mini-css-extract-plugin": "^2.9.0", - "postcss-loader": "^7.3.3", - "raw-loader": "^4.0.2", - "sass": "^1.49.8", - "sass-loader": "^13.0.2", - "style-loader": "^3.3.3", - "terser-webpack-plugin": "^5.3.9", - "thread-loader": "^4.0.2", - "ts-loader": "^9.3.0", - "ts-node": "^10.9.2", - "tslib": "^2.4.0", - "typescript": "^5.2.2", - "url-loader": "^4.1.1", - "vscode": "^1.1.37", - "webpack": "^5.94.0", - "webpack-cli": "^5.1.4", - "webpack-dev-server": "^4.15.1", - "yaml-loader": "^0.8.0" - } -} diff --git a/settings.gradle b/settings.gradle deleted file mode 100644 index de7bdb7f..00000000 --- a/settings.gradle +++ /dev/null @@ -1,2 +0,0 @@ -rootProject.name = "MMRL" -include ':app' diff --git a/settings.gradle.kts b/settings.gradle.kts new file mode 100644 index 00000000..b13af60f --- /dev/null +++ b/settings.gradle.kts @@ -0,0 +1,25 @@ +enableFeaturePreview("STABLE_CONFIGURATION_CACHE") +enableFeaturePreview("TYPESAFE_PROJECT_ACCESSORS") + +dependencyResolutionManagement { + repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) + repositories { + google() + mavenCentral() + maven("https://jitpack.io") + } +} + +pluginManagement { + includeBuild("build-logic") + repositories { + google() + mavenCentral() + gradlePluginPortal() + } +} + +rootProject.name = "MMRL" +include(":app") +include(":hidden-api") +include(":compat") \ No newline at end of file diff --git a/src/activitys/AboutActivity.tsx b/src/activitys/AboutActivity.tsx deleted file mode 100644 index 91dd441d..00000000 --- a/src/activitys/AboutActivity.tsx +++ /dev/null @@ -1,155 +0,0 @@ -import { Page } from "@Components/onsenui/Page"; -import { Toolbar } from "@Components/onsenui/Toolbar"; -import { useActivity } from "@Hooks/useActivity"; -import { useStrings } from "@Hooks/useStrings"; -import { useTheme } from "@Hooks/useTheme"; - -import Badge from "@mui/material/Badge"; -import List from "@mui/material/List"; -import ListItem from "@mui/material/ListItem"; -import ListItemText from "@mui/material/ListItemText"; -import Avatar from "@mui/material/Avatar"; -import Typography from "@mui/material/Typography"; -import Stack from "@mui/material/Stack"; -import CodeRoundedIcon from "@mui/icons-material/CodeRounded"; -import { Shell } from "@Native/Shell"; -import { ListSubheader } from "@mui/material"; -import React from "react"; -import { BuildConfig } from "@Native/BuildConfig"; -import { useFormatDate } from "@Hooks/useFormatDate"; - -const checkRoot = (): string | undefined => { - if (Shell.isMagiskSU()) { - return "assets/MagiskSULogo.png"; - } else if (Shell.isKernelSU()) { - return "assets/KernelSULogo.png"; - } else if (Shell.isAPatchSU()) { - return "assets/APatchSULogo.png"; - } else { - return undefined; - } -}; - -const AboutActivity = () => { - const { strings } = useStrings(); - const { context } = useActivity(); - - const renderToolbar = () => { - return ( - - - - - {strings("about")} - - ); - }; - - // false to ignore multiplying - const date = useFormatDate(BuildConfig.BUILD_DATE, false); - - type ListRender = { - title: string; - content: Array<{ primary: string; secondary: string | number }>; - }; - - const list = React.useMemo( - () => [ - { - title: "App", - content: [ - { primary: "Name", secondary: BuildConfig.APPLICATION_ID }, - { primary: "Version", secondary: `v${BuildConfig.VERSION_NAME} (${BuildConfig.VERSION_CODE})` }, - { primary: "Build date", secondary: date }, - { primary: "Build type", secondary: BuildConfig.BUILD_TYPE }, - ], - }, - { - title: "Root", - content: [ - { primary: "Root manager", secondary: Shell.getRootManager() }, - { primary: "Root version", secondary: `${Shell.VERSION_NAME().replace(/(.+):(.+)/gim, "$1")} (${Shell.VERSION_CODE()})` }, - ], - }, - { - title: "User", - content: [ - { primary: "Name", secondary: Shell.pw_name() }, - { primary: "User ID", secondary: Shell.pw_uid() }, - { primary: "Group ID", secondary: Shell.pw_gid() }, - ], - }, - ], - [] - ); - - return ( - - - - ({ - width: 40, - height: 40, - borderRadius: "unset", - bgcolor: "transparent", - })} - src={checkRoot()} - > - } - > - - - - - - - MMRL - - - {list.map((l) => ( - {l.title} : undefined} - > - {l.content.map((c) => ( - - - - - {c.primary} - - - {c.secondary} - - - - } - /> - - ))} - - ))} - - - - ); -}; - -export default AboutActivity; diff --git a/src/activitys/CommentsActivity.tsx b/src/activitys/CommentsActivity.tsx deleted file mode 100644 index ad9adf8f..00000000 --- a/src/activitys/CommentsActivity.tsx +++ /dev/null @@ -1,52 +0,0 @@ -import { Page } from "@Components/onsenui/Page"; -import { Toolbar } from "@Components/onsenui/Toolbar"; -import { useActivity } from "@Hooks/useActivity"; -import { useStrings } from "@Hooks/useStrings"; -import ArrowBackIcon from "@mui/icons-material/ArrowBack"; -import { useSettings } from "@Hooks/useSettings"; -import Giscus from "@giscus/react"; - -type Extra = { - id: string; -}; - -const CommentsActivity = () => { - const { strings } = useStrings(); - const [language] = useSettings("language"); - const { context, extra } = useActivity(); - - const renderToolbar = () => { - return ( - - - - - {strings("comments")} - - ); - }; - - return ( - - - - - - ); -}; - -export { CommentsActivity }; diff --git a/src/activitys/DescriptonActivity.tsx b/src/activitys/DescriptonActivity.tsx deleted file mode 100644 index 9f3ecf10..00000000 --- a/src/activitys/DescriptonActivity.tsx +++ /dev/null @@ -1,106 +0,0 @@ -import { ProgressCircular } from "react-onsenui"; -import { Markup } from "@Components/Markdown"; -import { useActivity } from "@Hooks/useActivity"; -import { Toolbar } from "@Components/onsenui/Toolbar"; -import { Page } from "@Components/onsenui/Page"; -import { useStrings } from "@Hooks/useStrings"; -import { useTheme } from "@Hooks/useTheme"; -import CloseIcon from "@mui/icons-material/Close"; -import Box from "@mui/material/Box"; -import Avatar from "@mui/material/Avatar"; -import Typography from "@mui/material/Typography"; - -type Extra = { - desc?: string; - name: string; - logo?: string; -}; - -function DescriptonActivity() { - const { context, extra } = useActivity(); - const { strings } = useStrings(); - const { theme } = useTheme(); - const { desc, name, logo } = extra; - - const renderToolbar = () => { - return ( - - - - ({ - bgcolor: theme.palette.primary.dark, - width: 40, - height: 40, - boxShadow: "0 -1px 5px rgba(0,0,0,.09), 0 3px 5px rgba(0,0,0,.06), 0 1px 2px rgba(0,0,0,.3), 0 1px 3px rgba(0,0,0,.15)", - borderRadius: "20%", - mr: 1.5, - fontSize: 14, - })} - src={logo} - > - {name.charAt(0).toUpperCase()} - - - - - {name} - - - {strings("about_this_module")} - - - - - - - - - ); - }; - - return ( - - - {!desc ? ( - - ) : ( - <> - - - )} - - - ); -} - -export default DescriptonActivity; diff --git a/src/activitys/FetchTextActivity.tsx b/src/activitys/FetchTextActivity.tsx deleted file mode 100644 index 069fbe4d..00000000 --- a/src/activitys/FetchTextActivity.tsx +++ /dev/null @@ -1,81 +0,0 @@ -import { ProgressCircular } from "react-onsenui"; -import { Markup } from "@Components/Markdown"; -import { useActivity } from "@Hooks/useActivity"; -import React from "react"; -import { Toolbar } from "@Components/onsenui/Toolbar"; -import { Page } from "@Components/onsenui/Page"; -import { useNetwork } from "@Hooks/useNetwork"; -import { MissingInternet } from "@Components/MissingInternet"; -import { useFetch } from "@Hooks/useFetch"; - -export type FetchTextActivityExtra = { - rendering?: React.FunctionComponent | React.ComponentType; - modulename?: string; - title?: string; - raw_data?: string; - url?: string; -}; - -interface State { - data?: T; - error?: Error; -} - -type Cache = { [url: string]: T }; -type Action = { type: "loading" } | { type: "fetched"; payload: T } | { type: "error"; payload: Error }; - -function FetchTextActivity() { - const { context, extra } = useActivity(); - const { isNetworkAvailable } = useNetwork(); - const { title, modulename, url } = extra; - - const [data] = useFetch(url, { - type: "text", - }); - - const state = data || extra.raw_data; - - const renderToolbar = () => { - return ( - - - - - {title || modulename} - - ); - }; - - if (!isNetworkAvailable) { - return ( - - - - ); - } - - if (!state) { - return ( - - - - ); - } - - return ( - - {!extra.rendering ? : } - - ); -} - -export default FetchTextActivity; diff --git a/src/activitys/InstallTerminalV2Activity/hooks/useExploreInstall.tsx b/src/activitys/InstallTerminalV2Activity/hooks/useExploreInstall.tsx deleted file mode 100644 index d293ffe8..00000000 --- a/src/activitys/InstallTerminalV2Activity/hooks/useExploreInstall.tsx +++ /dev/null @@ -1,147 +0,0 @@ -import { useActivity } from "@Hooks/useActivity"; -import { TerminalActivityExtra } from ".."; -import { v1 as uuidv1 } from "uuid"; -import { Download } from "@Native/Download"; -import React from "react"; -import { useLines } from "./useLines"; -import { useSettings } from "@Hooks/useSettings"; -import { Terminal } from "@Native/Terminal"; -import { BuildConfig } from "@Native/BuildConfig"; -import { Shell } from "@Native/Shell"; -import { RestartAlt } from "@mui/icons-material"; -import { SuFile } from "@Native/SuFile"; - -type ExploreInstall = { - url: string; - printExit?: boolean; -}; - -const useExploreInstall = (): [(options: ExploreInstall) => Promise, number] => { - const TMPDIR = "/data/local/tmp"; - const { extra } = useActivity(); - const [downloadProgress, setDownloadProgress] = React.useState(0); - const [printTerminalError] = useSettings("print_terminal_error"); - const { addText, addButton, setLastLine, rebootDevice, getInstallCLI } = useLines(); - const { source, issues } = extra; - - const exploreInstaller = async (options: ExploreInstall): Promise => { - return new Promise((resolve, reject) => { - const url = options.url; - const printExit = options.printExit ?? true; - const modPath = `${TMPDIR}/${uuidv1()}.zip`; - - console.debug(modPath); - - const dl = new Download(url, modPath); - - // Handle download progress - dl.onChange = (obj) => { - switch (obj.type) { - case "downloading": - setDownloadProgress(obj.state); - setLastLine(`- Downloading module progress: ${obj.state}%`); - break; - case "finished": - setDownloadProgress(0); - - const explore_install = new Terminal({ - cwd: TMPDIR, - printError: printTerminalError, - }); - - explore_install.env = { - ASH_STANDALONE: "1", - MMRL: "true", - MMRL_INTR: "true", - MMRL_VER: BuildConfig.VERSION_CODE.toString(), - ROOTMANAGER: Shell.getRootManager(), - }; - - // Add terminal line output - explore_install.onLine = (line) => { - addText(line); - }; - - if (printTerminalError) { - explore_install.onError = (err) => { - addText(`\x1b[38;5;130mⓘ${err}`); - }; - } - - explore_install.onExit = (code) => { - SuFile.deleteRecursive(modPath); - - if (printExit) { - switch (code) { - case Shell.M_INS_SUCCESS: - addText(" "); - addText( - "\x1b[93mYou can press the \x1b[33;4mbutton\x1b[93;0m\x1b[93m below to \x1b[33;4mreboot\x1b[93;0m\x1b[93m your device\x1b[0m" - ); - addButton("Reboot", { - startIcon: , - onClick: rebootDevice, - }); - addText( - "\x1b[2mModules that causes issues after installing belong not to \x1b[35;4mMMRL\x1b[0;2m!\nPlease report these issues to their support page\x1b[2m" - ); - if (issues) { - addText(`> \x1b[32mIssues: \x1b[33m${issues}\x1b[0m`); - } - if (source) { - addText(`> \x1b[32mSource: \x1b[33m${source}\x1b[0m`); - } - break; - case Shell.M_INS_FAILURE: - addText(" "); - addText( - "\x1b[2mModules that cause issues after installing belong not to \x1b[35;4mMMRL\x1b[0;2m!\nPlease report these issues to their support page\x1b[2m" - ); - if (issues) { - addText(`> \x1b[32mIssues: \x1b[33m${issues}\x1b[0m`); - } - if (source) { - addText(`> \x1b[32mSource: \x1b[33m${source}\x1b[0m`); - } - break; - case Shell.TERM_INTR_ERR: - addText("! \x1b[31mInternal error!\x1b[0m"); - break; - default: - addText(`? Unknown code returned (${code}})`); - break; - } - } - - resolve(); // Resolve the promise once the terminal exits - }; - - try { - // Execute the command but don't expect a return value - explore_install.exec(getInstallCLI({ ZIPFILE: modPath })); - } catch (err) { - addText(`! \x1b[31mExecution error: ${err}\x1b[0m`); - reject(err); // Reject the promise on execution error - } - - break; - } - }; - - // Handle download errors - dl.onError = (err) => { - setDownloadProgress(0); - addText("! \x1b[31mUnable to download the module\x1b[0m"); - addText("! \x1b[31mERR: " + err + "\x1b[0m"); - reject(err); // Reject the promise on download error - }; - - // Start the download - dl.start(); - }); - }; - - return [exploreInstaller, downloadProgress]; -}; - -export { useExploreInstall }; diff --git a/src/activitys/InstallTerminalV2Activity/hooks/useLines.tsx b/src/activitys/InstallTerminalV2Activity/hooks/useLines.tsx deleted file mode 100644 index 29ff7c8e..00000000 --- a/src/activitys/InstallTerminalV2Activity/hooks/useLines.tsx +++ /dev/null @@ -1,259 +0,0 @@ -import { Ansi } from "@Components/Ansi"; -import { Image } from "@Components/dapi/Image"; -import { useModFS } from "@Hooks/useModFS"; -import { useStrings } from "@Hooks/useStrings"; -import Button from "@mui/material/Button"; -import { Shell } from "@Native/Shell"; -import { SuFile } from "@Native/SuFile"; -import { path } from "@Util/path"; -import { useConfirm } from "material-ui-confirm"; -import ModFS from "modfs"; -import React from "react"; - -interface LinesContext { - processCommand: (rawCommand: string) => string | undefined; - lines: any[]; - setLines: React.Dispatch>; - addButton: (text: string, props?: object) => void; - addText: (text: string, props?: object) => void; - setLastLine: (text: string, props?: object) => void; - rebootDevice: (reason?: string) => void; - getInstallCLI: (adds?: Record) => string; - clearTerminal: () => void; -} - -const LinesContext = React.createContext({ - processCommand(_rawCommand) { - return ""; - }, - lines: [], - setLines() {}, - addButton(_text, _props) {}, - addText(_text, _props) {}, - setLastLine(_text, _props) {}, - rebootDevice(reason) {}, - getInstallCLI(adds) { - return "exit " + Shell.TERM_INTR_ERR; - }, - clearTerminal() {}, -}); - -const colors = { - R: "\x1b[0m", - BRIGHT: "\x1b[1m", - DIM: "\x1b[2m", - UNDERSCORE: "\x1b[4m", - FG: { - BLACK: "\x1b[30m", - RED: "\x1b[31m", - GREEN: "\x1b[32m", - YELLOW: "\x1b[33m", - BLUE: "\x1b[34m", - MAGENTA: "\x1b[35m", - CYAN: "\x1b[36m", - WHITE: "\x1b[37m", - GRAY: "\x1b[90m", - }, - BG: { - BLACK: "\x1b[40m", - RED: "\x1b[41m", - GREEN: "\x1b[42m", - YELLOW: "\x1b[43m", - BLUE: "\x1b[44m", - MAGENTA: "\x1b[45m", - CYAN: "\x1b[46m", - WHITE: "\x1b[47m", - GRAY: "\x1b[100m", - }, -}; - -interface LinesProviderProps extends React.PropsWithChildren {} - -const LinesProvider = (props: LinesProviderProps) => { - const { strings } = useStrings(); - const { modFS, modFSParse } = useModFS(); - const [useInt, setUseInt] = React.useState(false); - const [lines, setLines] = React.useState([]); - const confirm = useConfirm(); - const { children } = props; - - const addText = (text: string, props?: object) => { - const txt = processCommand(text); - - if (typeof txt === "string" && txt !== "undefined") { - setLines((lines) => [ - ...lines, - { - component: Ansi, - props: { - children: txt, - sx: { - mr: 1, - ml: 1, - }, - linkify: true, - ...props, - }, - }, - ]); - } - }; - - const setLastLine = (text: string, props?: object) => { - setLines((p) => p.slice(0, -1)); - addText(text, props); - }; - - const addImage = (data: string, props?: object) => { - if (typeof data === "string") { - setLines((lines) => [ - ...lines, - { - component: Image, - props: { - src: data, - noOpen: true, - sx: { - mr: 1, - ml: 1, - }, - ...props, - }, - }, - ]); - } - }; - - const addButton = (text: string, props?: object) => { - setLines((lines) => [ - ...lines, - { - component: Button, - props: { - children: text, - variant: "contained", - sx: { - width: "50vmin", - mt: 1, - mb: 1, - }, - ...props, - }, - }, - ]); - }; - - const format = React.useMemo( - () => ({ - addImage(data: string) { - addImage(data); - return "undefined"; - }, - setLastLine(text: string) { - if (typeof text === "undefined") return "undefined"; - setLastLine(text); - return "undefined"; - }, - color: (text: string) => { - if (typeof text === "undefined") return "undefined"; - return ModFS.format(text, colors); - }, - clearTerminal: () => { - setLines([]); - return "undefined"; - }, - removeLastLine: () => { - setLines((p) => p.slice(0, -1)); - return "undefined"; - }, - }), - [] - ); - - const processCommand = (rawCommand: string): string | "undefined" => { - if (rawCommand.startsWith("#!mmrl:")) { - rawCommand = rawCommand.substring(7); - return ModFS.format(rawCommand, format) as string | "undefined"; - } else { - const info = /^\-(\s+)?(.+)/gm; - const warn = /^\?(\s+)?(.+)/gm; - const erro = /^\!(\s+)?(.+)/gm; - - if (rawCommand.match(info)) { - return rawCommand.replace(info, "$2"); - } else if (rawCommand.match(erro)) { - return rawCommand.replace(erro, "\x1b[31m$2\x1b[0m"); - } else if (rawCommand.match(warn)) { - return rawCommand.replace(warn, "\x1b[33m$2\x1b[0m"); - } else { - return rawCommand; - } - } - }; - - const rebootDevice = React.useCallback((reason: string = "") => { - confirm({ - title: strings("reboot_device"), - description: strings("reboot_device_desc"), - confirmationText: strings("yes"), - cancellationText: strings("cancel"), - }).then(() => { - Shell.cmd(`/system/bin/svc power reboot ${reason} || /system/bin/reboot ${reason}`).exec(); - }); - }, []); - - const getInstallCLI = React.useCallback((adds?: Record) => { - const __adds = { - ...adds, - findBinary(binaryNames: string, args: string) { - const folders = ["/system/bin", "/ksu/bin", "/ap/bin", "/magisk"]; - const _binaryNames = binaryNames.split(","); - for (const binaryName of _binaryNames) { - for (const folder of folders) { - const binaryPath = modFSParse(path.join(folder, binaryName)); - if (SuFile.exist(binaryPath)) { - return `${binaryPath} ${args}`; - } - } - } - return null; - }, - }; - - switch (Shell.getRootManager()) { - case "Magisk": - return modFS("MSUINI", __adds); - case "KernelSU": - return modFS("KSUINI", __adds); - case "APatchSU": - return modFS("ASUINI", __adds); - default: - return `exit ${Shell.M_DWL_FAILURE}`; - } - }, []); - - const clearTerminal = () => { - setLines([]); - }; - - const value = React.useMemo( - () => ({ - processCommand: processCommand, - lines: lines, - setLines: setLines, - addButton: addButton, - addText: addText, - setLastLine: setLastLine, - rebootDevice, - getInstallCLI, - clearTerminal, - }), - [processCommand, lines, setLines, useInt, setUseInt, addButton, addText, setLastLine, rebootDevice, getInstallCLI, clearTerminal] - ); - - return ; -}; - -const useLines = () => React.useContext(LinesContext); - -export { useLines, LinesProvider }; diff --git a/src/activitys/InstallTerminalV2Activity/hooks/useLocalInstall.tsx b/src/activitys/InstallTerminalV2Activity/hooks/useLocalInstall.tsx deleted file mode 100644 index aab7afe7..00000000 --- a/src/activitys/InstallTerminalV2Activity/hooks/useLocalInstall.tsx +++ /dev/null @@ -1,195 +0,0 @@ -import { useActivity } from "@Hooks/useActivity"; -import { TerminalActivityExtra } from ".."; -import { v1 as uuidv1 } from "uuid"; -import { Download } from "@Native/Download"; -import React from "react"; -import { useLines } from "./useLines"; -import { useSettings } from "@Hooks/useSettings"; -import { Terminal } from "@Native/Terminal"; -import { BuildConfig } from "@Native/BuildConfig"; -import { Shell } from "@Native/Shell"; -import { Add, Remove, CodeRounded, ArrowBackIosRounded, RestartAlt } from "@mui/icons-material"; - -// const useLocalInstall = (): [() => void] => { -// const TMPDIR = "/data/local/tmp"; - -// const { extra } = useActivity(); -// const [printTerminalError] = useSettings("print_terminal_error"); - -// const { addText, addButton, rebootDevice, getInstallCLI } = useLines(); - -// const { modSource } = extra; - -// return [ -// () => { -// const zipfile = modSource[0]; -// // const zipfiles = modSource; - -// const local_install = new Terminal({ -// cwd: TMPDIR, -// printError: printTerminalError, -// }); - -// local_install.env = { -// ASH_STANDALONE: "1", -// MMRL: "true", -// MMRL_INTR: "true", -// MMRL_VER: BuildConfig.VERSION_CODE.toString(), -// ROOTMANAGER: Shell.getRootManager(), -// }; - -// local_install.onLine = (line) => { -// addText(line); -// }; - -// if (printTerminalError) { -// local_install.onError = (err) => { -// addText(`\x1b[38;5;130mⓘ${err}`); -// }; -// } - -// local_install.onExit = (code) => { -// switch (code) { -// case Shell.M_INS_SUCCESS: -// addText(" "); - -// addText( -// "\x1b[93mYou can press the \x1b[33;4mbutton\x1b[93;0m\x1b[93m below to \x1b[33;4mreboot\x1b[93;0m\x1b[93m your device\x1b[0m" -// ); - -// addButton("Reboot", { -// startIcon: , -// onClick: rebootDevice, -// }); - -// addText( -// "\x1b[2mModules that causes issues after installing belog not to \x1b[35;4mMMRL\x1b[0;2m!\nPlease report these issues to thier support page\x1b[2m" -// ); -// break; - -// case Shell.M_INS_FAILURE: -// addText(" "); - -// addText( -// "\x1b[2mModules that causes issues after installing belog not to \x1b[35;4mMMRL\x1b[0;2m!\nPlease report these issues to thier support page\x1b[2m" -// ); -// break; - -// case Shell.TERM_INTR_ERR: -// addText("! \x1b[31mInternal error!\x1b[0m"); -// break; - -// default: -// addText("- Unknown code returned"); -// break; -// } -// }; - -// local_install.exec( -// getInstallCLI({ -// ZIPFILE: zipfile, -// }) -// ); -// }, -// ]; -// }; - -// export { useLocalInstall }; - -type LocalInstall = { - file: string; - printExit?: boolean; -}; - -const useLocalInstall = (): [(options: LocalInstall) => Promise] => { - const TMPDIR = "/data/local/tmp"; - const { extra } = useActivity(); - const [printTerminalError] = useSettings("print_terminal_error"); - const { addText, addButton, rebootDevice, getInstallCLI } = useLines(); - const { modSource } = extra; - - const localInstaller = async (options: LocalInstall): Promise => { - return new Promise((resolve, reject) => { - const zipfile = options.file; - const printExit = options.printExit ?? true; - - const local_install = new Terminal({ - cwd: TMPDIR, - printError: printTerminalError, - }); - - local_install.env = { - ASH_STANDALONE: "1", - MMRL: "true", - MMRL_INTR: "true", - MMRL_VER: BuildConfig.VERSION_CODE.toString(), - ROOTMANAGER: Shell.getRootManager(), - }; - - // Handle terminal output lines - local_install.onLine = (line) => { - addText(line); - }; - - // Handle terminal errors - if (printTerminalError) { - local_install.onError = (err) => { - addText(`\x1b[38;5;130mⓘ${err}`); - }; - } - - // Handle terminal exit with a promise resolution - local_install.onExit = (code) => { - if (printExit) { - switch (code) { - case Shell.M_INS_SUCCESS: - addText(" "); - addText( - "\x1b[93mYou can press the \x1b[33;4mbutton\x1b[93;0m\x1b[93m below to \x1b[33;4mreboot\x1b[93;0m\x1b[93m your device\x1b[0m" - ); - addButton("Reboot", { - startIcon: , - onClick: rebootDevice, - }); - addText( - "\x1b[2mModules that causes issues after installing belong not to \x1b[35;4mMMRL\x1b[0;2m!\nPlease report these issues to their support page\x1b[2m" - ); - break; - - case Shell.M_INS_FAILURE: - addText(" "); - addText( - "\x1b[2mModules that causes issues after installing belong not to \x1b[35;4mMMRL\x1b[0;2m!\nPlease report these issues to their support page\x1b[2m" - ); - break; - - case Shell.TERM_INTR_ERR: - addText("! \x1b[31mInternal error!\x1b[0m"); - break; - - default: - addText(`? Unknown code returned (${code}})`); - break; - } - } - resolve(); // Resolve on unknown code, as execution has finished - }; - - // Execute the installation command - try { - local_install.exec( - getInstallCLI({ - ZIPFILE: zipfile, - }) - ); - } catch (err) { - addText(`! \x1b[31mExecution error: ${err}\x1b[0m`); - reject(err); // Reject on execution error - } - }); - }; - - return [localInstaller]; -}; - -export { useLocalInstall }; diff --git a/src/activitys/InstallTerminalV2Activity/index.tsx b/src/activitys/InstallTerminalV2Activity/index.tsx deleted file mode 100644 index dc53c978..00000000 --- a/src/activitys/InstallTerminalV2Activity/index.tsx +++ /dev/null @@ -1,202 +0,0 @@ -import { Page } from "@Components/onsenui/Page"; -import { Toolbar } from "@Components/onsenui/Toolbar"; -import { useActivity } from "@Hooks/useActivity"; -import { useSettings } from "@Hooks/useSettings"; -import { alpha, Box, LinearProgress, Stack, Typography } from "@mui/material"; -import { view, WindowManager } from "@Native/View"; -import FlatList from "flatlist-react"; -import React from "react"; -import { useExploreInstall } from "./hooks/useExploreInstall"; -import { LinesProvider, useLines } from "./hooks/useLines"; -import { useLocalInstall } from "./hooks/useLocalInstall"; - -export interface TerminalActivityExtra { - exploreInstall: boolean; - modSource: string[]; - source?: string; - issues?: string; -} - -const InstallerComponent = () => { - const { context, extra } = useActivity(); - - const termEndRef = React.useRef(null); - - const { clearTerminal, lines } = useLines(); - - const [termScrollBottom] = useSettings("term_scroll_bottom"); - const [termScrollBehavior] = useSettings("term_scroll_behavior"); - - const [terminalWordWrap] = useSettings("terminal_word_wrap"); - const [terminalNumbericLines] = useSettings("terminal_numberic_lines"); - - if (termScrollBottom) { - const termBehavior = React.useMemo(() => termScrollBehavior, [termScrollBehavior]); - - React.useEffect(() => { - termEndRef.current?.scrollIntoView({ behavior: termBehavior.value, block: "end", inline: "nearest" }); - }, [lines]); - } - - const [exploreInstaller, downloadProgress] = useExploreInstall(); - const [localInstaller] = useLocalInstall(); - - const handleExploreInstall = React.useCallback(async () => { - const { modSource } = extra; - - for (let idx = 0; idx < modSource.length; idx++) { - const mod = modSource[idx]; - const isLastItem = idx === modSource.length - 1; - const shouldPrintExit = modSource.length === 1 || isLastItem; - - try { - await exploreInstaller({ url: mod, printExit: shouldPrintExit }); - } catch (error) { - console.error("An error occurred during installation:", error); - } - } - }, []); - - const handleLocalInstall = React.useCallback(async () => { - const { modSource } = extra; - - for (let idx = 0; idx < modSource.length; idx++) { - const mod = modSource[idx]; - const isLastItem = idx === modSource.length - 1; - const shouldPrintExit = modSource.length === 1 || isLastItem; - - try { - await localInstaller({ file: mod, printExit: shouldPrintExit }); - } catch (error) { - console.error("An error occurred during installation:", error); - } - } - }, []); - - // ensure that it is always the same function - const nativeVolumeEventPrevent = React.useCallback((e: Event) => { - e.preventDefault(); - }, []); - React.useEffect(() => { - const { exploreInstall } = extra; - if (exploreInstall) { - handleExploreInstall(); - } else { - handleLocalInstall(); - } - - document.addEventListener("volumeupbutton", nativeVolumeEventPrevent, false); - document.addEventListener("volumedownbutton", nativeVolumeEventPrevent, false); - view.addFlags([WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON]); - return () => { - document.removeEventListener("volumeupbutton", nativeVolumeEventPrevent, false); - document.removeEventListener("volumedownbutton", nativeVolumeEventPrevent, false); - view.clearFlags([WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON]); - }; - }, []); - - const renderToolbar = () => { - return ( - - - - - Install - {downloadProgress !== 0 && ( - - )} - - ); - }; - - return ( - - - - ( - - {terminalNumbericLines && ( - ({ - minWidth: "40px", - paddingRight: "1em", - fontSize: "unset", - marginLeft: "calc(18px - 1em)", - color: theme.palette.text.secondary, - textAlign: "right", - textDecoration: "none", - })} - > - {Number(key) + 1} - - )} - - - - )} - renderOnScroll - renderWhenEmpty={() => <>} - /> - - - - - ); -}; - -export const InstallTerminalV2Activity = () => { - return ( - - - - ); -}; - -export default InstallTerminalV2Activity; diff --git a/src/activitys/LandingActivity/components/FAQ.tsx b/src/activitys/LandingActivity/components/FAQ.tsx deleted file mode 100644 index 16e7cda8..00000000 --- a/src/activitys/LandingActivity/components/FAQ.tsx +++ /dev/null @@ -1,81 +0,0 @@ -import * as React from "react"; -import { alpha, styled, SxProps } from "@mui/material/styles"; -import ArrowForwardIosSharpIcon from "@mui/icons-material/ArrowForwardIosSharp"; -import MuiAccordion, { AccordionProps } from "@mui/material/Accordion"; -import MuiAccordionSummary, { AccordionSummaryProps } from "@mui/material/AccordionSummary"; -import MuiAccordionDetails from "@mui/material/AccordionDetails"; -import Typography from "@mui/material/Typography"; -import { Markup } from "@Components/Markdown"; -import { Box } from "@mui/material"; - -const Accordion = styled((props: AccordionProps) => )(({ theme }) => ({ - border: `1px solid ${theme.palette.divider}`, - - backgroundColor: alpha(theme.palette.background.paper, 0.6), - backdropFilter: "saturate(180%) blur(20px)", - "&:not(:last-child)": { - borderBottom: 0, - }, - "&:last-child": { - borderRadius: "0px 0px 8px 8px", - }, - "&:first-child": { - borderRadius: "8px 8px 0px 0px", - }, - "&::before": { - display: "none", - }, -})); - -const AccordionSummary = styled((props: AccordionSummaryProps) => ( - } {...props} /> -))(({ theme }) => ({ - backgroundColor: "none", - - flexDirection: "row-reverse", - "& .MuiAccordionSummary-expandIconWrapper.Mui-expanded": { - transform: "rotate(90deg)", - }, - "& .MuiAccordionSummary-content": { - marginLeft: theme.spacing(1), - }, -})); - -const AccordionDetails = styled(MuiAccordionDetails)(({ theme }) => ({ - padding: theme.spacing(2), - backgroundColor: "none", -})); - -interface FAQItem { - q: string; - a: string; -} -interface FAQProps { - items: FAQItem[]; - sx?: SxProps; -} - -const FAQ = (props: FAQProps) => { - const [expanded, setExpanded] = React.useState(false); - - const handleChange = (panel: number) => (_: React.SyntheticEvent, newExpanded: boolean) => { - setExpanded(newExpanded ? panel : false); - }; - - return ( - - {props.items.map((item, index) => ( - - - {item.q} - - - {item.a} - - - ))} - - ); -}; - -export { FAQ }; diff --git a/src/activitys/LandingActivity/components/GridCard.tsx b/src/activitys/LandingActivity/components/GridCard.tsx deleted file mode 100644 index ca97b6da..00000000 --- a/src/activitys/LandingActivity/components/GridCard.tsx +++ /dev/null @@ -1,46 +0,0 @@ -import { useTheme } from "@Hooks/useTheme"; -import { alpha } from "@mui/material"; -import Card from "@mui/material/Card"; -import Typography from "@mui/material/Typography"; -import Grid from "@mui/material/Grid"; - -interface GridCardProps { - title?: string; - description?: string; -} - -const GridCard = (props: GridCardProps) => { - const { theme } = useTheme(); - - return ( - - - {props.title} - {props.description} - - - ); -}; - -export { GridCard, GridCardProps }; diff --git a/src/activitys/LandingActivity/components/GridImage.tsx b/src/activitys/LandingActivity/components/GridImage.tsx deleted file mode 100644 index 0a83f57f..00000000 --- a/src/activitys/LandingActivity/components/GridImage.tsx +++ /dev/null @@ -1,55 +0,0 @@ -import { Image } from "@Components/dapi/Image"; -import { useTheme } from "@Hooks/useTheme"; -import { alpha } from "@mui/material"; -import Card from "@mui/material/Card"; -import Grid from "@mui/material/Grid"; - -interface GridImageProps { - src?: string; - alt?: string; -} - -const GridImage = (props: GridImageProps) => { - const { theme } = useTheme(); - - return ( - - - {props.alt} - - - ); -}; - -export { GridImage, GridImageProps }; diff --git a/src/activitys/LandingActivity/components/LandingToolbar.tsx b/src/activitys/LandingActivity/components/LandingToolbar.tsx deleted file mode 100644 index 2a1b7fca..00000000 --- a/src/activitys/LandingActivity/components/LandingToolbar.tsx +++ /dev/null @@ -1,165 +0,0 @@ -import * as React from "react"; -import AppBar from "@mui/material/AppBar"; -import Box from "@mui/material/Box"; -import Toolbar from "@mui/material/Toolbar"; -import IconButton from "@mui/material/IconButton"; -import Typography from "@mui/material/Typography"; -import Menu from "@mui/material/Menu"; -import MenuIcon from "@mui/icons-material/Menu"; -import Container from "@mui/material/Container"; -import Avatar from "@mui/material/Avatar"; -import Button from "@mui/material/Button"; -import Tooltip from "@mui/material/Tooltip"; -import MenuItem from "@mui/material/MenuItem"; -import CodeRoundedIcon from "@mui/icons-material/CodeRounded"; -import GitHubIcon from "@mui/icons-material/GitHub"; -import { os } from "@Native/Os"; -import { StyledMenu } from "@Components/DropdownButton"; -import { useTheme } from "@Hooks/useTheme"; -import { alpha } from "@mui/material"; -import { view } from "@Native/View"; - -interface LandingPageMenuItem { - title: string; - onClick?: React.MouseEventHandler; -} - -interface LandigToolbarProps { - menuItems: LandingPageMenuItem[]; -} - -function LandingToolbar(props: LandigToolbarProps) { - const { theme } = useTheme(); - const [anchorElNav, setAnchorElNav] = React.useState(null); - - const handleOpenNavMenu = (event: React.MouseEvent) => { - setAnchorElNav(event.currentTarget); - }; - - const handleCloseNavMenu = () => { - setAnchorElNav(null); - }; - - return ( - - - - - - MMRL - - - - - - - - {props.menuItems.map((menuItem) => ( - { - handleCloseNavMenu(); - menuItem.onClick && menuItem.onClick(e); - }} - > - {menuItem.title} - - ))} - - - - - MMRL - - - {props.menuItems.map((menuItem) => ( - - ))} - - - { - os.openURL("https://github.com/DerGoogler/MMRL", "_blank"); - }} - sx={{ p: 0 }} - > - - - - - - - ); -} -export { LandingToolbar }; diff --git a/src/activitys/LandingActivity/components/SectionHeader.tsx b/src/activitys/LandingActivity/components/SectionHeader.tsx deleted file mode 100644 index a8b9f778..00000000 --- a/src/activitys/LandingActivity/components/SectionHeader.tsx +++ /dev/null @@ -1,13 +0,0 @@ -import { styled, Typography } from "@mui/material"; - -const SectionHeader = styled(Typography)(({ theme }) => ({ - marginTop: theme.spacing(8), - [theme.breakpoints.up("md")]: { - fontSize: "25px", - }, - - fontSize: "1.5rem", - textAlign: "center", -})); - -export { SectionHeader }; diff --git a/src/activitys/LandingActivity/index.tsx b/src/activitys/LandingActivity/index.tsx deleted file mode 100644 index 521d7070..00000000 --- a/src/activitys/LandingActivity/index.tsx +++ /dev/null @@ -1,345 +0,0 @@ -import { alpha, Box, Grid, Button, Divider, Typography, Stack, Card } from "@mui/material"; -import { Google, GitHub } from "@mui/icons-material"; -import { useTheme } from "@Hooks/useTheme"; -import { Image } from "@Components/dapi/Image"; -import { Anchor } from "@Components/dapi/Anchor"; -import React from "react"; -import { useConfirm } from "material-ui-confirm"; -import { Page } from "@Components/onsenui/Page"; -import { os } from "@Native/Os"; -import { useSettings } from "@Hooks/useSettings"; -import { useActivity } from "@Hooks/useActivity"; -import MainApplication from "../MainApplication"; -import { GridCard } from "./components/GridCard"; -import { GridImage } from "./components/GridImage"; -import { LandingToolbar } from "./components/LandingToolbar"; -import { SectionHeader } from "./components/SectionHeader"; -// @ts-ignore -import { FAQ } from "./components/FAQ"; -import { useLanguageMap } from "./../../locales/declaration"; -import { view } from "@Native/View"; - -export const LandingActivity = () => { - const { theme } = useTheme(); - const confirm = useConfirm(); - const [, setLanding] = useSettings("landingEnabled"); - const { context } = useActivity(); - const availableLangs = useLanguageMap(); - - const acceptCallback = React.useCallback((callback) => { - confirm({ - title: "Your privacy is more worth!", - description: ( - <> - Please make sure to read our{" "} - - Privacy Policy - {" "} - and{" "} - - Terms of Service - {" "} - before you continue. - - ), - acknowledgement: "Accept", - }) - .then(callback) - .catch(() => {}); - }, []); - - const randomGradientAngle = React.useMemo(() => Math.floor(Math.random() * (20 - 15 + 1) + 15), []); - - return ( - - - - - - - Magisk Module Repo Loader - - - - Your highly customizable module manager - - - - - - - or - - - - - - - Key Features - - - - - - - - - - - - Screenshots - - - - {Array.from(Array(8), (_, i) => i + 1).map((num) => ( - - ))} - - - - Frequently Asked Questions - - - MMRL-CLI to use it', - }, - { - q: "What are currently known repos that will work in MMRL?", - a: `Currently there are three repos that can be used in MMRL. -- Magisk Modules Alt Repo -- IzzyOnDroid Magisk Repo -- Googlers Magisk Repo`, - }, - { - q: "I want to use the Androidacy Magisk Repo here, does it work?", - a: "No. The Androidacy Magisk Repo does not work in MMRL, and there are currently no plant to support it.", - }, - { - q: "How to use ModConf's from Magisk Modules?", - a: `1. Open MMRL -2. Switch to the Installed tab -3. Scroll to your choosen module -4. Click on "CONFIG" - -> [!WARNING] -> The module developer develop the ModConf and errors that show up there has mainly nothing to do with MMRL`, - }, - - { - q: "In what programming language is MMRL written?", - a: "MMRL is written in Java, JavaScript and TypeScript. It uses React as framework and uses Onsen UI and MUI for front-end design.", - }, - { - q: "How can I build my own repo?", - a: "Check out [magisk-modules-repo-util](https://github.com/Googlers-Repo/magisk-modules-repo-util.git) for more.", - }, - - { - q: "ModFS seems to be broken, cannot install modules", - a: "If you receive error like `/system/bin/sh: /data/adb/magisk/magisk32: inaccessible or not found` while installing your modules, this can be sometimes ModFS related.\n\nCommon root solutions:\n- `MSUINI` Magisk\n- `KSUINI` KernelSU\n- `ASUINI` APatch", - }, - - { - q: "In what languages is MMRL available?", - a: `MMRL is in ${Object.keys(availableLangs).length} languages available. - - -${Object.entries(availableLangs) - .map( - ([_, lang]) => - `\`${lang.name}\`` - ) - .join("")} - - `, - }, - ]} - /> - - - - - - © {new Date().getUTCFullYear()} Der_Googler & Googlers Repo. All rights reserved. - - - - - - Privacy Policy - - - - - Terms of Service - - - - - - ); -}; diff --git a/src/activitys/LicensesActivity.tsx b/src/activitys/LicensesActivity.tsx deleted file mode 100644 index 67cbc55c..00000000 --- a/src/activitys/LicensesActivity.tsx +++ /dev/null @@ -1,114 +0,0 @@ -import { Page } from "@Components/onsenui/Page"; -import { Toolbar } from "@Components/onsenui/Toolbar"; -import { useActivity } from "@Hooks/useActivity"; -import { useStrings } from "@Hooks/useStrings"; -import Button from "@mui/material/Button"; -import Card from "@mui/material/Card"; -import Stack from "@mui/material/Stack"; -import Typography from "@mui/material/Typography"; -import FlatList from "flatlist-react"; - -import { useFetch } from "@Hooks/useFetch"; -import { useTheme } from "@Hooks/useTheme"; -import { os } from "@Native/Os"; -import li from "@Util/licenses.json"; -import { Activities } from "."; - -const DepCard = (props: { dep: (typeof li)[0] }) => { - const { theme } = useTheme(); - const { strings } = useStrings(); - const { context } = useActivity(); - const dep = props.dep; - - const handleOpenSource = () => { - os.open(dep.source, { - target: "_blank", - features: { - color: theme.palette.primary.main, - }, - }); - }; - - const [licenseData] = useFetch(`https://raw.githubusercontent.com/spdx/license-list-data/main/website/${dep.license}.json`); - - const handleOpenLicense = () => { - if (licenseData) { - context.pushPage({ - component: Activities.FetchText, - key: "license_" + dep.license, - extra: { - raw_data: licenseData.licenseText, - modulename: licenseData.name, - }, - }); - } - }; - - return ( - - - - - {dep.author} - - - {dep.license} - - - - - {dep.name} - - - {dep.version} - - - {dep.description} - - - - - - - - - ); -}; - -const LicensesActivity = () => { - const { strings } = useStrings(); - const { context } = useActivity(); - - const renderToolbar = () => { - return ( - - - - - {strings("licenses")} - - ); - }; - - return ( - - - } - renderOnScroll - display={{ - row: true, - rowGap: "8px", - }} - /> - - - ); -}; - -export default LicensesActivity; diff --git a/src/activitys/LogcatActivity.tsx b/src/activitys/LogcatActivity.tsx deleted file mode 100644 index e100f675..00000000 --- a/src/activitys/LogcatActivity.tsx +++ /dev/null @@ -1,126 +0,0 @@ -import React from "react"; -import { Add, Remove } from "@mui/icons-material"; -import { Stack, Box, Slider } from "@mui/material"; -import FlatList from "flatlist-react"; -import { Toolbar } from "@Components/onsenui/Toolbar"; -import { BottomToolbar } from "@Components/onsenui/BottomToolbar"; -import { Page } from "@Components/onsenui/Page"; -import { BuildConfig } from "@Native/BuildConfig"; -import { useActivity } from "@Hooks/useActivity"; -import { useTheme } from "@Hooks/useTheme"; -import { useNativeStorage } from "@Hooks/useNativeStorage"; -import { useSettings } from "@Hooks/useSettings"; -import { Ansi } from "@Components/Ansi"; -import { Terminal } from "@Native/Terminal"; - -const LogcatActivity = () => { - const [fontSize, setFontSize] = useNativeStorage("mmrlini_log_terminal", 100); - const { context } = useActivity(); - const { theme } = useTheme(); - const [lines, setLines] = React.useState([]); - const [termScrollBottom] = useSettings("term_scroll_bottom"); - const [termScrollBehavior] = useSettings("term_scroll_behavior"); - - const termEndRef = React.useRef(null); - - if (termScrollBottom) { - const termBehavior = React.useMemo(() => termScrollBehavior, [termScrollBehavior]); - - React.useEffect(() => { - termEndRef.current?.scrollIntoView({ behavior: termBehavior.value, block: "end", inline: "nearest" }); - }, [lines]); - } - - const addLine = (line: string) => { - setLines((lines) => [...lines, line]); - }; - - const startLog = () => { - const logcat = new Terminal(); - logcat.env = { - PACKAGENAME: BuildConfig.APPLICATION_ID, - }; - logcat.onLine = (line) => { - addLine(line); - }; - logcat.onExit = (code) => {}; - logcat.exec("logcat --pid=`pidof -s $PACKAGENAME` -v color"); - }; - - return ( - ( - - - - - Logcat - - )} - modifier="noshadow" - renderBottomToolbar={() => { - return ( - - - - { - setFontSize(Number(newValue)); - }} - step={10} - marks - min={20} - max={200} - /> - - - - ); - }} - > -
- - ( - - {line} - - )} - renderOnScroll - renderWhenEmpty={() => <>} - /> - -
-
- - ); -}; - -export default LogcatActivity; diff --git a/src/activitys/MainActivity.tsx b/src/activitys/MainActivity.tsx deleted file mode 100644 index c8c71900..00000000 --- a/src/activitys/MainActivity.tsx +++ /dev/null @@ -1,418 +0,0 @@ -import { Code } from "@Components/dapi/Code"; -import { Pre } from "@Components/dapi/Pre"; -import { ErrorBoundary } from "@Components/ErrorBoundary"; -import Icon from "@Components/Icon"; -import { Page } from "@Components/onsenui/Page"; -import { RouterNavigator } from "@Components/onsenui/RouterNavigator"; -import { Splitter } from "@Components/onsenui/Splitter"; -import { Toolbar } from "@Components/onsenui/Toolbar"; -import { useSettings } from "@Hooks/useSettings"; -import { useTheme } from "@Hooks/useTheme"; -import { CloseRounded } from "@mui/icons-material"; -import CodeRoundedIcon from "@mui/icons-material/CodeRounded"; -import { Button, Typography } from "@mui/material"; -import { os } from "@Native/Os"; -import { Shell } from "@Native/Shell"; -import { RouterUtil } from "@Util/RouterUtil"; -import eruda from "eruda"; -import React, { Suspense, useState } from "react"; -import { IntentPusher } from "../hooks/useActivity"; -import { DrawerFragment } from "./fragments/DrawerFragment"; -import MainApplication from "./MainApplication"; -import NoRootActivity from "./NoRootActivity"; -import SettingsActivity from "./SettingsActivity"; -import useMediaQuery from "@mui/material/useMediaQuery"; -import { useModFS } from "@Hooks/useModFS"; -import { useStrings } from "@Hooks/useStrings"; -import { SuFile } from "@Native/SuFile"; -import pkg from "@Package"; -import UnverifiedHostActivity from "./UnverifiedHostActivity"; -import { LandingActivity } from "./LandingActivity"; -import { ModulesQueue } from "@Components/ModulesQueue"; -import { useConfirm } from "material-ui-confirm"; -import { ProgressCircular } from "react-onsenui"; -import { Activities } from "."; - -const getLocation = () => { - if (window.location !== window.parent.location) { - return window.parent.location; - } else if (window.self !== window.top && window.top) { - return window.top.location; - } else { - return window.location; - } -}; - -const useCheckRoot = () => { - const [landing] = useSettings("landingEnabled"); - - return React.useMemo(() => { - if (pkg.config.verified_hosts.some((e) => new RegExp(e[0], e[1]).test(getLocation().hostname))) { - if (os.isAndroid) { - // Shell.isAppGrantedRoot() doesn't work on KSU - if (Shell.isSuAvailable()) { - return React.memo(MainApplication); - } else { - return React.memo(NoRootActivity); - } - } else { - if (landing) { - return React.memo(LandingActivity); - } else { - return React.memo(MainApplication); - } - } - } else { - return React.memo(UnverifiedHostActivity); - } - }, []); -}; - -const MainActivity = (): JSX.Element => { - const { strings } = useStrings(); - const { theme } = useTheme(); - const { modFS } = useModFS(); - const InitialActivity = useCheckRoot(); - const confirm = useConfirm(); - - const [isSplitterOpen, setIsSplitterOpen] = useState(false); - - const hideSplitter = () => { - setIsSplitterOpen(false); - }; - - const showSplitter = () => { - setIsSplitterOpen(true); - }; - - const [erudaConsoleEnabled] = useSettings("eruda_console_enabled"); - - const erudaRef = React.useRef(null); - - const _eruda = React.useMemo(() => eruda, [erudaConsoleEnabled]); - - React.useEffect(() => { - if (erudaConsoleEnabled) { - _eruda.init({ - container: erudaRef.current as HTMLElement, - tool: ["console", "elements", "resources", "info"], - }); - } else { - if ((window as any).eruda) { - _eruda.destroy(); - } - } - }, [erudaConsoleEnabled]); - - const pushContext = { - pushPage: (props: IntentPusher) => pushPage(props), - popPage: (options?: any) => popPage(options), - replacePage: (props: IntentPusher) => replacePage(props), - splitter: { - show: () => showSplitter(), - hide: () => hideSplitter(), - }, - }; - - const ignoreThat = RouterUtil.init([ - { - route: { - component: InitialActivity, - props: { - key: "main", - }, - }, - key: "main", - props: { - key: "main", - }, - context: pushContext, - }, - ]); - - const [routeConfig, setRouteConfig] = useState(ignoreThat); - - const popPage = (options?: any) => { - setRouteConfig((prev: any) => - RouterUtil.pop({ - routeConfig: prev, - key: prev.key, - options: { - ...options, - animationOptions: { - // duration: 0.2, - duration: 0, - // timing: "ease-in", - timing: "none", - // animation: "fade-md", - animation: "none", - }, - }, - } as any) - ); - }; - - const pushPage = (props: IntentPusher): void => { - const route = { - component: !props.noMemo ? React.memo(props.component) : props.component, - props: { - key: props.component.name || props.key, - }, - }; - - const options = { - animationOptions: { - // duration: 0.2, - duration: 0, - // timing: "ease-in", - timing: "none", - // animation: "fade-md", - animation: "none", - }, - }; - - setRouteConfig((prev: any) => - RouterUtil.push({ - routeConfig: prev, - route: route, - options: options, - key: props.component.name || props.key, - props: props.props, - context: pushContext, - extra: props.extra ? props.extra : {}, - }) - ); - }; - - const replacePage = (props: IntentPusher): void => { - const route = { - component: !props.noMemo ? React.memo(props.component) : props.component, - props: { - key: props.key, - }, - }; - - const options = {}; - - setRouteConfig((prev: any) => - RouterUtil.replace({ - routeConfig: prev, - route: route, - options: options, - key: props.key, - props: props.props, - context: pushContext, - extra: props.extra ? props.extra : {}, - }) - ); - }; - - const onPostPush = () => { - setRouteConfig((prev: any) => RouterUtil.postPush(prev)); - }; - - const onPostPop = () => { - setRouteConfig((prev: any) => RouterUtil.postPop(prev)); - }; - - const fallbackSuspense = React.useMemo( - () => ( - { - return ( - - - - - Loading... - - ); - }} - > - - - ), - [] - ); - - const renderPage = (route: any, props: any) => { - return ( - - - - - - ); - }; - - const renderSpliterToolbar = () => { - return ( - <> - - - - - MMRL - - - - - - - - - - ); - }; - - const fallback = (error: Error, errorInfo: React.ErrorInfo, resetErrorBoundary) => { - const style = { - backgroundColor: theme.palette.background.paper, - color: theme.palette.text.primary, - borderRadius: theme.shape.borderRadius / theme.shape.borderRadius, - lineHeight: 1.45, - overflow: "auto", - padding: 2, - }; - - const handleOpenSettings = () => { - pushPage({ - component: Activities.Settings, - key: "SettingsActivity", - }); - }; - - const handleOpenLogcat = () => { - pushPage({ - component: Activities.Logcat, - key: "LogcatActivity", - }); - }; - - return ( - { - return ( - - {strings("we_hit_a_brick")} - - ); - }} - > - -
-            {error.message}
-          
- - - - {os.isAndroid && ( - - )} - -
-            {errorInfo.componentStack}
-          
-
-
- ); - }; - - const matches = useMediaQuery("(max-width: 767px)"); - - return ( - - { - if (isSplitterOpen) { - hideSplitter(); - } else { - confirm({ - title: strings("exit_app"), - description: strings("exit_app_desc"), - confirmationText: strings("yes"), - cancellationText: strings("no"), - }) - .then(() => { - if (typeof e.callParentHandler === "function") { - e.callParentHandler(); - } else { - navigator.app.exitApp(); - } - }) - .catch(() => {}); - } - }} - onInit={() => { - const mmrlFolder = new SuFile(modFS("MMRLFOL")); - - if (Shell.isSuAvailable() && !mmrlFolder.exist()) { - mmrlFolder.create(SuFile.NEW_FOLDERS); - } - }} - > - - - - - - popPage(options)} - routeConfig={routeConfig} - renderPage={renderPage} - onPostPush={() => onPostPush()} - onPostPop={() => onPostPop()} - /> - - - - - ); -}; - -export { MainActivity }; diff --git a/src/activitys/MainApplication.tsx b/src/activitys/MainApplication.tsx deleted file mode 100644 index f4b65610..00000000 --- a/src/activitys/MainApplication.tsx +++ /dev/null @@ -1,271 +0,0 @@ -import DeviceModule from "@Components/module/DeviceModule"; -import ExploreModule from "@Components/module/ExploreModule"; -import UpdateModule from "@Components/module/UpdateModule"; -import { useModuleQueue } from "@Components/ModulesQueue"; -import Fab from "@Components/onsenui/Fab"; -import { Page } from "@Components/onsenui/Page"; -import { Tabbar, TabbarRenderTab } from "@Components/onsenui/Tabbar"; -import { Toolbar } from "@Components/onsenui/Toolbar"; -import { useActivity } from "@Hooks/useActivity"; -import { useLocalModules } from "@Hooks/useLocalModules"; -import { useRepos } from "@Hooks/useRepos"; -import { useSettings } from "@Hooks/useSettings"; -import { useStrings } from "@Hooks/useStrings"; -import CodeRoundedIcon from "@mui/icons-material/CodeRounded"; -import CreateNewFolderIcon from "@mui/icons-material/CreateNewFolder"; -import LayersIcon from "@mui/icons-material/Layers"; -import MenuIcon from "@mui/icons-material/Menu"; -import VolunteerActivismIcon from "@mui/icons-material/VolunteerActivism"; -import Typography from "@mui/material/Typography"; -import { Chooser } from "@Native/Chooser"; -import { Log } from "@Native/Log"; -import { os } from "@Native/Os"; -import { Shell } from "@Native/Shell"; -import { SuFile } from "@Native/SuFile"; -import { SuZip } from "@Native/SuZip"; -import { useConfirm } from "material-ui-confirm"; -import { Properties } from "properties-file"; -import React from "react"; -import { Activities } from "."; -import ModuleFragment from "./fragments/ModuleFragment"; - -const TAG = "MainApplication"; - -const MainApplication = () => { - const { context } = useActivity(); - const { strings } = useStrings(); - const { modules } = useRepos(); - const [index, setIndex] = React.useState(0); - const localModules = useLocalModules(); - const confirm = useConfirm(); - - const [swipeableTabs] = useSettings("swipeable_tabs"); - - const handleBackEvent = React.useCallback( - (e: any) => { - if (index === 0) { - if (typeof e.callParentHandler === "function") { - e.callParentHandler(); - } - } else { - setIndex(0); - } - }, - [index] - ); - - React.useEffect(() => { - const sharedFile = SuFile.getSharedFile(); - if (sharedFile) { - const file = new SuFile(sharedFile); - - if (file.exist()) { - const zipFile = new SuZip(file.getPath(), "module.prop"); - const props = new Properties(zipFile.read()).toObject(); - - if (!props.id) { - return; - } - - confirm({ - title: strings("install_module", { name: props.name }), - description: strings("install_module_dialog_desc", { name: {props.name} }), - confirmationText: strings("yes"), - }) - .then(() => { - context.pushPage({ - component: Activities.InstallTerminal, - key: "InstallTerminalV2Activity", - extra: { - exploreInstall: false, - modSource: [file.getPath()], - }, - }); - }) - .catch(() => {}); - } else { - Log.i(TAG, "Unable to find shared file"); - } - } - }, []); - - React.useEffect(() => { - const urlParams = new URLSearchParams(window.location.search); - const id = urlParams.get("module"); - const m_ = modules.find((m) => m.id === id); - if (m_) { - context.pushPage({ - component: Activities.ModuleView, - key: "ModuleViewActivity", - extra: m_, - }); - } - }, [modules]); - - const { toggleQueueView } = useModuleQueue(); - - const renderTabs = (): TabbarRenderTab[] => { - return [ - { - content: ( - } - renderFixed={() => { - return ( - - - - ); - }} - /> - ), - tab: , - }, - ...(os.isAndroid - ? [ - { - content: ( - } - renderFixed={() => { - if (os.isAndroid && (Shell.isMagiskSU() || Shell.isKernelSU() || Shell.isAPatchSU())) { - return ( - { - const chooseModule = new Chooser("application/zip"); - chooseModule.allowMultiChoose = true; - chooseModule.onChose = (files) => { - if (Chooser.isSuccess(files)) { - context.pushPage({ - component: Activities.InstallTerminal, - key: "InstallTerminalV2Activity", - extra: { - exploreInstall: false, - modSource: files, - }, - }); - } - }; - - chooseModule.getFiles(); - }} - position="bottom right" - > - - - ); - } - }} - /> - ), - tab: , - }, - { - content: ( - } - /> - ), - tab: , - }, - ] - : []), - ]; - }; - - const renderToolbar = () => { - return ( - - - - - - - - - MMRL - - - - - { - os.openURL("https://github.com/sponsors/DerGoogler", "_blank"); - }} - /> - - - ); - }; - - return ( - - { - if (event.index != index) { - setIndex(event.index); - } - }} - renderTabs={renderTabs} - /> - - ); -}; - -export default MainApplication; diff --git a/src/activitys/ModConfActivity/components/ModConfView/index.tsx b/src/activitys/ModConfActivity/components/ModConfView/index.tsx deleted file mode 100644 index 6c8d039c..00000000 --- a/src/activitys/ModConfActivity/components/ModConfView/index.tsx +++ /dev/null @@ -1,76 +0,0 @@ -import { useLog } from "@Hooks/native/useLog"; -import { ModFS, useModFS } from "@Hooks/useModFS"; -import { IsolatedEval, IsolatedEvalOptions } from "@Native/IsolatedEval"; -import { os } from "@Native/Os"; -import * as React from "react"; -import { libraries } from "./libs"; - -interface ModConfViewProps { - children: string; - modid: string; - indexFile: string; - cwd: string; - standaloneFile?: string; -} - -export const ModConfView = (props: ModConfViewProps) => { - const { modFS } = useModFS(); - - const { modid, children } = props; - const log = useLog(`Config-${modid}`); - const format = React.useCallback<(key: K) => ModFS[K]>((key) => modFS(key, { MODID: modid }), []); - - const internalFetch = React.useCallback( - (input: string | URL | Request, init?: RequestInit | undefined) => { - return fetch(input, init); - }, - [modid] - ); - - const isoEval = React.useMemo(() => { - const options: IsolatedEvalOptions = { - libraries: libraries, - indexFile: props.indexFile, - cwd: props.cwd, - scope: { - log: log, - __idname: modid, - __filename: props.indexFile, - __dirname: props.cwd, - __modpath: format("MODULECWD"), - window: { - fetch: internalFetch, - open: os.openURL, - }, - fetch: internalFetch, - - // @deprecated - modid: modid, - modpath: (path: string) => `${format("MODULECWD")}/${path}`, - confpath: (path: string) => `${format("CONFCWD")}/${path}`, - include: (modulePath: string, opt: { isolate: boolean } = { isolate: false }) => { - if (opt.isolate) { - modulePath = `${format("CONFCWD")}/${modulePath}`; - } - - console.warn("include(...) is deprecated, please use require(...)"); - - return isoEval.require(modulePath); - }, - }, - standaloneFile: props.standaloneFile, - }; - - return new IsolatedEval>(options); - }, [children, modid]); - - const box = React.useCallback((code: string) => isoEval.compileTransform(code), []); - - const Component = box(children as string); - - if (Component.exports.default) { - return ; - } - - return <>; -}; diff --git a/src/activitys/ModConfActivity/components/ModConfView/libs.ts b/src/activitys/ModConfActivity/components/ModConfView/libs.ts deleted file mode 100644 index 2326bb17..00000000 --- a/src/activitys/ModConfActivity/components/ModConfView/libs.ts +++ /dev/null @@ -1,129 +0,0 @@ -import { Video } from "@Components/dapi/Video"; -import { Image } from "@Components/dapi/Image"; -import { Anchor } from "@Components/dapi/Anchor"; -import { DiscordWidget } from "@Components/dapi/DiscordWidget"; -import { BottomToolbar } from "@Components/onsenui/BottomToolbar"; -import { Page } from "@Components/onsenui/Page"; -import { Tabbar } from "@Components/onsenui/Tabbar"; -import { useTheme } from "@Hooks/useTheme"; -import { useNativeStorage } from "@Hooks/useNativeStorage"; -import { useNativeProperties } from "@Hooks/useNativeProperties"; -import { useActivity } from "@Hooks/useActivity"; -import { Toolbar } from "@Components/onsenui/Toolbar"; -import { StringsProvider, useStrings } from "@Hooks/useStrings"; -import { Ansi } from "@Components/Ansi"; -import { os } from "@Native/Os"; -import { useSettings } from "@Hooks/useSettings"; -import { ConfigProvider, useConfig, useNativeFileStorage } from "@Hooks/useNativeFileStorage"; -import { useModFS } from "@Hooks/useModFS"; -import PicturePreviewActivity from "@Activitys/PicturePreviewActivity"; -import { useConfirm } from "material-ui-confirm"; -import { Markup } from "@Components/Markdown"; -import { DialogEditTextListItem } from "@Components/DialogEditTextListItem"; -import { SearchActivity } from "@Activitys/SearchActivity"; -import { withRequireNewVersion } from "../../../../hoc/withRequireNewVersion"; -import { CodeBlock } from "@Components/CodeBlock"; -import { VerifiedIcon } from "@Components/icons/VerifiedIcon"; -import { IsolatedFunctionBlockError } from "@Native/IsolatedEval/IsolatedFunctionBlockError"; -import { Terminal } from "@Native/Terminal"; -import { useFetch } from "@Hooks/useFetch"; - -// Libaries -import * as React from "react"; -import * as MUI from "@mui/material"; -import * as ICONS_MUI from "@mui/icons-material"; -import * as LAB_MUI from "@mui/lab"; -import * as FlatListReact from "flatlist-react"; -import OnsenUI from "onsenui"; -import * as DefaultComposer from "default-composer"; -import * as UseHooksTS from "usehooks-ts"; -import * as ModFS from "modfs"; - -export const InternalReact = { - ...React, - createElement(type: any, props: any, ...children: any[]) { - switch (type) { - // prevents webview url change - case "a": - return React.createElement(Anchor, props, ...children); - case "iframe": - throw new IsolatedFunctionBlockError("iframe"); - default: - return React.createElement(type, props, ...children); - } - }, -}; - -export const libraries = { - react: InternalReact, - - "@mui/material": MUI, - - "@mui/lab": LAB_MUI, - - "@mui/icons-material": ICONS_MUI, - - "@mmrl/terminal": os.isAndroid ? Terminal : {}, - - "flatlist-react": FlatListReact.default, - - onsenui: OnsenUI, - - "@mmrl/activity": { - SearchActivity: SearchActivity, - PicturePreviewActivity: PicturePreviewActivity, - }, - - // high order components - "@mmrl/hoc": { - withRequireNewVersion: withRequireNewVersion, - }, - - "@mmrl/icons": { - VerifiedIcon: VerifiedIcon, - }, - - "@mmrl/ui": { - Anchor: Anchor, - Page: Page, - BottomToolbar: BottomToolbar, - Tabbar: Tabbar, - Toolbar: Toolbar, - Video: Video, - DiscordWidget: DiscordWidget, - Markdown: Markup, - ListItemDialogEditText: DialogEditTextListItem, - Image: Image, - Ansi: Ansi, - CodeBlock: CodeBlock, - }, - - "@mmrl/hooks": { - useConfirm: useConfirm, - useFetch: useFetch, - useConfig: useConfig, - useModFS: useModFS, - useActivity: useActivity, - useNativeProperties: useNativeProperties, - useNativeFileStorage: useNativeFileStorage, - useNativeStorage: useNativeStorage, - useTheme: useTheme, - useSettings: useSettings, - useStrings: useStrings, - }, - - "@mmrl/providers": { - ConfigProvider: ConfigProvider, - StringsProvider: StringsProvider, - }, - modfs: ModFS, - "default-composer": DefaultComposer, - "usehooks-ts": { - ...UseHooksTS, - useLocalStorage: undefined, - useScript: undefined, - useSessionStorage: undefined, - useDocumentTitle: undefined, - useDarkMode: undefined, - }, -}; diff --git a/src/activitys/ModConfActivity/index.tsx b/src/activitys/ModConfActivity/index.tsx deleted file mode 100644 index 06438d35..00000000 --- a/src/activitys/ModConfActivity/index.tsx +++ /dev/null @@ -1,42 +0,0 @@ -import { useActivity } from "@Hooks/useActivity"; -import React from "react"; -import { SuFile } from "@Native/SuFile"; -import { ModConfView } from "@Activitys/ModConfActivity/components/ModConfView"; -import { PreviewErrorBoundary } from "../ModConfPlaygroundActivity"; - -interface ModConfActivityExtra { - /** - * ## This field is required by ModConf - */ - indexFile: string; - /** - * ## This field is required by ModConf - */ - cwd: string; - modId: string; -} - -const ModConfActivity = () => { - const { extra } = useActivity(); - - const config: string = React.useMemo(() => { - const notFound = 'import {Page} from "@mmrl/ui";export default () => Config file not found'; - - const file = new SuFile(extra.indexFile); - if (file.exist()) { - return file.read(); - } else { - return notFound; - } - }, []); - - return ( - - - - ); -}; - -export default ModConfActivity; - -export { ModConfActivityExtra }; diff --git a/src/activitys/ModConfPlaygroundActivity.tsx b/src/activitys/ModConfPlaygroundActivity.tsx deleted file mode 100644 index 2fe4c721..00000000 --- a/src/activitys/ModConfPlaygroundActivity.tsx +++ /dev/null @@ -1,279 +0,0 @@ -import { Box, Stack, styled } from "@mui/material"; -import * as React from "react"; -import useMediaQuery from "@mui/material/useMediaQuery"; -import { Toolbar } from "@Components/onsenui/Toolbar"; -import { Page } from "@Components/onsenui/Page"; -import { IntentPusher, useActivity } from "@Hooks/useActivity"; -import PreviewIcon from "@mui/icons-material/Preview"; -import * as monacoEditor from "monaco-editor/esm/vs/editor/editor.api"; -import Editor, { Monaco } from "@monaco-editor/react"; -import { ErrorBoundaryProps, ErrorBoundaryState, errorBoundaryInitialState } from "@Components/ErrorBoundary"; -import editorTheme from "@Util/editorTheme"; -import { ModConfView } from "@Activitys/ModConfActivity/components/ModConfView"; -import { useNativeStorage } from "@Hooks/useNativeStorage"; -import { useStrings } from "@Hooks/useStrings"; -import { useNativeFileStorage } from "@Hooks/useNativeFileStorage"; -import { useModFS } from "@Hooks/useModFS"; -import { Pre } from "@Components/dapi/Pre"; -import { Code } from "@Components/dapi/Code"; - -export interface PlaygroundExtra { - title: string; - editorMode?: string; - defaultText?: string; - previewPage: IntentPusher["component"]; - preview: React.FunctionComponent | React.ComponentType; -} - -export interface PreviewErrorBoundaryChildren extends React.PropsWithChildren { - hasError: boolean; -} - -interface PreviewErrorBoundaryProps extends Omit {} -interface PreviewErrorBoundaryState extends ErrorBoundaryState {} - -const preElementStyle = (theme: any) => ({ - backgroundColor: theme.palette.background.paper, - color: theme.palette.text.primary, - borderRadius: theme.shape.borderRadius / theme.shape.borderRadius, - lineHeight: 1.45, - overflow: "auto", - padding: 2, -}); - -export class PreviewErrorBoundary extends React.Component { - public constructor(props: PreviewErrorBoundaryProps) { - super(props); - this.state = errorBoundaryInitialState; - } - - public static getDerivedStateFromError(error: any) { - return { hasError: true }; - } - - public componentDidCatch(error: Error, errorInfo: React.ErrorInfo) { - this.setState({ error, errorInfo }); - } - - public render() { - if (this.state.hasError) { - return ( - { - return ( - - Complile error - - ); - }} - > -
-            {this.state.error.message}
-          
- -
-            {this.state.errorInfo.componentStack}
-          
-
- ); - } - - return this.props.children; - } -} - -const createDependencyProposals = (monaco: typeof monacoEditor, range: any): any => { - // returning a static list of proposals, not even looking at the prefix (filtering is done by the Monaco editor), - // here you could do a server side lookup - return [ - { - label: "native", - kind: monaco.languages.CompletionItemKind.Function, - documentation: "", - insertText: "native", - range: range, - }, - { - label: "ignore", - kind: monaco.languages.CompletionItemKind.Function, - documentation: "", - insertText: "// @ts-ignore", - range: range, - }, - ]; -}; - -const editorDidMount = (editor: monacoEditor.editor.IStandaloneCodeEditor, monaco: Monaco) => { - monaco.editor.defineTheme("editorTheme", editorTheme); - monaco.editor.setTheme("editorTheme"); - - monaco.languages.typescript.javascriptDefaults.setDiagnosticsOptions({ - noSemanticValidation: true, - noSyntaxValidation: true, - }); - - monaco.languages.typescript.javascriptDefaults.setCompilerOptions({ - jsx: monaco.languages.typescript.JsxEmit.React, - jsxFactory: "React.createElement", - reactNamespace: "React", - allowNonTsExtensions: true, - allowJs: true, - target: monaco.languages.typescript.ScriptTarget.ES2015, - }); - - monaco.languages.registerCompletionItemProvider("javascript", { - provideCompletionItems: (model, position) => { - const word = model.getWordUntilPosition(position); - const range = { - startLineNumber: position.lineNumber, - endLineNumber: position.lineNumber, - startColumn: word.startColumn, - endColumn: word.endColumn, - }; - return { - suggestions: createDependencyProposals(monaco, range), - }; - }, - }); - editor.focus(); -}; - -const ModConfPlaygroundActivity = () => { - const { context, extra } = useActivity(); - const { strings } = useStrings(); - const { modFS } = useModFS(); - const [description, setDescription] = useNativeFileStorage(modFS("MODCONF_PLAYGROUND"), extra.defaultText || ""); - const [errBoundKey, setErrBoundKey] = React.useState(0); - - const isLargeScreen = useMediaQuery("(min-width:600px)"); - - const handlePreview = () => { - context.pushPage({ - component: ModConfView, - noMemo: true, - key: extra.title, - extra: { - modulename: "Preview", - }, - props: { - cwd: modFS("MMRLFOL"), - indexFile: modFS("MODCONF_PLAYGROUND"), - modid: modFS("MODCONF_PLAYGROUND_MODID"), - children: description, - }, - }); - }; - - const renderToolbar = () => { - return ( - - - - - {strings("modconf_playground")} - - - - - ); - }; - - return ( - -
- - - - { - if (value) { - setErrBoundKey((prev) => prev + 1); - setDescription(value); - } - }} - onMount={editorDidMount} - options={{ - autoIndent: "full", - contextmenu: true, - fontFamily: "monospace", - fontSize: 13, - lineHeight: 24, - hideCursorInOverviewRuler: true, - matchBrackets: "always", - minimap: { - enabled: false, - }, - scrollbar: { - horizontalSliderSize: 4, - verticalSliderSize: 18, - }, - selectOnLineNumbers: true, - roundedSelection: false, - readOnly: false, - cursorStyle: "line", - automaticLayout: true, - }} - /> - - - {isLargeScreen && ( - - - - - - - - )} - - -
-
- ); -}; - -const Preview = styled("div")(({ theme }) => ({ - flex: 1, - flexBasis: "50%", - height: "100%", - width: "100%", - minHeight: "100%", - position: "relative", - borderRadius: theme.shape.borderRadius, - borderStyle: "solid", - borderWidth: "1px", - minWidth: "0%", - overflow: "auto", - borderColor: theme.palette.divider, - section: { - position: "absolute", - overflowY: "scroll", - }, - ".monaco-editor": { - borderRadius: theme.shape.borderRadius / theme.shape.borderRadius, - }, - ".overflow-guard": { - borderRadius: theme.shape.borderRadius / theme.shape.borderRadius, - }, -})); - -export default ModConfPlaygroundActivity; diff --git a/src/activitys/ModConfStandaloneActivity.tsx b/src/activitys/ModConfStandaloneActivity.tsx deleted file mode 100644 index 5e333144..00000000 --- a/src/activitys/ModConfStandaloneActivity.tsx +++ /dev/null @@ -1,80 +0,0 @@ -import { Page } from "@Components/onsenui/Page"; -import { Toolbar } from "@Components/onsenui/Toolbar"; -import { useActivity } from "@Hooks/useActivity"; -import { useModFS } from "@Hooks/useModFS"; -import { List, ListItem, ListItemButton, ListItemText } from "@mui/material"; -import { SuFile } from "@Native/SuFile"; -import ModFS from "modfs"; -import { Activities } from "."; -import { ModConfActivityExtra } from "./ModConfActivity"; - -const ModConfStandaloneActivity = () => { - const { context } = useActivity(); - const { modFS, _modFS } = useModFS(); - - const renderToolbar = () => { - return ( - - - - - ModConf Standalone - - ); - }; - - const mcalone = new SuFile(modFS("MCALONE")); - - if (!mcalone.exist()) { - return No files; - } - - return ( - - - - {mcalone.list().map((item) => { - const confFile = new SuFile(modFS("MCALONEMETA", { MODID: item })); - if (!confFile.exist()) return null; - - try { - const metaData = JSON.parse(confFile.read()); - - if (!metaData.id && metaData.id !== item) return null; - - return ( - <> - - { - context.pushPage({ - component: Activities.ModConf, - key: `${metaData.id}_configure_standalone`, - extra: { - indexFile: metaData.main - ? ModFS.format(metaData.main, { MODID: item, ..._modFS }) - : modFS("MCALONEIDX", { MODID: item }), - cwd: metaData.cwd - ? ModFS.format(metaData.cwd, { MODID: item, ..._modFS }) - : modFS("MCALONECWD", { MODID: item }), - modId: metaData.id, - }, - }); - }} - > - - - - - ); - } catch { - return null; - } - })} - - - - ); -}; - -export default ModConfStandaloneActivity; diff --git a/src/activitys/ModFSActivity.tsx b/src/activitys/ModFSActivity.tsx deleted file mode 100644 index f938ab29..00000000 --- a/src/activitys/ModFSActivity.tsx +++ /dev/null @@ -1,307 +0,0 @@ -import { Alert, Box, Button, Divider, List, ListItemText, ListSubheader, Stack, TextField, Typography } from "@mui/material"; -import AvatarGroup from "@mui/material/AvatarGroup"; -import Avatar from "@mui/material/Avatar"; -import { Toolbar } from "@Components/onsenui/Toolbar"; -import { Page } from "@Components/onsenui/Page"; -import { useActivity } from "@Hooks/useActivity"; -import { ModFS, useModFS } from "@Hooks/useModFS"; -import { Shell } from "@Native/Shell"; -import { DialogEditTextListItem } from "@Components/DialogEditTextListItem"; -import React from "react"; -import { Anchor } from "@Components/dapi/Anchor"; -import { useStrings } from "@Hooks/useStrings"; -import FlatList, { FlatListProps } from "flatlist-react"; - -interface ModFSSections { - sectionText: string; - items: ModFSListItem[]; -} - -interface ModFSListItem { - confKey: K; - text: React.ReactNode; - dialogDesc?: React.ReactNode; - /** - * Used for the config requirement - */ - logoText?: string | Array; - disabled?: boolean; - multiline?: boolean; - maxRows?: number; -} - -function ModFSActivity() { - const { context } = useActivity(); - const { strings } = useStrings(); - - const { _modFS, setModFS } = useModFS(); - - const renderToolbar = () => { - return ( - - - - - {strings("modfs")} - - ); - }; - - const items: ModFSSections[] = React.useMemo( - () => [ - { - sectionText: "Installer", - items: [ - { - text: "Magisk Install Script", - disabled: !Shell.isMagiskSU(), - logoText: "assets/MagiskSULogo.png", - // dialogDesc: ( - // <> - // - // Check the{" "} - // - // ModFS documentations - // {" "} - // for more informations! - //
- // {""}, {""} and {""} can also be used, shell supported. - //
- // - // ), - confKey: "MSUINI", - }, - { - text: "KernelSU Install Script", - disabled: !Shell.isKernelSU(), - logoText: "assets/KernelSULogo.png", - confKey: "KSUINI", - }, - { - text: "APatch Install Script", - disabled: !Shell.isAPatchSU(), - logoText: "assets/APatchSULogo.png", - confKey: "ASUINI", - }, - ], - }, - { - sectionText: "Default paths", - items: [ - { - text: "Base path", - confKey: "ADB", - }, - { - text: "MMRL path", - confKey: "MMRLFOL", - }, - { - text: "Modules path", - confKey: "MODULES", - }, - { - text: "Module work directory", - confKey: "MODULECWD", - }, - { - text: "Module properties path", - confKey: "PROPS", - }, - { - text: "Module system properties path", - confKey: "SYSTEM", - }, - { - text: "Module SEPolicy rules path", - confKey: "SEPOLICY", - }, - ], - }, - { - sectionText: "Service paths", - items: [ - { - text: "Late service path", - confKey: "LATESERVICE", - }, - { - text: "Post service path", - confKey: "POSTSERVICE", - }, - { - text: "Post mount service path", - disabled: !(Shell.isKernelSU() || Shell.isAPatchSU()), - logoText: ["assets/KernelSULogo.png", "assets/APatchSULogo.png"], - confKey: "POSTMOUNT", - }, - { - text: "Boot complete service path", - disabled: !(Shell.isKernelSU() || Shell.isAPatchSU()), - logoText: ["assets/KernelSULogo.png", "assets/APatchSULogo.png"], - confKey: "BOOTCOMP", - }, - ], - }, - { - sectionText: "Status paths", - items: [ - { - text: "Skip mount path", - confKey: "SKIPMOUNT", - }, - { - text: "Disable path", - confKey: "DISABLE", - }, - { - text: "Remove path", - confKey: "REMOVE", - }, - { - text: "Update path", - confKey: "UPDATE", - }, - ], - }, - { - sectionText: "ModConf", - items: [ - { - text: "Config working directory", - confKey: "CONFCWD", - }, - { - text: "Config index file", - confKey: "CONFINDEX", - }, - { - text: "ModConf Playground Root", - dialogDesc: ( - <> - - Check the{" "} - - ModConf documentations - {" "} - for more informations! - - - ), - confKey: "MODCONF_PLAYGROUND", - }, - { - text: "ModConf Playground Module ID", - confKey: "MODCONF_PLAYGROUND_MODID", - }, - ], - }, - { - sectionText: "ModConf Standalone", - items: [ - { - text: "Standalone root directory", - confKey: "MCALONE", - }, - { - text: "Standalone working directory", - confKey: "MCALONECWD", - }, - { - text: "Stadnalone meta file", - confKey: "MCALONEMETA", - }, - ], - }, - ], - [] - ); - - const [search, setSearch] = React.useState(""); - - const handleSearch = () => {}; - - return ( - - - - I am not responsible for anything that may happen to your phone by changing these informations. You do it at your own risk and - take the responsibility upon yourself and you are not to blame us or MMRL and its respected developers - - - setSearch(e.target.value)} /> - - ( - <> - {section.sectionText}}> - ( - { - if (value) { - setModFS(item.confKey, value); - } - }} - multiline={item.multiline} - maxRows={item.maxRows} - > - - {`<${item.confKey}>`} - - {" "} - - {item.logoText && Array.isArray(item.logoText) ? ( - <> - - {item.logoText.map((logo) => ( - - ))} - - - ) : ( - item.logoText && ( - - ) - )} - {item.text} - - - - } - secondary={_modFS[item.confKey]} - /> - - )} - renderOnScroll - renderWhenEmpty={() => <>} - /> - - - - )} - renderOnScroll - renderWhenEmpty={() => <>} - /> - - - ); -} - -export default ModFSActivity; diff --git a/src/activitys/ModuleViewActivity/index.tsx b/src/activitys/ModuleViewActivity/index.tsx deleted file mode 100644 index 7e2c7b9c..00000000 --- a/src/activitys/ModuleViewActivity/index.tsx +++ /dev/null @@ -1,433 +0,0 @@ -import { TerminalActivityExtra } from "@Activitys/InstallTerminalV2Activity"; -import { AvatarWithProgress } from "@Components/AvatarWithProgress"; -import { DropdownButton } from "@Components/DropdownButton"; -import { VerifiedIcon } from "@Components/icons/VerifiedIcon"; -import { useModuleQueue } from "@Components/ModulesQueue"; -import { Page } from "@Components/onsenui/Page"; -import { Toolbar } from "@Components/onsenui/Toolbar"; -import { useActivity } from "@Hooks/useActivity"; -import { useDownloadModule } from "@Hooks/useDownloadModule"; -import { useFormatBytes } from "@Hooks/useFormatBytes"; -import { useModuleInfo } from "@Hooks/useModuleInfo"; -import { useRepos } from "@Hooks/useRepos"; -import { useStrings } from "@Hooks/useStrings"; -import { useTheme } from "@Hooks/useTheme"; -import { VolunteerActivism } from "@mui/icons-material"; -import LayersIcon from "@mui/icons-material/Layers"; -import TelegramIcon from "@mui/icons-material/Telegram"; -import Box from "@mui/material/Box"; -import Button from "@mui/material/Button"; -import CardMedia from "@mui/material/CardMedia"; -import Divider from "@mui/material/Divider"; -import Fade from "@mui/material/Fade"; -import Stack from "@mui/material/Stack"; -import SvgIcon from "@mui/material/SvgIcon"; -import Tab from "@mui/material/Tab"; -import Tabs from "@mui/material/Tabs"; -import Typography from "@mui/material/Typography"; -import { Environment } from "@Native/Environment"; -import { os } from "@Native/Os"; -import { Shell } from "@Native/Shell"; -import { view } from "@Native/View"; -import { useConfirm } from "material-ui-confirm"; -import React from "react"; -import { Disappear } from "react-disappear"; -import { useDocumentTitle } from "usehooks-ts"; -import { Activities } from ".."; -import { AboutTab } from "./tabs/AboutTabs"; -import { OverviewTab } from "./tabs/OverviewTab"; -import { VersionsTab } from "./tabs/VersionsTab"; - -function a11yProps(index: number) { - return { - id: `simple-tab-${index}`, - "aria-controls": `simple-tabpanel-${index}`, - }; -} - -interface TabPanelProps { - children?: React.ReactNode; - index: number; - value: number; -} - -function CustomTabPanel(props: TabPanelProps) { - const { children, value, index, ...other } = props; - - return ( - - ); -} - -const ModuleViewActivity = () => { - const { strings } = useStrings(); - const confirm = useConfirm(); - const { theme } = useTheme(); - const { modules } = useRepos(); - const { context, extra } = useActivity(); - - const { id, name, version, versionCode, author, versions, track } = extra; - const { cover, icon, verified, donate, support, latestVersion, timestamp, size } = useModuleInfo(extra); - const [moduleFileSize, moduleFileSizeByteText] = useFormatBytes(size); - - const search = React.useMemo(() => new URLSearchParams(window.location.search), [window.location.search]); - - const { addModule: addModuleToQueue } = useModuleQueue(); - - useDocumentTitle(`${name} — MMRL`, { preserveTitleOnUnmount: false }); - - React.useEffect(() => { - search.set("module", id); - const newRelativePathQuery = window.location.pathname + "?" + search.toString(); - history.pushState(null, "", newRelativePathQuery); - return () => { - search.delete("module"); - const newRelativePathQuery = window.location.pathname + search.toString(); - history.pushState(null, "", newRelativePathQuery); - }; - }, []); - - const renderToolbar = () => { - return ( - - - - - - - {name} - - - - { - os.open( - `https://t.me/share/url?url=${encodeURIComponent(window.location.href)}&text=${encodeURIComponent( - "Check out this module on MMRL. Requires a machted repo to open this module. " - )}`, - { - target: "_blank", - features: { - color: theme.palette.primary.main, - }, - } - ); - }} - /> - - - ); - }; - - const [isNameVisible, setIsNameVisible] = React.useState(true); - - const boxRef = React.useRef(null); - const [value, setValue] = React.useState(0); - const handleChange = (event: React.SyntheticEvent, newValue: number) => { - setValue(newValue); - }; - - const cconfirm = useConfirm(); - - const [startDL, progress] = useDownloadModule(); - - return ( - - - {cover && ( - ({ - background: `linear-gradient(to top,${ - theme.palette.background.default - } 0,rgba(0,0,0,0) calc(56% - ${view.getWindowTopInsets()}px))`, - })} - > - - - - - - - - - - - - - - )} - - ({ - pt: cover ? 0 : 2, - pl: 2, - pr: 2, - pb: 2, - backgroundColor: theme.palette.background.default, - color: "white", - display: "flex", - flexDirection: "column", - alignItems: "flex-start", - })} - > - - ({ - bgcolor: theme.palette.primary.dark, - width: 100, - height: 100, - boxShadow: "0 -1px 5px rgba(0,0,0,.09), 0 3px 5px rgba(0,0,0,.06), 0 1px 2px rgba(0,0,0,.3), 0 1px 3px rgba(0,0,0,.15)", - borderRadius: "20%", - fontSize: 50, - })} - src={icon} - progressTextVariant="body2" - > - {name.charAt(0).toUpperCase()} - - - - - setIsNameVisible(!visible)}> - {name} - - - - {author} - - - - - {/* DL SECTION */} - - - } - spacing={2} - > - - - {version} - - - name - - - - - - {versionCode} - - - code - - - {size && ( - - - {moduleFileSize} - - - {moduleFileSizeByteText} - - - )} - {donate && ( - { - os.open(donate, { - target: "_blank", - features: { - color: theme.palette.primary.main, - }, - }); - }} - sx={{ cursor: "pointer" }} - > - - - - - donate - - - )} - - - - - { - const lasSeg = new URL(latestVersion.zipUrl).pathname.split("/").pop(); - const dlPath = Environment.getPublicDir(Environment.DIRECTORY_DOWNLOADS) + "/" + lasSeg; - startDL(latestVersion.zipUrl, dlPath); - }, - }, - { - children: strings("install"), - disabled: !(os.isAndroid && (Shell.isMagiskSU() || Shell.isKernelSU() || Shell.isAPatchSU())), - onClick: () => { - confirm({ - title: strings("install_module", { name: name }), - description: strings("install_module_dialog_desc", { name: {name} }), - confirmationText: strings("yes"), - }).then(() => { - context.pushPage({ - component: Activities.InstallTerminal, - key: "InstallTerminalV2Activity", - extra: { - issues: support, - source: track.source, - exploreInstall: true, - modSource: [latestVersion.zipUrl], - }, - }); - }); - }, - }, - { - children: strings("update"), - disabled: true, - onClick: () => { - console.log("Rebase and merge"); - }, - }, - ]} - /> - - - - - - - - - - - - - - - - - - - - - - - ); -}; - -export default ModuleViewActivity; diff --git a/src/activitys/ModuleViewActivity/tabs/AboutTabs.tsx b/src/activitys/ModuleViewActivity/tabs/AboutTabs.tsx deleted file mode 100644 index d69f0a8e..00000000 --- a/src/activitys/ModuleViewActivity/tabs/AboutTabs.tsx +++ /dev/null @@ -1,224 +0,0 @@ -import { Activities } from "@Activitys/index"; -import { Pre } from "@Components/dapi/Pre"; -import { useActivity } from "@Hooks/useActivity"; -import { useModuleInfo } from "@Hooks/useModuleInfo"; -import { useStrings } from "@Hooks/useStrings"; -import { useTheme } from "@Hooks/useTheme"; -import BugReportIcon from "@mui/icons-material/BugReport"; -import FormatAlignLeftIcon from "@mui/icons-material/FormatAlignLeft"; -import GitHubIcon from "@mui/icons-material/GitHub"; -import VerifiedIcon from "@mui/icons-material/Verified"; -import { Avatar, AvatarGroup, Box, Divider, ListSubheader, Stack, Typography } from "@mui/material"; -import List from "@mui/material/List"; -import ListItem from "@mui/material/ListItem"; -import ListItemButton from "@mui/material/ListItemButton"; -import ListItemIcon from "@mui/material/ListItemIcon"; -import ListItemText from "@mui/material/ListItemText"; -import { os } from "@Native/Os"; -import React from "react"; - -const preSx = { display: "inline" }; - -const AboutTab = () => { - const { strings } = useStrings(); - const { context, extra } = useActivity(); - const { theme } = useTheme(); - - const { track, features } = extra; - const { license, verified, support } = useModuleInfo(extra); - - return ( - <> - - {verified && ( - - - - - - - )} - - {license && ( - { - fetch(`https://raw.githubusercontent.com/spdx/license-list-data/main/website/${license}.json`) - .then((res) => { - if (res.status === 200) { - return res.json(); - } else { - throw new Error("Fetching license failed"); - } - }) - .then((json: LicenseSPX) => { - context.pushPage({ - component: Activities.FetchText, - key: "license_" + license, - extra: { - raw_data: json.licenseText, - modulename: json.name, - }, - }); - }) - .catch((err) => {}); - }} - > - - - - - - )} - - {support && ( - { - os.open(support, { - target: "_blank", - features: { - color: theme.palette.primary.main, - }, - }); - }} - > - - - - - - )} - - { - os.open(track.source, { - target: "_blank", - features: { - color: theme.palette.primary.main, - }, - }); - }} - > - - - - - - - - {features && Object.keys(features).length !== 0 && ( - <> - - - {strings("features")}}> - - This module contains the file
service.sh
. This file will be executed in late_start service. - - } - /> - - This module contains the file
post-fs-data.sh
. This file will be executed in post-fs-data. - - } - /> - - This module will manipulate system properties with
resetprop
- - } - /> - - - - - - This module contains the file
post-mount.sh
. This file will be executed in post-mount. - - } - icons={["assets/KernelSULogo.png", "assets/APatchSULogo.png"]} - /> - - This module contains the file
boot-completed.sh
. This file will be executed when the boot is - completed. - - } - icons={["assets/KernelSULogo.png", "assets/APatchSULogo.png"]} - /> - - - -
- - )} - - ); -}; - -interface FeatureItemProps { - feat: boolean | undefined; - title: React.ReactNode; - icons?: string[]; - desc: React.ReactNode; -} - -const FeatureItem = React.memo(({ feat, title, desc, icons }) => { - if (feat) { - return ( - - - - - {icons && Array.isArray(icons) ? ( - <> - - {icons.map((icon) => ( - - ))} - - - ) : ( - icons && - )} - {title} - - - - } - secondary={desc} - /> - - ); - } -}); - -export { AboutTab }; - diff --git a/src/activitys/ModuleViewActivity/tabs/OverviewTab.tsx b/src/activitys/ModuleViewActivity/tabs/OverviewTab.tsx deleted file mode 100644 index e75b8c45..00000000 --- a/src/activitys/ModuleViewActivity/tabs/OverviewTab.tsx +++ /dev/null @@ -1,298 +0,0 @@ -import { Activities } from "@Activitys/index"; -import AntiFeatureListItem from "@Components/AntiFeatureListItem"; -import { Image } from "@Components/dapi/Image"; -import { useActivity } from "@Hooks/useActivity"; -import { useBlacklist } from "@Hooks/useBlacklist"; -import { useCategories } from "@Hooks/useCategories"; -import { useFetch } from "@Hooks/useFetch"; -import { useFormatDate } from "@Hooks/useFormatDate"; -import { useLowQualityModule } from "@Hooks/useLowQualityModule"; -import { useModuleInfo } from "@Hooks/useModuleInfo"; -import { useRepos } from "@Hooks/useRepos"; -import { useSettings } from "@Hooks/useSettings"; -import { useStrings } from "@Hooks/useStrings"; -import { useSupportedRoot } from "@Hooks/useSupportedRoot"; -import ArrowForwardIcon from "@mui/icons-material/ArrowForward"; -import Alert from "@mui/material/Alert"; -import AlertTitle from "@mui/material/AlertTitle"; -import Box from "@mui/material/Box"; -import Card from "@mui/material/Card"; -import CardContent from "@mui/material/CardContent"; -import Chip from "@mui/material/Chip"; -import IconButton from "@mui/material/IconButton"; -import ImageList from "@mui/material/ImageList"; -import ImageListItem from "@mui/material/ImageListItem"; -import List from "@mui/material/List"; -import ListItem from "@mui/material/ListItem"; -import ListItemButton from "@mui/material/ListItemButton"; -import ListItemText from "@mui/material/ListItemText"; -import Stack from "@mui/material/Stack"; -import Typography from "@mui/material/Typography"; -import { Build } from "@Native/Build"; -import { os } from "@Native/Os"; -import { Shell } from "@Native/Shell"; -import React from "react"; - -const colorHandler = (color?: ModuleNoteColors) => { - switch (color) { - case "green": - case "success": - return "success"; - - case "info": - case "blue": - return "info"; - - case "warning": - case "yellow": - return "warning"; - - case "error": - case "red": - return "error"; - - default: - return "info"; - } -}; - -const OverviewTab = () => { - const { strings } = useStrings(); - const { context, extra } = useActivity(); - const { modules } = useRepos(); - const { id, name, description, versions, minApi, note, track } = extra; - - const { icon, screenshots, require, readme: moduleReadme, categories, root } = useModuleInfo(extra); - const [isModuleSupported, currentRootVersion] = useSupportedRoot(root, []); - - const { filteredCategories } = useCategories(categories); - - const [lowQualityModule] = useSettings("_low_quality_module"); - const isLowQuality = useLowQualityModule(extra, !lowQualityModule); - const latestVersion = React.useMemo(() => versions[versions.length - 1], [versions]); - const formatLastUpdate = useFormatDate(latestVersion.timestamp); - - const blacklistedModules = useBlacklist(); - const findHardCodedAntifeature = React.useMemo(() => { - return [...(track.antifeatures || []), ...(blacklistedModules.find((mod) => mod.id === id)?.antifeatures || [])]; - }, [id, track.antifeatures]); - - const [readme] = useFetch(moduleReadme, { type: "text" }); - - return ( - <> - - {note && ( - - {note.title && {note.title}} - {note.message} - - )} - - {isLowQuality && ( - - {strings("low_quality_module")} - {strings("low_quality_module_warn")} - - )} - - {minApi && minApi > os.sdk && ( - - {strings("module_require_android_ver", { andro_ver: Build.parseVersion(minApi) })} - - )} - - {!isModuleSupported && ( - - {strings("unsupported_root", { manager: Shell.getRootManagerV2(), version: currentRootVersion })} - - )} - - - - - - {strings("about_this_module")} - - {readme && ( - { - context.pushPage({ - component: Activities.Description, - key: "DescriptonActivity", - extra: { - desc: readme, - name: name, - logo: icon, - }, - }); - }} - sx={{ ml: 0.5 }} - > - - - )} - - - - {description} - - - {strings("updated_on")} - - {formatLastUpdate} - - - {filteredCategories.length !== 0 && ( - - {filteredCategories.map((category) => ( - - ))} - - )} - - - - {findHardCodedAntifeature && findHardCodedAntifeature.length !== 0 && ( - - - - {strings("antifeatures")} - - - - {typeof findHardCodedAntifeature === "string" ? ( - - ) : ( - Array.isArray(findHardCodedAntifeature) && findHardCodedAntifeature.map((anti) => ) - )} - - - - )} - - {require && require.length !== 0 && ( - - - - {"Dependencies"} - - - - - - {require.map((req) => { - const findRequire = React.useMemo(() => modules.find((module) => module.id === req), [modules]); - - if (findRequire) { - return ( - { - context.pushPage({ - component: Activities.ModuleView, - key: "ModuleViewActivity", - extra: findRequire, - }); - }} - > - - - ); - } else { - return ( - - - - ); - } - })} - - - - )} - - {screenshots && screenshots.length !== 0 && ( - - - - {strings("images")} - - - - - {screenshots.map((image, i) => ( - ({ - ml: 1, - mr: 1, - })} - > - - - ))} - - - )} - - - ); -}; - -export { OverviewTab }; diff --git a/src/activitys/ModuleViewActivity/tabs/VersionsTab.tsx b/src/activitys/ModuleViewActivity/tabs/VersionsTab.tsx deleted file mode 100644 index 5f9e551f..00000000 --- a/src/activitys/ModuleViewActivity/tabs/VersionsTab.tsx +++ /dev/null @@ -1,142 +0,0 @@ -import { Activities } from "@Activitys/index"; -import { useActivity } from "@Hooks/useActivity"; -import { useDownloadModule } from "@Hooks/useDownloadModule"; -import { useFormatDate } from "@Hooks/useFormatDate"; -import { useStrings } from "@Hooks/useStrings"; -import { useTheme } from "@Hooks/useTheme"; -import { Environment } from "@Native/Environment"; -import { os } from "@Native/Os"; -import { Shell } from "@Native/Shell"; -import DownloadIcon from "@mui/icons-material/Download"; -import InstallMobileIcon from "@mui/icons-material/InstallMobile"; -import ManageHistoryIcon from "@mui/icons-material/ManageHistory"; -import Chip from "@mui/material/Chip"; -import IconButton from "@mui/material/IconButton"; -import LinearProgress from "@mui/material/LinearProgress"; -import List from "@mui/material/List"; -import ListItem from "@mui/material/ListItem"; -import ListItemText from "@mui/material/ListItemText"; -import Stack from "@mui/material/Stack"; -import Typography from "@mui/material/Typography"; -import { useConfirm } from "material-ui-confirm"; -import React from "react"; - -const VersionsTab = () => { - const { context, extra } = useActivity(); - const { id, versions } = extra; - - return ( - - {versions.toReversed().map((version, index) => ( - - ))} - - ); -}; - -interface VersionItemProps { - index: number; - id: string; - version: Version; -} - -const VersionItem = React.memo(({ id, version, index }) => { - const ts = useFormatDate(version.timestamp); - const { context, extra } = useActivity(); - const confirm = useConfirm(); - const { strings } = useStrings(); - const { theme } = useTheme(); - - const [startDL, progress] = useDownloadModule(); - - const { track, support } = extra; - - const versionName = `${version.version} (${version.versionCode})`; - - const handleInstall = () => { - confirm({ - title: `Install ${versionName}?`, - confirmationText: "Yes", - }).then(() => { - context.pushPage({ - component: Activities.InstallTerminal, - key: "InstallTerminalV2Activity", - extra: { - issues: support, - source: track.source, - id: id, - exploreInstall: true, - modSource: [version.zipUrl], - }, - }); - }); - }; - - return ( - - {version.changelog && ( - { - context.pushPage({ - component: Activities.FetchText, - key: `changelog_${id}`, - extra: { - title: version.version, - url: version.changelog, - }, - }); - }} - edge="end" - aria-label="download" - > - - - )} - - {os.isAndroid && (Shell.isMagiskSU() || Shell.isKernelSU() || Shell.isAPatchSU()) && ( - - - - )} - - { - const lasSeg = new URL(version.zipUrl).pathname.split("/").pop(); - const dlPath = Environment.getPublicDir(Environment.DIRECTORY_DOWNLOADS) + "/" + lasSeg; - startDL(version.zipUrl, dlPath); - }} - edge="end" - aria-label="download" - > - - - - } - > - - {versionName} - {index === 0 && } - - } - secondary={ts} - /> - {progress !== 0 && ( - - )} - - ); -}); - -export { VersionsTab }; - diff --git a/src/activitys/NoRootActivity.tsx b/src/activitys/NoRootActivity.tsx deleted file mode 100644 index 02be7956..00000000 --- a/src/activitys/NoRootActivity.tsx +++ /dev/null @@ -1,84 +0,0 @@ -import { Page } from "@Components/onsenui/Page"; -import { Toolbar } from "@Components/onsenui/Toolbar"; -import { useStrings } from "@Hooks/useStrings"; -import Button from "@mui/material/Button"; -import ButtonGroup from "@mui/material/ButtonGroup"; -import Card from "@mui/material/Card"; -import CardContent from "@mui/material/CardContent"; -import Typography from "@mui/material/Typography"; -import React from "react"; - -type RootManager = { - name: string; - package: string; -}; - -const NoRootActivity = () => { - const { strings } = useStrings(); - const rootManagers: RootManager[] = [ - { - name: "Open KernelSU", - package: "me.weishu.kernelsu", - }, - { - name: "Open Magisk", - package: "com.topjohnwu.magisk", - }, - { - name: "Open Magisk Delta", - package: "io.github.huskydg.magisk", - }, - { - name: "Open APatch", - package: "me.bmax.apatch", - }, - ]; - - const renderToolbar = () => { - return ( - - {strings("no_root")} - - ); - }; - - const getRootManagers = rootManagers.filter((manager) => window.__os__.isPackageInstalled(manager.package)); - - return ( - - - - - - {strings("failed")}! - - - {strings("no_root_message")} - - - - - {getRootManagers.map((manager) => ( - - ))} - - - - ); -}; - -export default NoRootActivity; diff --git a/src/activitys/PicturePreviewActivity.tsx b/src/activitys/PicturePreviewActivity.tsx deleted file mode 100644 index 93c34e3e..00000000 --- a/src/activitys/PicturePreviewActivity.tsx +++ /dev/null @@ -1,70 +0,0 @@ -import React from "react"; -import { Page } from "@Components/onsenui/Page"; -import { Toolbar } from "@Components/onsenui/Toolbar"; -import { TransformWrapper, TransformComponent } from "react-zoom-pan-pinch"; -import { useActivity } from "@Hooks/useActivity"; -import Box from "@mui/material/Box"; - -const generalStyle = { - width: "100%", - height: `100%`, -}; - -interface PicturePreviewActivityExtra { - picture?: string; - src: string; -} - -const PicturePreviewActivity = () => { - const { context, extra } = useActivity(); - const { picture, src } = extra; - const [fullscreen, setFullscreen] = React.useState(false); - - const renderToolbar = () => { - return ( - - - - - - - ); - }; - - if (typeof (picture || src) !== "string") throw new TypeError("'src' is undefined in PicturePreviewActivity"); - - return ( - - - - - - - - ); -}; - -export default PicturePreviewActivity; diff --git a/src/activitys/PlaygroundsActivity.tsx b/src/activitys/PlaygroundsActivity.tsx deleted file mode 100644 index 93992a87..00000000 --- a/src/activitys/PlaygroundsActivity.tsx +++ /dev/null @@ -1,245 +0,0 @@ -import { Box, Stack, styled } from "@mui/material"; -import * as React from "react"; -import useMediaQuery from "@mui/material/useMediaQuery"; -import ArrowBackIcon from "@mui/icons-material/ArrowBack"; -import { Toolbar } from "@Components/onsenui/Toolbar"; -import { Page } from "@Components/onsenui/Page"; -import { IntentPusher, useActivity } from "@Hooks/useActivity"; -import PreviewIcon from "@mui/icons-material/Preview"; -import * as monacoEditor from "monaco-editor/esm/vs/editor/editor.api"; -import Editor, { Monaco } from "@monaco-editor/react"; -import { ErrorBoundaryProps, ErrorBoundaryState, errorBoundaryInitialState } from "@Components/ErrorBoundary"; -import editorTheme from "@Util/editorTheme"; -import { useNativeStorage } from "@Hooks/useNativeStorage"; -import { useModFS } from "@Hooks/useModFS"; - -export interface PlaygroundExtra { - title: string; - editorMode?: string; - defaultText?: string; - previewPage: IntentPusher["component"]; - preview: React.FunctionComponent | React.ComponentType; -} - -export interface PreviewErrorBoundaryChildren extends React.PropsWithChildren { - modid: string; - hasError: boolean; -} - -interface PreviewErrorBoundaryProps extends Omit { - modid: string; - renderElement: React.FunctionComponent | React.ComponentType; -} -interface PreviewErrorBoundaryState extends ErrorBoundaryState {} - -export class PreviewErrorBoundary extends React.Component { - public constructor(props: PreviewErrorBoundaryProps) { - super(props); - this.state = errorBoundaryInitialState; - } - - public static getDerivedStateFromError(error: any) { - return { hasError: true }; - } - - public componentDidCatch(error: Error, errorInfo: React.ErrorInfo) { - this.setState({ error, errorInfo }); - } - - public render() { - if (this.state.hasError) { - return ( - -
{this.state.error?.message}
-
- ); - } - - return ; - } -} - -const createDependencyProposals = (monaco: typeof monacoEditor, range: any): any => { - // returning a static list of proposals, not even looking at the prefix (filtering is done by the Monaco editor), - // here you could do a server side lookup - return [ - { - label: "native", - kind: monaco.languages.CompletionItemKind.Function, - documentation: "", - insertText: "native", - range: range, - }, - { - label: "ignore", - kind: monaco.languages.CompletionItemKind.Function, - documentation: "", - insertText: "// @ts-ignore", - range: range, - }, - ]; -}; - -const editorDidMount = (editor: monacoEditor.editor.IStandaloneCodeEditor, monaco: Monaco) => { - monaco.editor.defineTheme("editorTheme", editorTheme); - monaco.editor.setTheme("editorTheme"); - - monaco.languages.typescript.javascriptDefaults.setDiagnosticsOptions({ - noSemanticValidation: true, - noSyntaxValidation: true, - }); - - monaco.languages.typescript.javascriptDefaults.setCompilerOptions({ - jsx: monaco.languages.typescript.JsxEmit.React, - jsxFactory: "React.createElement", - reactNamespace: "React", - allowNonTsExtensions: true, - allowJs: true, - target: monaco.languages.typescript.ScriptTarget.ES2015, - }); - - monaco.languages.registerCompletionItemProvider("javascript", { - provideCompletionItems: (model, position) => { - const word = model.getWordUntilPosition(position); - const range = { - startLineNumber: position.lineNumber, - endLineNumber: position.lineNumber, - startColumn: word.startColumn, - endColumn: word.endColumn, - }; - return { - suggestions: createDependencyProposals(monaco, range), - }; - }, - }); - editor.focus(); -}; - -const PlaygroundsActivity = () => { - const { context, extra } = useActivity(); - const { modFS } = useModFS(); - - const [description, setDescription] = useNativeStorage("playground_" + extra.title, extra.defaultText || ""); - const [errBoundKey, setErrBoundKey] = React.useState(0); - - const isLargeScreen = useMediaQuery("(min-width:600px)"); - - const handlePreview = () => { - context.pushPage({ - component: extra.previewPage, - key: extra.title, - extra: { - modulename: "Preview", - raw_data: description, - }, - }); - }; - - const renderToolbar = () => { - return ( - - - - - {extra.title} - {!isLargeScreen && } - - ); - }; - - return ( - -
- - - - { - if (value) { - setErrBoundKey((prev) => prev + 1); - setDescription(value); - } - }} - onMount={editorDidMount} - options={{ - autoIndent: "full", - contextmenu: true, - fontFamily: "monospace", - fontSize: 13, - lineHeight: 24, - hideCursorInOverviewRuler: true, - matchBrackets: "always", - minimap: { - enabled: false, - }, - scrollbar: { - horizontalSliderSize: 4, - verticalSliderSize: 18, - }, - selectOnLineNumbers: true, - roundedSelection: false, - readOnly: false, - cursorStyle: "line", - automaticLayout: true, - }} - /> - - - {isLargeScreen && ( - - - - - - )} - - -
-
- ); -}; - -const Preview = styled("div")(({ theme }) => ({ - flex: 1, - flexBasis: "50%", - height: "100%", - width: "100%", - minHeight: "100%", - position: "relative", - borderRadius: theme.shape.borderRadius, - borderStyle: "solid", - borderWidth: "1px", - minWidth: "0%", - overflow: "auto", - borderColor: "rgba(0, 0, 0, 0.23)", - section: { - position: "absolute", - overflowY: "scroll", - }, - ".monaco-editor": { - borderRadius: theme.shape.borderRadius / theme.shape.borderRadius, - }, - ".overflow-guard": { - borderRadius: theme.shape.borderRadius / theme.shape.borderRadius, - }, -})); - -export default PlaygroundsActivity; diff --git a/src/activitys/RepoActivity/components/LocalRepository.tsx b/src/activitys/RepoActivity/components/LocalRepository.tsx deleted file mode 100644 index 1e9c3e43..00000000 --- a/src/activitys/RepoActivity/components/LocalRepository.tsx +++ /dev/null @@ -1,172 +0,0 @@ -import React from "react"; -import ExpandLess from "@mui/icons-material/ExpandLess"; -import ExpandMore from "@mui/icons-material/ExpandMore"; -import Collapse from "@mui/material/Collapse"; -import { useRepos } from "@Hooks/useRepos"; -import { useStrings } from "@Hooks/useStrings"; -import { IconButton, List, ListItem, ListItemButton, ListItemIcon, ListItemText, Switch, Typography } from "@mui/material"; -import { OverridableComponent } from "@mui/material/OverridableComponent"; -import { SvgIconTypeMap } from "@mui/material/SvgIcon/SvgIcon"; -import { DeleteRounded, LanguageRounded, SupportRounded, UploadFileRounded, VolunteerActivismRounded } from "@mui/icons-material"; -import { os } from "@Native/Os"; -import { useFormatDate } from "@Hooks/useFormatDate"; -import { useConfirm } from "material-ui-confirm"; -import DeleteIcon from "@mui/icons-material/Delete"; -import { useFetch } from "@Hooks/useFetch"; - -interface ListItemProps { - part?: any; - text: React.ReactNode; - icon: OverridableComponent>; - onClick: () => void; -} - -interface LocalRepositoryProps { - repo: RepoConfig; -} - -const MListItem = React.memo((props) => { - return props.part ? ( - - - - - - - ) : null; -}); - -export const LocalRepository = React.memo((props) => { - const { repo } = props; - const { strings } = useStrings(); - const confirm = useConfirm(); - const { actions } = useRepos(); - const [enabled, setEnabled] = React.useState(actions.isRepoEnabled(repo.base_url)); - const [open, setOpen] = React.useState(false); - - const [data] = useFetch(`${repo.base_url}json/modules.json`); - - const formatLastUpdate = useFormatDate(data ? data.metadata.timestamp : 0); - - const handleRepoDelete = () => { - confirm({ - title: "Delete?", - confirmationText: "Sure", - description: strings("confirm_repo_delete", { - name: repo.name, - }), - }).then(() => { - actions.removeRepo({ - id: repo.base_url, - }); - }); - }; - - if (!data) { - return ( - - - - } - > - - - ); - } - - const handleClick = () => { - setOpen(!open); - }; - - return ( - <> - - {open ? : } - - {formatLastUpdate} - - - Holds{" "} - - {data.modules.length.toString()} - {" "} - modules - - - } - /> - - , checked: boolean) => { - actions.setRepoEnabled({ - id: repo.base_url, - callback(state) { - setEnabled(!state.some((elem) => elem === repo.base_url)); - }, - }); - }} - checked={enabled} - /> - - - - { - if (repo.website) { - os.open(repo.website); - } - }} - /> - { - if (repo.support) { - os.open(repo.support); - } - }} - /> - { - if (repo.donate) { - os.open(repo.donate); - } - }} - /> - { - if (repo.submission) { - os.open(repo.submission); - } - }} - /> - - - - - ); -}); diff --git a/src/activitys/RepoActivity/components/RecommendedRepo.tsx b/src/activitys/RepoActivity/components/RecommendedRepo.tsx deleted file mode 100644 index 438ab232..00000000 --- a/src/activitys/RepoActivity/components/RecommendedRepo.tsx +++ /dev/null @@ -1,38 +0,0 @@ -import IconButton from "@mui/material/IconButton"; -import AddIcon from "@mui/icons-material/Add"; -import { useRepos } from "@Hooks/useRepos"; -import { os } from "@Native/Os"; -import { ListItem, ListItemText } from "@mui/material"; - -interface RecommendedRepoProps { - name: string; - link: string; -} - -export const RecommendedRepo = (props: RecommendedRepoProps) => { - const { actions } = useRepos(); - - return ( - { - actions.addRepo({ - url: props.link, - callback: (state) => {}, - error: (error) => { - os.toast(error.message, Toast.LENGTH_SHORT); - }, - }); - }} - > - - - } - > - - - ); -}; diff --git a/src/activitys/RepoActivity/index.tsx b/src/activitys/RepoActivity/index.tsx deleted file mode 100644 index e493cab9..00000000 --- a/src/activitys/RepoActivity/index.tsx +++ /dev/null @@ -1,162 +0,0 @@ -import { Add } from "@mui/icons-material"; -import { - Box, - Button, - Dialog, - DialogActions, - DialogContent, - DialogContentText, - DialogTitle, - Divider, - List, - ListSubheader, - Stack, - TextField, - Typography, -} from "@mui/material"; -import { useRepos } from "@Hooks/useRepos"; -import React from "react"; -import { useActivity } from "@Hooks/useActivity"; -import { Toolbar } from "@Components/onsenui/Toolbar"; -import ArrowBackIcon from "@mui/icons-material/ArrowBack"; -import { os } from "@Native/Os"; -import { useStrings } from "@Hooks/useStrings"; -import { Page } from "@Components/onsenui/Page"; -import { RecommendedRepo } from "./components/RecommendedRepo"; -import { LocalRepository } from "./components/LocalRepository"; -import { useNetwork } from "@Hooks/useNetwork"; - -const recommended_repos = [ - { - name: "Magisk Modules Alternative Repository", - link: "https://magisk-modules-alt-repo.github.io/json-v2/", - }, - { - name: "Googlers Magisk Repo", - link: "https://gr.dergoogler.com/gmr/", - }, - { - name: "IzzyOnDroid Magisk Repository", - link: "https://apt.izzysoft.de/magisk/", - }, - // { - // name: "Magisk Modules Repo (Official)", - // link: "https://gr.dergoogler.com/mmr/", - // }, -]; - -const MemoizdRecommendedRepos = React.memo((props) => { - const { strings } = useStrings(); - const { repos } = useRepos(); - return ( - <> - {repos.length !== 0 && } - ({ bgcolor: theme.palette.background.default })}>{strings("explore_repositories")} - } - > - {recommended_repos.map((repo, index) => ( - - ))} - - - ); -}); - -const RepoActivity = () => { - const { isNetworkAvailable } = useNetwork(); - const { context } = useActivity(); - const { strings } = useStrings(); - - const { repos, actions } = useRepos(); - const [repoLink, setRepoLink] = React.useState(""); - - const [open, setOpen] = React.useState(false); - - const handleClickOpen = () => { - if (isNetworkAvailable) { - setOpen(true); - } else { - os.toast("Please chack your internet connection", Toast.LENGTH_SHORT); - } - }; - - const handleClose = () => { - setOpen(false); - }; - - const handleRepoLinkChange = (event: React.ChangeEvent) => { - setRepoLink(event.target.value); - }; - - const renderToolbar = () => { - return ( - - - - - {strings("repositories")} - - - - - ); - }; - - return ( - <> - - - - {repos.map((repo, index) => ( - - ))} - - {isNetworkAvailable && } - - - - {strings("add_repository")} - - {strings("add_repository_description")} - - - - - - - - - - ); -}; - -export default RepoActivity; diff --git a/src/activitys/SearchActivity.tsx b/src/activitys/SearchActivity.tsx deleted file mode 100644 index f7adf2f2..00000000 --- a/src/activitys/SearchActivity.tsx +++ /dev/null @@ -1,109 +0,0 @@ -import React from "react"; -import FlatList, { SearchOptionsInterface, FlatListProps } from "flatlist-react"; -import { useActivity } from "@Hooks/useActivity"; -import { Toolbar } from "@Components/onsenui/Toolbar"; -import { Page } from "@Components/onsenui/Page"; -import { useStrings } from "@Hooks/useStrings"; -import InputBase from "@mui/material/InputBase"; -import ClearIcon from "@mui/icons-material/Clear"; -import { Box, List } from "@mui/material"; -import { renderFunc } from "flatlist-react/lib/___subComponents/uiFunctions"; -import { useTheme } from "@Hooks/useTheme"; - -const RenderWhenEmpty = React.memo(() => { - const { theme } = useTheme(); - return ( - - What you looking for? - - ); -}); - -interface SearchActivityProps { - list: any[]; - placeholder?: string; - initialSearch?: string; - search?: SearchOptionsInterface; - group?: FlatListProps["group"]; - renderList: renderFunc; -} - -function SearchActivity(props: SearchActivityProps) { - const { strings } = useStrings(); - const { context } = useActivity(); - - const { placeholder, list, renderList, initialSearch = "" } = props; - const __placeholder = placeholder ? placeholder : (strings("search") as string); - - const [search, setSearch] = React.useState(initialSearch); - const __renderList = React.useCallback(renderList, [search, list]); - - const renderToolbar = () => { - return ( - - - - - - setSearch(e.target.value)} - placeholder={__placeholder} - /> - - - - { - setSearch(""); - }} - /> - - - ); - }; - - const __list = React.useMemo(() => (search ? list : []), [search]); - - return ( - - - - } - renderOnScroll - search={{ term: search, ...props.search }} - group={props.group} - /> - - - - ); -} - -export { SearchActivity, SearchActivityProps }; diff --git a/src/activitys/SettingsActivity.tsx b/src/activitys/SettingsActivity.tsx deleted file mode 100644 index 67bdfdac..00000000 --- a/src/activitys/SettingsActivity.tsx +++ /dev/null @@ -1,267 +0,0 @@ -import { Divider, List, ListItem, ListItemButton, ListItemText, ListSubheader, Switch } from "@mui/material"; -import { BuildConfig } from "@Native/BuildConfig"; -import { Toolbar } from "@Components/onsenui/Toolbar"; -import ArrowBackIcon from "@mui/icons-material/ArrowBack"; -import { Page } from "@Components/onsenui/Page"; -import { useStrings } from "@Hooks/useStrings"; -import { useActivity } from "@Hooks/useActivity"; -import { termScrollBehaviors, useSettings } from "@Hooks/useSettings"; -import { ListPickerItem } from "@Components/ListPickerItem"; -import { os } from "@Native/Os"; -import { useTheme } from "@Hooks/useTheme"; -import { useRepos } from "@Hooks/useRepos"; -import { Shell } from "@Native/Shell"; -import { Properties } from "@Native/Properties"; -import { useLanguageMap } from "./../locales/declaration"; -import { useModFS } from "@Hooks/useModFS"; -import { useLocalModules } from "@Hooks/useLocalModules"; - -function SettingsActivity() { - const { context } = useActivity(); - const { _modFS } = useModFS(); - const { strings } = useStrings(); - const availableLangs = useLanguageMap(); - const { setRepos } = useRepos(); - - const { theme } = useTheme(); - - // Prefs - const [swipeableTabs, setSwipeableTabs] = useSettings("swipeable_tabs"); - const [linkProtection, setLinkProtection] = useSettings("link_protection"); - const [lowQualityModule, setLowQualityModule] = useSettings("_low_quality_module"); - const [termScrollBottom, setTermScrollBottom] = useSettings("term_scroll_bottom"); - const [printTerminalError, setPrintTerminalError] = useSettings("print_terminal_error"); - const [terminalWordWrap, setTerminalWordWrap] = useSettings("terminal_word_wrap"); - const [terminalNumbericLines, setTerminalNumbericLines] = useSettings("terminal_numberic_lines"); - const [erudaConsoleEnabled, setErudaConsoleEnabled] = useSettings("eruda_console_enabled"); - - const renderToolbar = () => { - return ( - - - - - {strings("settings")} - - ); - }; - - const localModules = useLocalModules(); - - return ( - - - {strings("appearance")}}> - - - { - setSwipeableTabs(e.target.checked); - }} - checked={swipeableTabs} - /> - - - - - - - {strings("security")}}> - - - { - setLinkProtection(e.target.checked); - }} - checked={linkProtection} - /> - - - - - - {strings("module")}}> - - - { - setLowQualityModule(e.target.checked); - }} - checked={lowQualityModule} - /> - - - - {os.isAndroid && ( - <> - - {strings("terminal")}}> - - - { - setTermScrollBottom(e.target.checked); - }} - checked={termScrollBottom} - /> - - - - { - setPrintTerminalError(e.target.checked); - }} - checked={printTerminalError} - /> - - - - { - setTerminalWordWrap(e.target.checked); - }} - checked={terminalWordWrap} - /> - - - - { - setTerminalNumbericLines(e.target.checked); - }} - checked={terminalNumbericLines} - /> - - - - - )} - - - - {strings("development")}}> - - - { - setErudaConsoleEnabled(e.target.checked); - }} - checked={erudaConsoleEnabled} - inputProps={{ - "aria-labelledby": "switch-list-label-eruda", - }} - /> - - { - os.open("https://github.com/DerGoogler/MMRL/issues", { - target: "_blank", - features: { - color: theme.palette.primary.main, - }, - }); - }} - > - - - { - os.shareText( - "Share via", - JSON.stringify( - { - device: { - sdk: Properties.get("ro.build.version.sdk", "unknown"), - brand: Properties.get("ro.product.brand", "unknown"), - model: Properties.get("ro.product.model", "unknown"), - }, - application: { - user_agent: navigator.userAgent, - package_name: BuildConfig.APPLICATION_ID, - version_name: BuildConfig.VERSION_NAME, - version_code: BuildConfig.VERSION_CODE, - }, - root: { - manager: Shell.getRootManager(), - version_name: Shell.VERSION_NAME(), - version_code: Shell.VERSION_CODE(), - }, - modconf: _modFS, - modules: localModules, - }, - null, - 4 - ) - ); - }} - > - - - - - - - {strings("storage")}}> - { - setRepos([]); - }} - > - - {" "} - {/* { - patchSettings(); - }} - > - - */} - - - - - {BuildConfig.DEBUG && ( - <> - Debug}> - { - throw new Error("Test error thrown!"); - }} - > - - - - - - - )} - - - - {BuildConfig.APPLICATION_ID} v{BuildConfig.VERSION_NAME} ({BuildConfig.VERSION_CODE})
- {os.isAndroid ? `${Shell.VERSION_NAME()} (${Shell.VERSION_CODE()})` : ""} - - } - /> -
-
-
- ); -} - -export default SettingsActivity; diff --git a/src/activitys/SubmitModuleActivity.tsx b/src/activitys/SubmitModuleActivity.tsx deleted file mode 100644 index a9c6b3f4..00000000 --- a/src/activitys/SubmitModuleActivity.tsx +++ /dev/null @@ -1,325 +0,0 @@ -import React from "react"; -import { TextField, Autocomplete, Chip, Stack, Avatar, Typography, Divider } from "@mui/material"; -import { Toolbar } from "@Components/onsenui/Toolbar"; -import { Page } from "@Components/onsenui/Page"; -import { useCategories } from "@Hooks/useCategories"; -import { useActivity } from "@Hooks/useActivity"; -import { useStrings } from "@Hooks/useStrings"; -import { useTheme } from "@Hooks/useTheme"; -import { MMRL } from "@Components/icons/MMRL"; -import { MRepo } from "@Components/icons/MRepo"; -import { useNativeFileStorage } from "@Hooks/useNativeFileStorage"; -import { licenseTypes } from "@Util/licenseTypes"; -import { CodeBlock } from "@Components/CodeBlock"; -import { useModFS } from "@Hooks/useModFS"; -import { path } from "@Util/path"; -import { en_antifeatures } from "./../locales/antifeatures/en"; - -interface FormTypesTrack { - id: string; - enable: boolean; - verified: boolean; - update_to: string; - source: string; - antifeatures: string[]; -} - -interface FormTypesRepo { - license: string; - support: string; - donate: string; - cover: string; - icon: string; - categories: string[]; - require: string[]; - screenshots: string[]; - readme: string; -} - -const INITIAL_TRACK_FORM: FormTypesTrack = { - id: "", - enable: true, - verified: false, - update_to: "", - source: "", - antifeatures: [], -}; - -const INITIAL_COM_REPO_FORM: FormTypesRepo = { - license: "", - support: "", - donate: "", - cover: "", - icon: "", - categories: [], - require: [], - screenshots: [], - readme: "", -}; - -const antifeatures = en_antifeatures.map((af) => af.id); - -const SupportedApp = React.memo<{ mmrl?: boolean; mrepo?: boolean }>((props) => { - return ( - - } - spacing={1} - > - {props.mmrl && ( - - MMRL - - )} - {props.mrepo && ( - - MRepo - - )} - - ); -}); - -const SubmitModuleActivity = () => { - const { allCategories } = useCategories(); - const { context } = useActivity(); - const { strings } = useStrings(); - const { theme } = useTheme(); - const { modFS } = useModFS(); - - const renderToolbar = () => { - return ( - - - - - {strings("submit_module")} - - ); - }; - const [trackFormData, setTrackFormData] = useNativeFileStorage( - path.resolve(modFS("MMRLFOL"), "submit-form-track.json"), - INITIAL_TRACK_FORM, - { - loader: "json", - } - ); - const [repoFormData, setRepoFormData] = useNativeFileStorage( - path.resolve(modFS("MMRLFOL"), "submit-form-repo.json"), - INITIAL_COM_REPO_FORM, - { - loader: "json", - } - ); - - const handleTrackChange = (e: React.ChangeEvent) => { - const { name, value } = e.target; - - setTrackFormData((prevState) => ({ ...prevState, [name]: value })); - }; - - const handleRepoChange = (e: React.ChangeEvent) => { - const { name, value } = e.target; - - setRepoFormData((prevState) => ({ ...prevState, [name]: value })); - }; - - const isInvalidLicense = React.useMemo(() => !licenseTypes.includes(repoFormData.license), [repoFormData.license]); - - return ( - - - - {JSON.stringify( - { - "track.json": trackFormData, - "common/repo.json": repoFormData, - }, - null, - 2 - )} - - - - track.json - - - - - - - - - { - setTrackFormData((prevState) => ({ ...prevState, antifeatures: value })); - }} - filterSelectedOptions - disableCloseOnSelect - renderInput={(params) => } - /> - - - common/repo.json - - - - - - - - - { - setRepoFormData((prevState) => ({ ...prevState, categories: value })); - }} - disableCloseOnSelect - filterSelectedOptions - renderInput={(params) => } - /> - - - - - - - - { - setRepoFormData((prevState) => ({ ...prevState, screenshots: value })); - }} - options={[]} - freeSolo - renderTags={(value, getTagProps) => ( - - {value.map((option, index) => ( - } {...getTagProps({ index })} /> - ))} - - )} - renderInput={(params) => } - /> - - { - setRepoFormData((prevState) => ({ ...prevState, require: value })); - }} - options={[]} - freeSolo - renderTags={(value, getTagProps) => ( - - {value.map((option, index) => ( - - ))} - - )} - renderInput={(params) => } - /> - - - - ); -}; - -export default SubmitModuleActivity; diff --git a/src/activitys/TestTerminalActivity.tsx b/src/activitys/TestTerminalActivity.tsx deleted file mode 100644 index f773e77f..00000000 --- a/src/activitys/TestTerminalActivity.tsx +++ /dev/null @@ -1,147 +0,0 @@ -import React from "react"; -import { Stack, Box, styled, Button } from "@mui/material"; -import { useActivity } from "@Hooks/useActivity"; -import { Toolbar } from "@Components/onsenui/Toolbar"; -import { Page } from "@Components/onsenui/Page"; -import { Ansi } from "@Components/Ansi"; -import { useConfirm } from "material-ui-confirm"; -import RestartAltIcon from "@mui/icons-material/RestartAlt"; -import { GestureDetector } from "@Components/onsenui/GestureDetector"; -import { useNativeStorage } from "@Hooks/useNativeStorage"; - -function TestTerminalActivity() { - const { context } = useActivity(); - const [lines, setLines] = React.useState([]); - const confirm = useConfirm(); - - const addText = (props: object) => { - setLines((lines) => [ - ...lines, - { - component: Ansi, - props: { - linkify: true, - sx: { - ml: 1, - mr: 1, - }, - ...props, - }, - }, - ]); - }; - - const addButton = (props: object) => { - setLines((lines) => [ - ...lines, - { - component: Button, - props: { - variant: "contained", - sx: { - width: "50vmin", - m: 1, - }, - ...props, - }, - }, - ]); - }; - - const renderToolbar = () => { - return ( - - - - - Test Terminal - - ); - }; - - const startLog = React.useMemo(() => { - addText({ children: "- \x1b[31mlolol" }); - addText({ children: "- \x1b[31mlolol" }); - addText({ children: "- \x1b[41mlolol" }); - addText({ children: "- https://github.com/DerGoogler/MMRL" }); - - addText({ children: " " }); - addText({ - children: - "\x1b[93mYou can press the \x1b[33;4mbutton\x1b[93;0m\x1b[93m below to \x1b[33;4mreboot\x1b[93;0m\x1b[93m your device\x1b[0m", - }); - addButton({ - children: "Reboot", - startIcon: , - onClick: () => { - confirm({ - title: "Reboot device?", - description: "Are you sure to reboot your device?", - confirmationText: "Yes", - }).then(() => { - console.log("REBOOT!!!"); - }); - }, - }); - - addText({ - children: - "\x1b[2mModules that causes issues after installing belog not to \x1b[35;4mMMRL\x1b[0;2m!\nPlease report these issues to thier support page\x1b[2m", - }); - addText({ - children: "Support for this module:", - }); - addText({ - children: "- \x1b[32mIssues: \x1b[33mhttps://github.com/Googlers-Repo/mmrl_install_tools/issues\x1b[0m", - }); - addText({ - children: "- \x1b[32mSource: \x1b[33mhttps://github.com/Googlers-Repo/mmrl_install_tools\x1b[0m", - }); - }, []); - - const [fontSize, setFontSize] = useNativeStorage("term_fodnt_size", 12); - - return ( - - { - setFontSize((init) => { - const newFontSize = init * (1 + (e.gesture.scale - 1) * 0.5); - return Math.min(Math.max(newFontSize, 12), 100); - }); - }} - sx={{ - display: "flex", - flexWrap: "wrap", - height: "100%", - }} - > - - {lines.map((line) => ( - - ))} - - - - ); -} - -export default TestTerminalActivity; diff --git a/src/activitys/UnverifiedHostActivity.tsx b/src/activitys/UnverifiedHostActivity.tsx deleted file mode 100644 index 38ab9aa3..00000000 --- a/src/activitys/UnverifiedHostActivity.tsx +++ /dev/null @@ -1,61 +0,0 @@ -import { Page } from "@Components/onsenui/Page"; -import { Toolbar } from "@Components/onsenui/Toolbar"; -import { useStrings } from "@Hooks/useStrings"; -import { useTheme } from "@Hooks/useTheme"; -import Card from "@mui/material/Card"; -import CardContent from "@mui/material/CardContent"; -import Typography from "@mui/material/Typography"; -import BugReportIcon from "@mui/icons-material/BugReport"; -import { Divider } from "@mui/material"; -import {Anchor} from "@Components/dapi/Anchor"; - -const UnverifiedHostActivity = () => { - const { strings } = useStrings(); - const { theme } = useTheme(); - - const renderToolbar = () => { - return ( - - {strings("unverified_host")} - - ); - }; - - return ( - - - - - - {strings("caution")}! - - - {strings("unverified_host_text", { - url: ( - - {location.host} - - ), - })} - - - - - - - {strings("unverified_host_text_help", { - issues: ( - - issues - - ), - })} - - - - - - ); -}; - -export default UnverifiedHostActivity; diff --git a/src/activitys/ViewBlacklistedModulesActivity.tsx b/src/activitys/ViewBlacklistedModulesActivity.tsx deleted file mode 100644 index 040ca9a3..00000000 --- a/src/activitys/ViewBlacklistedModulesActivity.tsx +++ /dev/null @@ -1,109 +0,0 @@ -import { ProgressCircular } from "react-onsenui"; -import AntiFeatureListItem from "@Components/AntiFeatureListItem"; -import { Anchor } from "@Components/dapi/Anchor"; -import { Page } from "@Components/onsenui/Page"; -import { Toolbar } from "@Components/onsenui/Toolbar"; -import { useActivity } from "@Hooks/useActivity"; -import { useBlacklist } from "@Hooks/useBlacklist"; -import { useStrings } from "@Hooks/useStrings"; -import { ExpandLess, ExpandMore } from "@mui/icons-material"; -import { Card, CardContent, Collapse, List, ListItem, ListItemIcon, ListItemText, Typography } from "@mui/material"; -import FlatList from "flatlist-react"; -import React from "react"; - -interface BlacklistItemProps { - module: any; -} - -function BlacklistItem({ module }: BlacklistItemProps) { - const [open, setOpen] = React.useState(false); - - const handleClick = () => { - setOpen((prev) => !prev); - }; - - return ( - <> - - {open ? : } - - - - - - - - {module.notes && ( - - - - - Additional notes - - {module.notes} - - - - )} - {typeof module.antifeatures === "string" ? ( - - ) : ( - Array.isArray(module.antifeatures) && module.antifeatures.map((anti) => ) - )} - - - - ); -} - -function ViewBlacklistedModulesActivity() { - const { context } = useActivity(); - const { strings } = useStrings(); - - const blacklistedModules = useBlacklist(); - - const renderToolbar = () => { - return ( - - - - - {strings("blacklisted_modules")} - - ); - }; - - if (blacklistedModules.length === 0) { - return ( - - - - ); - } - - return ( - - - - } - renderOnScroll - renderWhenEmpty={() => <>} - /> - - - - ); -} - -export default ViewBlacklistedModulesActivity; diff --git a/src/activitys/fragments/DrawerFragment.tsx b/src/activitys/fragments/DrawerFragment.tsx deleted file mode 100644 index fae5a272..00000000 --- a/src/activitys/fragments/DrawerFragment.tsx +++ /dev/null @@ -1,206 +0,0 @@ -import { FetchTextActivityExtra } from "@Activitys/FetchTextActivity"; -import { ModConfView } from "@Activitys/ModConfActivity/components/ModConfView"; -import PlaygroundsActivity, { PlaygroundExtra } from "@Activitys/PlaygroundsActivity"; -import { Markup } from "@Components/Markdown"; -import { IntentPusher } from "@Hooks/useActivity"; -import { useStrings } from "@Hooks/useStrings"; -import { useTheme } from "@Hooks/useTheme"; -import { Divider, List, ListItemButton, ListItemText, ListSubheader } from "@mui/material"; -import { os } from "@Native/Os"; -import { configureSample } from "@Util/configure-sample"; -import { dapiSample } from "@Util/dapi-sample"; -import { Page } from "react-onsenui"; -import { Activities } from ".."; - -type Props = { - renderToolbar: () => JSX.Element; - hideSplitter: () => void; - pushPage: (props: IntentPusher) => void; -}; - -export const DrawerFragment = (props: Props) => { - const hide = props.hideSplitter; - const pushPage = props.pushPage; - const { theme } = useTheme(); - const { strings } = useStrings(); - - return ( - - App}> - { - pushPage({ - component: Activities.Settings, - key: "settings", - }); - hide(); - }} - > - - - { - pushPage({ - component: Activities.Repo, - key: "repos", - }); - hide(); - }} - > - - - - { - pushPage({ - component: Activities.ModFS, - key: "ModFSActivity", - extra: {}, - }); - hide(); - }} - > - - - - { - pushPage({ - component: Activities.ViewBlacklistedModules, - key: "ViewBlacklistedModulesActivity", - extra: {}, - }); - hide(); - }} - > - - - - - - - Components}> - { - pushPage({ - noMemo: true, - component: Activities.SubmitModule, - key: "SubmitModuleActivity", - extra: {}, - }); - hide(); - }} - > - - - { - pushPage({ - component: PlaygroundsActivity, - key: "dapitestActivity", - extra: { - title: "DAPI Tester", - defaultText: dapiSample, - editorMode: "markdown", - previewPage: Activities.FetchText, - preview: Markup, - }, - }); - hide(); - }} - > - - - { - pushPage({ - component: Activities.ModConfPlayground, - key: "ModConfPlaygroundActivity", - extra: { - editorMode: "javascript", - defaultText: configureSample, - }, - }); - hide(); - }} - > - - {" "} - { - pushPage({ - component: Activities.ModConfStandalone, - key: "ModConfStandaloneActivity", - extra: {}, - }); - hide(); - }} - > - - - - - - - Other}> - { - os.openURL("https://dergoogler.com/legal/privacy-policy", "_blank"); - hide(); - }} - > - - - { - pushPage({ - component: Activities.About, - key: "abt", - extra: {}, - }); - hide(); - }} - > - - - { - pushPage({ - component: Activities.Licenses, - key: "license", - }); - hide(); - }} - > - - - { - pushPage({ - component: Activities.FetchText, - key: "changelog", - extra: { - rendering: ModConfView, - url: "https://raw.githubusercontent.com/wiki/DerGoogler/MMRL/JSX-Changelog.md", - modulename: "Changelog", - }, - }); - hide(); - }} - > - - - { - os.openURL("https://t.me/GooglersRepo", "_blank"); - hide(); - }} - > - - - - - {/* */} - - ); -}; diff --git a/src/activitys/fragments/ModuleFragment.tsx b/src/activitys/fragments/ModuleFragment.tsx deleted file mode 100644 index 06fa83f7..00000000 --- a/src/activitys/fragments/ModuleFragment.tsx +++ /dev/null @@ -1,138 +0,0 @@ -import { StyledMenu } from "@Components/DropdownButton"; -import { MissingInternet } from "@Components/MissingInternet"; -import { SearchBar } from "@Components/Searchbar"; -import { Page, RenderFunction } from "@Components/onsenui/Page"; -import { filters, useModuleFilter } from "@Hooks/useModulesFilter"; -import { useNetwork } from "@Hooks/useNetwork"; -import { useStrings } from "@Hooks/useStrings"; -import { useTheme } from "@Hooks/useTheme"; -import Box from "@mui/material/Box"; -import MenuItem from "@mui/material/MenuItem"; -import FlatList, { FlatListProps } from "flatlist-react"; -import { renderFunc } from "flatlist-react/lib/___subComponents/uiFunctions"; -import React from "react"; - -const RenderWhenEmpty = React.memo(() => { - const { theme } = useTheme(); - return ( - - No modules were found - - ); -}); - -export interface ModuleFragmentProps { - id: "explore" | "update" | "local"; - modules: Array; - group?: FlatListProps["group"]; - disableNoInternet?: boolean; - searchBy?: string[]; - renderItem: renderFunc; - renderFixed?: RenderFunction; -} - -const ModuleFragment = React.memo((props) => { - const { isNetworkAvailable } = useNetwork(); - const { theme } = useTheme(); - const { strings } = useStrings(); - const renderItem = React.useCallback>((m, k) => props.renderItem(m, k), []); - const [filter, _filter, setFilter] = useModuleFilter(`${props.id}_filter`); - const [search, setSearch] = React.useState(""); - - if (!isNetworkAvailable && !props.disableNoInternet) { - return ( - - - - ); - } - - const modules = React.useMemo(() => props.modules, [props.modules]); - - const [anchorEl, setAnchorEl] = React.useState(null); - const open = Boolean(anchorEl); - const handleClick = (event: React.MouseEvent) => { - setAnchorEl(event.currentTarget); - }; - - const findCurrentFilter = React.useMemo(() => filters.find((f) => f.value === _filter), [_filter]); - - return ( - - - { - setSearch(value); - console.log(value); - }} - filterIcon={findCurrentFilter?.icon} - onFilterIconClick={handleClick} - /> - { - setAnchorEl(null); - }} - > - {filters.map((fil) => { - if (fil.allowedIds.includes(props.id)) { - return ( - { - setFilter(fil.value); - setAnchorEl(null); - }} - disableRipple - > - - {fil.name} - - ); - } else { - return null; - } - })} - - - - } - sortBy={filter} - search={{ - term: search, - by: props.searchBy || ["id", "name", "author"], - //onEveryWord: true, - caseInsensitive: true, - }} - display={{ - row: true, - rowGap: "8px", - }} - group={props.group} - /> - - - - ); -}); - -export default ModuleFragment; diff --git a/src/activitys/index.ts b/src/activitys/index.ts deleted file mode 100644 index baa2c1cd..00000000 --- a/src/activitys/index.ts +++ /dev/null @@ -1,38 +0,0 @@ -import React from "react"; - -type Names = - | "Settings" - | "About" - | "Description" - | "FetchText" - | "Licenses" - | "Logcat" - | "ModConfPlayground" - | "ModConfStandalone" - | "ModFS" - | "PicturePreview" - | "SubmitModule" - | "ViewBlacklistedModules" - | "ModuleView" - | "Repo" - | "ModConf" - | "InstallTerminal"; - -export const Activities: Record> = { - Settings: React.lazy(() => import("./SettingsActivity")), - About: React.lazy(() => import("./AboutActivity")), - Description: React.lazy(() => import("./DescriptonActivity")), - FetchText: React.lazy(() => import("./FetchTextActivity")), - Licenses: React.lazy(() => import("./LicensesActivity")), - Logcat: React.lazy(() => import("./LogcatActivity")), - ModConfPlayground: React.lazy(() => import("./ModConfPlaygroundActivity")), - ModConfStandalone: React.lazy(() => import("./ModConfStandaloneActivity")), - ModFS: React.lazy(() => import("./ModFSActivity")), - PicturePreview: React.lazy(() => import("./PicturePreviewActivity")), - SubmitModule: React.lazy(() => import("./SubmitModuleActivity")), - ViewBlacklistedModules: React.lazy(() => import("./ViewBlacklistedModulesActivity")), - ModuleView: React.lazy(() => import("./ModuleViewActivity")), - Repo: React.lazy(() => import("./RepoActivity")), - ModConf: React.lazy(() => import("./ModConfActivity")), - InstallTerminal: React.lazy(() => import("./InstallTerminalV2Activity")), -}; diff --git a/src/components/Ansi.tsx b/src/components/Ansi.tsx deleted file mode 100644 index a1d1e966..00000000 --- a/src/components/Ansi.tsx +++ /dev/null @@ -1,192 +0,0 @@ -import Anser, { AnserJsonEntry } from "anser"; -import { escapeCarriageReturn } from "escape-carriage"; -import linkifyit from "linkify-it"; -import * as React from "react"; -import { Code } from "./dapi/Code"; -import { Anchor } from "./dapi/Anchor"; - -function ansiToJSON(input: string, use_classes: boolean = false): AnserJsonEntry[] { - input = escapeCarriageReturn(fixBackspace(input)); - return Anser.ansiToJson(input, { - json: true, - remove_empty: true, - use_classes, - }); -} - -function createClass(bundle: AnserJsonEntry): string | null { - let classNames: string = ""; - - if (bundle.bg) { - classNames += `${bundle.bg}-bg `; - } - if (bundle.fg) { - classNames += `${bundle.fg}-fg `; - } - if (bundle.decoration) { - classNames += `ansi-${bundle.decoration} `; - } - - if (classNames === "") { - return null; - } - - classNames = classNames.substring(0, classNames.length - 1); - return classNames; -} - -function createStyle(bundle: AnserJsonEntry): React.CSSProperties { - const style: React.CSSProperties = {}; - if (bundle.bg) { - style.backgroundColor = `rgb(${bundle.bg})`; - } - if (bundle.fg) { - style.color = `rgb(${bundle.fg})`; - } - switch (bundle.decoration) { - case "bold": - style.fontWeight = "bold"; - break; - case "dim": - style.opacity = "0.5"; - break; - case "italic": - style.fontStyle = "italic"; - break; - case "hidden": - style.visibility = "hidden"; - break; - case "strikethrough": - style.textDecoration = "line-through"; - break; - case "underline": - style.textDecoration = "underline"; - break; - case "blink": - style.textDecoration = "blink"; - break; - default: - break; - } - return style; -} - -function convertBundleIntoReact(linkify: boolean | "fuzzy", useClasses: boolean, bundle: AnserJsonEntry, key: number): JSX.Element { - const style = useClasses ? null : createStyle(bundle); - const className = useClasses ? createClass(bundle) : null; - - if (!linkify) { - return React.createElement("span", { style, key, className }, bundle.content); - } - - if (linkify === "fuzzy") { - return linkWithLinkify(bundle, key, style, className); - } - - return linkWithClassicMode(bundle, key, style, className); -} - -function linkWithClassicMode(bundle: AnserJsonEntry, key: number, style: React.CSSProperties | null, className: string | null) { - const content: React.ReactNode[] = []; - const linkRegex = /(\s|^)(https?:\/\/(?:www\.|(?!www))[^\s.]+\.[^\s]{2,}|www\.[^\s]+\.[^\s]{2,})/g; - - let index = 0; - let match: RegExpExecArray | null; - while ((match = linkRegex.exec(bundle.content)) !== null) { - const [, pre, url] = match; - - const startIndex = match.index + pre.length; - if (startIndex > index) { - content.push(bundle.content.substring(index, startIndex)); - } - - const href = url.startsWith("www.") ? `http://${url}` : url; - content.push( - React.createElement( - Anchor, - { - noIcon: true, - color: "unset", - key: index, - href, - target: "_blank", - }, - `${url}` - ) - ); - - index = linkRegex.lastIndex; - } - - if (index < bundle.content.length) { - content.push(bundle.content.substring(index)); - } - - return React.createElement("span", { style, key, className }, content); -} - -function linkWithLinkify(bundle: AnserJsonEntry, key: number, style: React.CSSProperties | null, className: string | null): JSX.Element { - const linker = linkifyit({ fuzzyEmail: false }).tlds(["io"], true); - - if (!linker.pretest(bundle.content)) { - return React.createElement("span", { style, key, className }, bundle.content); - } - - const matches = linker.match(bundle.content); - - if (!matches) { - return React.createElement("span", { style, key, className }, bundle.content); - } - - const content: React.ReactNode[] = [bundle.content.substring(0, matches[0]?.index)]; - - matches.forEach((match, i) => { - content.push( - React.createElement( - Anchor, - { - noIcon: true, - color: "unset", - href: match.url, - target: "_blank", - key: i, - }, - bundle.content.substring(match.index, match.lastIndex) - ) - ); - - if (matches[i + 1]) { - content.push(bundle.content.substring(matches[i].lastIndex, matches[i + 1]?.index)); - } - }); - - if (matches[matches.length - 1].lastIndex !== bundle.content.length) { - content.push(bundle.content.substring(matches[matches.length - 1].lastIndex, bundle.content.length)); - } - return React.createElement("span", { style, key, className }, content); -} - -export interface AnsiProps { - children?: string; - linkify?: boolean | "fuzzy"; - className?: string; - useClasses?: boolean; -} - -export function Ansi(props: AnsiProps): JSX.Element { - const { className, useClasses, children, linkify, ...rest } = props; - return React.createElement( - Code, - { className, ...rest }, - ansiToJSON(children ?? "", useClasses ?? false).map(convertBundleIntoReact.bind(null, linkify ?? false, useClasses ?? false)) - ); -} - -function fixBackspace(txt: string) { - let tmp = txt; - do { - txt = tmp; - tmp = txt.replace(/[^\n]\x08/gm, ""); - } while (tmp.length < txt.length); - return txt; -} diff --git a/src/components/AntiFeatureListItem.tsx b/src/components/AntiFeatureListItem.tsx deleted file mode 100644 index 6f9de23e..00000000 --- a/src/components/AntiFeatureListItem.tsx +++ /dev/null @@ -1,24 +0,0 @@ -import { useStrings } from "@Hooks/useStrings"; -import ListItem from "@mui/material/ListItem"; -import { en_antifeatures } from "./../locales/antifeatures/en"; -import React from "react"; -import { ListItemText, SxProps } from "@mui/material"; - -interface AntiFeatureListItemProps { - sx?: SxProps; - type: string; -} - -const AntiFeatureListItem = (props: AntiFeatureListItemProps) => { - const find = React.useMemo(() => en_antifeatures.find((anti) => anti.id === props.type), []); - - if (!find) return null; - - return ( - - - - ); -}; - -export default AntiFeatureListItem; diff --git a/src/components/AntifeaturesButton.tsx b/src/components/AntifeaturesButton.tsx deleted file mode 100644 index d0cb99d4..00000000 --- a/src/components/AntifeaturesButton.tsx +++ /dev/null @@ -1,66 +0,0 @@ -import { Chip, Dialog, DialogActions, DialogContent, DialogContentText, DialogTitle, List, SxProps } from "@mui/material"; -import Button from "@mui/material/Button"; -import WarningAmberIcon from "@mui/icons-material/WarningAmber"; -import React from "react"; -import { useStrings } from "@Hooks/useStrings"; -import AntiFeatureListItem from "./AntiFeatureListItem"; -import { GestureDetector } from "./onsenui/GestureDetector"; - -type Props = { - sx?: SxProps; - useChip?: boolean; - antifeatures?: Track["antifeatures"]; -}; - -export const AntifeatureButton = (props: Props) => { - const [open, setOpen] = React.useState(false); - - const { strings } = useStrings(); - - const handleClickOpen = (e: React.MouseEvent) => { - e.preventDefault(); - e.stopPropagation(); - setOpen(true); - }; - - const handleClose = (e: React.MouseEvent) => { - e.preventDefault(); - e.stopPropagation(); - setOpen(false); - }; - - return ( - <> - {props.useChip ? ( - } - label={strings("antifeatures")} - /> - ) : ( - // @ts-ignore - - )} - - {strings("antifeatures")} - - - {typeof props.antifeatures === "string" ? ( - - ) : ( - Array.isArray(props.antifeatures) && props.antifeatures.map((anti) => ) - )} - - - - - - - - ); -}; diff --git a/src/components/AvatarWithProgress.tsx b/src/components/AvatarWithProgress.tsx deleted file mode 100644 index 15a551cc..00000000 --- a/src/components/AvatarWithProgress.tsx +++ /dev/null @@ -1,75 +0,0 @@ -import { Avatar, Box, CircularProgress, SxProps, Typography, TypographyProps } from "@mui/material"; -import React from "react"; - -interface AvatarWithProgressProps extends React.PropsWithChildren { - value: number; - src?: string; - sx?: SxProps; - alt?: string; - progressTextVariant?: TypographyProps["variant"]; -} - -const AvatarWithProgress = (props: AvatarWithProgressProps) => { - const isActive = React.useMemo(() => props.value > 0, [props.value]); - - return ( - - - {isActive && ( - <> - ({ - position: "absolute", - top: 0, - left: 0, - right: 0, - bottom: 0, - backgroundColor: theme.palette.background.paper, - opacity: isActive ? 0.6 : 0, - zIndex: 0, - // @ts-ignore - borderRadius: props.sx.borderRadius, - })} - /> - - - {`${Math.round(props.value)}%`} - - )} - - ); -}; - -export { AvatarWithProgress }; diff --git a/src/components/CodeBlock.tsx b/src/components/CodeBlock.tsx deleted file mode 100644 index 4cead0fb..00000000 --- a/src/components/CodeBlock.tsx +++ /dev/null @@ -1,41 +0,0 @@ -import React from "react"; -import { StyledMarkdown } from "./Markdown/StyledMarkdown"; -import { Pre } from "./dapi/Pre"; -import { Code } from "./dapi/Code"; -import { SxProps } from "@mui/material"; -import hljs from "highlight.js"; - -interface CodeBlockProps { - lang?: string; - children?: string; - sx?: SxProps; -} - -const CodeBlock = React.forwardRef(({ lang, children, sx }, ref) => { - const _ref = React.useRef(null); - - const className = React.useMemo(() => `lang-${lang} hljs language-${lang}`, [lang]); - - if (typeof children !== "string") { - throw new TypeError("CodeBlock children aren't a string"); - } - - React.useEffect(() => { - if (_ref.current) { - _ref.current.querySelectorAll("pre code").forEach((block) => { - block.removeAttribute("data-highlighted"); - hljs.highlightElement(block); - }); - } - }, [children]); - - return ( - -
-        {children}
-      
-
- ); -}); - -export { CodeBlock, CodeBlockProps }; diff --git a/src/components/DialogEditTextListItem.tsx b/src/components/DialogEditTextListItem.tsx deleted file mode 100644 index 7223bb69..00000000 --- a/src/components/DialogEditTextListItem.tsx +++ /dev/null @@ -1,95 +0,0 @@ -import { useStrings } from "@Hooks/useStrings"; -import { useTheme } from "@Hooks/useTheme"; -import Button from "@mui/material/Button"; -import Dialog from "@mui/material/Dialog"; -import DialogActions from "@mui/material/DialogActions"; -import DialogContent from "@mui/material/DialogContent"; -import DialogContentText from "@mui/material/DialogContentText"; -import DialogTitle from "@mui/material/DialogTitle"; -import { InputBaseProps } from "@mui/material/InputBase"; -import ListItemButton from "@mui/material/ListItemButton"; -import TextField from "@mui/material/TextField"; -import React from "react"; - -export interface DialogEditTextListItemProps extends React.PropsWithChildren { - inputLabel: React.ReactNode; - title: React.ReactNode; - disabled?: boolean; - initialValue: string; - description?: React.ReactNode; - type?: React.HTMLInputTypeAttribute; - onSuccess: (value: string) => void; - InputProps?: Partial; - counter?: boolean; - helperText?: string; - maxLength?: number; - multiline?: boolean; - maxRows?: number; -} - -export const DialogEditTextListItem = (props: DialogEditTextListItemProps) => { - const { strings } = useStrings(); - - const [textInput, setTextInput] = React.useState(props.initialValue); - const [open, setOpen] = React.useState(false); - - const handleClickOpen = () => { - setOpen(true); - }; - - const handleClose = () => { - setOpen(false); - }; - - const handleRepoLinkChange = (event: React.ChangeEvent) => { - setTextInput(event.target.value); - }; - - return ( - <> - - {props.children} - - document.getElementById("ModConf-Container")} - open={open} - maxWidth="md" - fullWidth - onClose={handleClose} - > - {props.title} - - {props.description && {props.description}} - - - - - - - - - - ); -}; diff --git a/src/components/Disappear.tsx b/src/components/Disappear.tsx deleted file mode 100644 index e09ac5ba..00000000 --- a/src/components/Disappear.tsx +++ /dev/null @@ -1,45 +0,0 @@ -import { doc, Util } from "googlers-tools"; -import * as React from "react"; - -interface DisappearProps { - children: React.ReactNode; - style?: Util.Undefineable; - className?: string; - /** - * @return The current state of the disappear component - */ - onDisappear: (visible: boolean) => void; - /** - * Used to wrap the inner children - */ - wrapper: keyof JSX.IntrinsicElements; -} - -const Disappear = (props: DisappearProps) => { - const [isIntersecting, setIntersecting] = React.useState(false); - const ref = React.useRef(null); - - const observer = new IntersectionObserver(([entry]) => setIntersecting(entry.isIntersecting)); - - React.useEffect(() => { - doc.findRef(ref, (r) => { - observer.observe(r as any); - }); - return () => { - observer.disconnect(); - }; - }, []); - - const { children, className, style, wrapper } = props; - return React.createElement( - wrapper, - { - ref: ref, - className: className, - style: style, - }, - children - ); -}; - -export { Disappear, DisappearProps }; diff --git a/src/components/DropdownButton.tsx b/src/components/DropdownButton.tsx deleted file mode 100644 index 9a864855..00000000 --- a/src/components/DropdownButton.tsx +++ /dev/null @@ -1,121 +0,0 @@ -import { useNativeStorage } from "@Hooks/useNativeStorage"; -import ArrowDropDownIcon from "@mui/icons-material/ArrowDropDown"; -import { Box, Divider, SxProps } from "@mui/material"; -import Button from "@mui/material/Button"; -import ButtonGroup from "@mui/material/ButtonGroup"; -import Menu, { MenuProps } from "@mui/material/Menu"; -import MenuItem from "@mui/material/MenuItem"; -import { alpha, styled } from "@mui/material/styles"; -import * as React from "react"; - -interface DropdownButtonPropsOptions extends React.PropsWithChildren { - type?: "divider" | "item"; - disabled?: boolean; - onClick?: React.MouseEventHandler; -} - -interface DropdownButtonProps { - sx?: SxProps; - options: DropdownButtonPropsOptions[]; -} - -export const StyledMenu = styled((props: MenuProps) => ( - -))(({ theme }) => ({ - "& .MuiPaper-root": { - borderRadius: 6, - marginTop: theme.spacing(1), - minWidth: 180, - color: theme.palette.mode === "light" ? "rgb(55, 65, 81)" : theme.palette.grey[300], - boxShadow: - "rgb(255, 255, 255) 0px 0px 0px 0px, rgba(0, 0, 0, 0.05) 0px 0px 0px 1px, rgba(0, 0, 0, 0.1) 0px 10px 15px -3px, rgba(0, 0, 0, 0.05) 0px 4px 6px -2px", - "& .MuiMenu-list": { - padding: "4px 0", - }, - "& .MuiMenuItem-root": { - "& .MuiSvgIcon-root": { - fontSize: 18, - color: theme.palette.text.secondary, - marginRight: theme.spacing(1.5), - }, - "&:active": { - backgroundColor: alpha(theme.palette.primary.main, theme.palette.action.selectedOpacity), - }, - }, - }, -})); - -export const DropdownButton = (props: DropdownButtonProps) => { - const [open, setOpen] = React.useState(false); - const anchorRef = React.useRef(null); - const [selectedIndex, setSelectedIndex] = useNativeStorage("module_page_button_action", 0); - - const handleMenuItemClick = (event: React.MouseEvent, index: number) => { - setSelectedIndex(index); - setOpen(false); - }; - - const handleToggle = () => { - setOpen((prevOpen) => !prevOpen); - }; - - const handleClose = (event: Event) => { - if (anchorRef.current && anchorRef.current.contains(event.target as HTMLElement)) { - return; - } - - setOpen(false); - }; - - return ( - - - {/* @ts-ignore */} - - - - - {props.options.map((option, index) => { - switch (option.type) { - case "divider": - return ; - - default: - return ( - handleMenuItemClick(event, index)} - > - {option.children} - - ); - } - })} - - - ); -}; diff --git a/src/components/ErrorBoundary.tsx b/src/components/ErrorBoundary.tsx deleted file mode 100644 index 2b5d7aa8..00000000 --- a/src/components/ErrorBoundary.tsx +++ /dev/null @@ -1,59 +0,0 @@ -import React from "react"; - -export interface ErrorBoundaryProps extends React.PropsWithChildren { - fallback(error: Error, errorInfo: React.ErrorInfo, resetErrorBoundary: () => void): JSX.Element; -} - -export interface ErrorBoundaryState { - hasError: boolean; - error: Error; - errorInfo: React.ErrorInfo; -} - -export const errorBoundaryInitialState = { - hasError: false, - error: { - name: "string", - message: "string", - stack: "string", - }, - errorInfo: { - /** - * Captures which component contained the exception, and its ancestors. - */ - componentStack: "string", - }, -}; - -export class ErrorBoundary extends React.Component { - public constructor(props: ErrorBoundaryProps) { - super(props); - this.resetErrorBoundary = this.resetErrorBoundary.bind(this); - this.state = errorBoundaryInitialState; - } - - public static getDerivedStateFromError(error: any) { - return { hasError: true }; - } - - public componentDidCatch(error: Error, errorInfo: React.ErrorInfo) { - this.setState({ error, errorInfo }); - } - - public resetErrorBoundary() { - const { error } = this.state; - - if (error !== null) { - this.setState(errorBoundaryInitialState); - } - } - - public render() { - if (this.state.hasError) { - // You can render any custom fallback UI - return this.props.fallback(this.state.error, this.state.errorInfo, this.resetErrorBoundary); - } - - return this.props.children; - } -} diff --git a/src/components/For.tsx b/src/components/For.tsx deleted file mode 100644 index b1a23f10..00000000 --- a/src/components/For.tsx +++ /dev/null @@ -1,34 +0,0 @@ -import React from "react"; - -export type ForProps = { - each: readonly T[]; - fallback: () => JSX.Element; - catch: (e: Error | undefined) => JSX.Element; - render: (item: T, index: number) => U; - renderTop?: () => JSX.Element; -}; - -export function For(props: ForProps) { - const handler = () => { - try { - if (props.each.length !== 0) { - return props.each.map(props.render); - } else { - return props.fallback(); - } - } catch (e) { - if (e instanceof Error) { - return props.catch(e); - } else { - return props.catch(undefined); - } - } - }; - - return ( - - {props.each.length !== 0 && props.renderTop && props.renderTop()} - {handler()} - - ); -} diff --git a/src/components/Gesture.tsx b/src/components/Gesture.tsx deleted file mode 100644 index 457f9a40..00000000 --- a/src/components/Gesture.tsx +++ /dev/null @@ -1,43 +0,0 @@ -import { doc } from "googlers-tools"; -import React from "react"; - -interface Props { - event: - | "drag" - | "dragleft" - | "dragright" - | "dragup" - | "dragdown" - | "hold" - | "release" - | "swipe" - | "swipeleft" - | "swiperight" - | "swipeup" - | "swipedown" - | "tap" - | "doubletap" - | "touch" - | "transform" - | "pinch" - | "pinchin" - | "pinchout" - | "rotate"; - callback(...props: any): void; - children: React.ReactNode; -} - -const Gesture = (props: Props) => { - const { callback, event, children } = props; - const gerstureID = React.useRef(null); - - React.useEffect(() => { - doc.findRef(gerstureID, (ref: HTMLDivElement) => { - ref.addEventListener(event, callback); - }); - }, []); - - return
{children}
; -}; - -export default Gesture; diff --git a/src/components/Icon.tsx b/src/components/Icon.tsx deleted file mode 100644 index fda8a8a6..00000000 --- a/src/components/Icon.tsx +++ /dev/null @@ -1,30 +0,0 @@ -import { OverridableComponent } from "@mui/material/OverridableComponent"; -import { SvgIconProps, SvgIconTypeMap } from "@mui/material/SvgIcon"; - -interface IProps extends SvgIconProps { - icon: OverridableComponent; - /** - * Keeps the icons in light colors even if it's dark mode on - */ - keepLight?: boolean; - - [key: string]: any; -} - -/** - * An icon wrapper for Material React icons - */ -const Icon = (props: IProps) => { - const { keepLight, icon: WarpperIcon, ...rest } = props; - return ( - - ); -}; - -export default Icon; diff --git a/src/components/ListPickerItem.tsx b/src/components/ListPickerItem.tsx deleted file mode 100644 index f24bf243..00000000 --- a/src/components/ListPickerItem.tsx +++ /dev/null @@ -1,135 +0,0 @@ -import Button from "@mui/material/Button"; -import DialogTitle from "@mui/material/DialogTitle"; -import DialogContent from "@mui/material/DialogContent"; -import DialogActions from "@mui/material/DialogActions"; -import Dialog from "@mui/material/Dialog"; -import RadioGroup from "@mui/material/RadioGroup"; -import Radio from "@mui/material/Radio"; -import FormControlLabel from "@mui/material/FormControlLabel"; -import React from "react"; - -import { ListItemButton, ListItemText } from "@mui/material"; -import { useSettings } from "@Hooks/useSettings"; - -type ContentMap = { - name: string; - value: string; -}; - -interface PickerItemProps { - id: string; - disabled?: boolean; - contentMap: ContentMap[]; - targetSetting: "language" | "term_scroll_behavior"; - title: React.ReactNode; -} - -/** - * Remembers! The first item in the array will be the default. - * @param props - * @returns - */ -export function ListPickerItem(props: PickerItemProps) { - const [open, setOpen] = React.useState(false); - - const [setting, setSetting] = useSettings(props.targetSetting); - - const handleOpen = () => { - setOpen(true); - }; - - const handleClose = (val: any) => { - setOpen(false); - - if (val.name && val.value) { - setSetting(val); - } - }; - - return ( - <> - - - - - - ); -} - -export interface ConfirmationDialogRawProps { - id: string; - keepMounted: boolean; - title: React.ReactNode; - value: ContentMap; - open: boolean; - contentMap: ContentMap[]; - onClose: (val: ContentMap | null) => void; -} - -export function ConfirmationDialogRaw(props: ConfirmationDialogRawProps) { - const { onClose, value: valueProp, open, ...other } = props; - const [value, setValue] = React.useState(valueProp); - const radioGroupRef = React.useRef(null); - - React.useEffect(() => { - if (!open) { - setValue(valueProp); - } - }, [valueProp, open]); - - const handleEntering = () => { - if (radioGroupRef.current != null) { - radioGroupRef.current.focus(); - } - }; - - const handleCancel = () => { - onClose(null); - }; - - const handleOk = () => { - onClose(value); - }; - - const handleChange = (event: React.ChangeEvent) => { - setValue(JSON.parse((event.target as HTMLInputElement).value)); - }; - - return ( - - {props.title} - - - {props.contentMap.map((option) => ( - } - label={option.name} - /> - ))} - - - - - - - - ); -} diff --git a/src/components/Markdown/StyledMarkdown.tsx b/src/components/Markdown/StyledMarkdown.tsx deleted file mode 100644 index 4c092996..00000000 --- a/src/components/Markdown/StyledMarkdown.tsx +++ /dev/null @@ -1,563 +0,0 @@ -import { Theme, styled } from "@mui/material"; -import React from "react"; -import { useTheme } from "@Hooks/useTheme"; - -interface Props { - children?: React.ReactNode; - style?: React.CSSProperties; -} - -export const StyledMarkdown = styled("article")(() => { - const { theme } = useTheme(); - - return { - msTextSizeAdjust: "100%", - WebkitTextSizeAdjust: "100%", - margin: "0", - color: theme.palette.text.primary, - // backgroundColor: "#ffffff", - fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif, "Apple Color Emoji",\n "Segoe UI Emoji"', - fontSize: "16px", - lineHeight: 1.5, - wordWrap: "break-word", - ".octicon": { - display: "inline-block", - fill: "currentColor", - verticalAlign: "text-bottom", - overflow: "visible !important", - }, - "h1:hover .anchor .octicon-link:before,\n h2:hover .anchor .octicon-link:before,\n h3:hover .anchor .octicon-link:before,\n h4:hover .anchor .octicon-link:before,\n h5:hover .anchor .octicon-link:before,\n h6:hover .anchor .octicon-link:before": - { - width: "16px", - height: "16px", - content: '" "', - display: "inline-block", - backgroundColor: "currentColor", - WebkitMaskImage: - "url(\"data:image/svg+xml,\")", - maskImage: - "url(\"data:image/svg+xml,\")", - }, - "details,\n figcaption,\n figure": { display: "block" }, - summary: { display: "list-item" }, - "[hidden]": { display: "none !important" }, - a: { - backgroundColor: "transparent", - color: "#4a148c", - textDecoration: "none", - "&:active,\n &:hover": { outlineWidth: "0" }, - }, - "abbr[title]": { borderBottom: "none", textDecoration: "underline dotted" }, - "b,\n strong": { fontWeight: 600 }, - dfn: { fontStyle: "italic" }, - h1: { - margin: "0.67em 0", - fontWeight: 600, - paddingBottom: "0.3em", - fontSize: "2em", - borderBottom: `thin solid ${theme.palette.divider}`, - "tt,\n code": { padding: "0 0.2em", fontSize: "inherit" }, - }, - mark: { backgroundColor: "#fff8c5", color: "#24292f" }, - small: { fontSize: "90%" }, - "sub,\n sup": { - fontSize: "75%", - lineHeight: 0, - position: "relative", - verticalAlign: "baseline", - }, - sub: { bottom: "-0.25em" }, - sup: { top: "-0.5em" }, - img: { - borderStyle: "none", - maxWidth: "100%", - boxSizing: "content-box", - '&[align="right"]': { paddingLeft: "20px" }, - '&[align="left"]': { paddingRight: "20px" }, - }, - "code,\n kbd,\n pre,\n samp": { - fontFamily: "monospace, monospace", - fontSize: "1em", - }, - figure: { margin: "1em 40px" }, - hr: { - boxSizing: "content-box", - overflow: "hidden", - background: "transparent", - borderBottom: `thin solid ${theme.palette.divider}`, - height: "0.25em", - padding: "0", - margin: "24px 0", - backgroundColor: theme.palette.divider, - border: "0", - "&::before": { display: "table", content: '""' }, - "&::after": { display: "table", clear: "both", content: '""' }, - }, - input: { - font: "inherit", - margin: "0", - overflow: "visible", - fontFamily: "inherit", - fontSize: "inherit", - lineHeight: "inherit", - "&::-webkit-outer-spin-button,\n &::-webkit-inner-spin-button": { - margin: "0", - WebkitAppearance: "none", - appearance: "none", - }, - }, - '[type="button"],\n [type="reset"],\n [type="submit"]': { - WebkitAppearance: "button", - }, - '[type="button"]::-moz-focus-inner,\n [type="reset"]::-moz-focus-inner,\n [type="submit"]::-moz-focus-inner': { - borderStyle: "none", - padding: "0", - }, - '[type="button"]:-moz-focusring,\n [type="reset"]:-moz-focusring,\n [type="submit"]:-moz-focusring': { - outline: "1px dotted ButtonText", - }, - '[type="checkbox"],\n [type="radio"]': { - boxSizing: "border-box", - padding: "0", - }, - '[type="number"]': { - "&::-webkit-inner-spin-button,\n &::-webkit-outer-spin-button": { - height: "auto", - }, - }, - '[type="search"]': { - WebkitAppearance: "textfield", - outlineOffset: "-2px", - "&::-webkit-search-cancel-button,\n &::-webkit-search-decoration": { - WebkitAppearance: "none", - }, - }, - "::-webkit-input-placeholder": { color: "inherit", opacity: 0.54 }, - "::-webkit-file-upload-button": { - WebkitAppearance: "button", - font: "inherit", - }, - "a:hover": { textDecoration: "underline" }, - table: { - borderSpacing: "0", - borderCollapse: "collapse", - display: "block", - width: "max-content", - maxWidth: "100%", - overflow: "auto", - th: { fontWeight: 600, padding: "6px 13px", border: `1px solid ${theme.palette.divider}` }, - td: { padding: "6px 13px", border: `1px solid ${theme.palette.divider}` }, - tr: { - backgroundColor: theme.palette.background.default, - borderTop: `thin solid ${theme.palette.divider}`, - "&:nth-child(2n)": { - backgroundColor: theme.palette.background.paper, - }, - }, - img: { backgroundColor: "transparent" }, - }, - "td,\n th": { padding: "0" }, - details: { - summary: { cursor: "pointer" }, - "&:not([open]) > *:not(summary)": { display: "none !important" }, - }, - kbd: { - display: "inline-block", - padding: "3px 5px", - font: "11px ui-monospace, SFMono-Regular, SF Mono, Menlo, Consolas, Liberation Mono, monospace", - lineHeight: "10px", - color: "#24292f", - verticalAlign: "middle", - backgroundColor: "#f6f8fa", - border: "solid 1px rgba(175, 184, 193, 0.2)", - borderBottomColor: "rgba(175, 184, 193, 0.2)", - borderRadius: "6px", - boxShadow: "inset 0 -1px 0 rgba(175, 184, 193, 0.2)", - }, - "h1,\n h2,\n h3,\n h4,\n h5,\n h6": { - marginTop: "24px", - marginBottom: "16px", - fontWeight: 600, - lineHeight: 1.25, - }, - h2: { - fontWeight: 600, - paddingBottom: "0.3em", - fontSize: "1.5em", - borderBottom: `thin solid ${theme.palette.divider}`, - "tt,\n code": { padding: "0 0.2em", fontSize: "inherit" }, - }, - h3: { - fontWeight: 600, - fontSize: "1.25em", - "tt,\n code": { padding: "0 0.2em", fontSize: "inherit" }, - }, - h4: { - fontWeight: 600, - fontSize: "1em", - "tt,\n code": { padding: "0 0.2em", fontSize: "inherit" }, - }, - h5: { - fontWeight: 600, - fontSize: "0.875em", - "tt,\n code": { padding: "0 0.2em", fontSize: "inherit" }, - }, - h6: { - fontWeight: 600, - fontSize: "0.85em", - color: "#57606a", - "tt,\n code": { padding: "0 0.2em", fontSize: "inherit" }, - }, - p: { marginTop: "0", marginBottom: "10px" }, - blockquote: { - margin: "0", - padding: "0 1em", - color: theme.palette.text.secondary, - borderLeft: `5px solid ${theme.palette.text.secondary}`, - }, - ul: { - marginTop: "0", - marginBottom: "0", - paddingLeft: "2em", - ol: { listStyleType: "lower-roman" }, - "ul ol,\n ol ol": { listStyleType: "lower-alpha" }, - "ul,\n ol": { marginTop: "0", marginBottom: "0" }, - }, - ol: { - marginTop: "0", - marginBottom: "0", - paddingLeft: "2em", - ol: { listStyleType: "lower-roman" }, - "ul ol,\n ol ol": { listStyleType: "lower-alpha" }, - "&.no-list": { padding: "0", listStyleType: "none" }, - '&[type="1"]': { listStyleType: "decimal" }, - '&[type="a"]': { listStyleType: "lower-alpha" }, - '&[type="i"]': { listStyleType: "lower-roman" }, - "ol,\n ul": { marginTop: "0", marginBottom: "0" }, - }, - dd: { marginLeft: "0" }, - "tt,\n code": { - fontFamily: "ui-monospace, SFMono-Regular, SF Mono, Menlo, Consolas, Liberation Mono, monospace", - fontSize: "12px", - }, - pre: { - marginTop: "0", - marginBottom: "0", - fontFamily: "ui-monospace, SFMono-Regular, SF Mono, Menlo, Consolas, Liberation Mono, monospace", - fontSize: "85%", - wordWrap: "normal", - code: { fontSize: "100%" }, - "> code": { - padding: "0", - margin: "0", - wordBreak: "normal", - whiteSpace: "pre", - background: "transparent", - border: "0", - }, - padding: "16px", - overflow: "auto", - lineHeight: 1.45, - color: theme.palette.text.primary, - backgroundColor: theme.palette.background.paper, - borderRadius: theme.shape.borderRadius, - "code,\n tt": { - display: "inline", - maxWidth: "auto", - padding: "0", - margin: "0", - overflow: "visible", - lineHeight: "inherit", - wordWrap: "normal", - backgroundColor: "transparent", - border: "0", - }, - }, - "::placeholder": { color: "#6e7781", opacity: 1 }, - ".pl-c": { color: "#6e7781" }, - ".pl-c1,\n .pl-s .pl-v": { color: "#0550ae" }, - ".pl-e,\n .pl-en": { color: "#8250df" }, - ".pl-smi,\n .pl-s .pl-s1": { color: "#24292f" }, - ".pl-ent": { color: "#116329" }, - ".pl-k": { color: "#cf222e" }, - ".pl-s,\n .pl-pds,\n .pl-s .pl-pse .pl-s1": { color: "#0a3069" }, - ".pl-sr": { - color: "#0a3069", - ".pl-cce,\n .pl-sre,\n .pl-sra": { color: "#0a3069" }, - }, - ".pl-v,\n .pl-smw": { color: "#953800" }, - ".pl-bu": { color: "#82071e" }, - ".pl-ii": { color: "#f6f8fa", backgroundColor: "#82071e" }, - ".pl-c2": { color: "#f6f8fa", backgroundColor: "#cf222e" }, - ".pl-sr .pl-cce": { fontWeight: "bold", color: "#116329" }, - ".pl-ml": { color: "#3b2300" }, - ".pl-mh": { - fontWeight: "bold", - color: "#0550ae", - ".pl-en": { fontWeight: "bold", color: "#0550ae" }, - }, - ".pl-ms": { fontWeight: "bold", color: "#0550ae" }, - ".pl-mi": { fontStyle: "italic", color: "#24292f" }, - ".pl-mb": { fontWeight: "bold", color: "#24292f" }, - ".pl-md": { color: "#82071e", backgroundColor: "#ffebe9" }, - ".pl-mi1": { color: "#116329", backgroundColor: "#dafbe1" }, - ".pl-mc": { color: "#953800", backgroundColor: "#ffd8b5" }, - ".pl-mi2": { color: "#eaeef2", backgroundColor: "#0550ae" }, - ".pl-mdr": { fontWeight: "bold", color: "#8250df" }, - ".pl-ba": { color: "#57606a" }, - ".pl-sg": { color: "#8c959f" }, - ".pl-corl": { textDecoration: "underline", color: "#0a3069" }, - "[data-catalyst]": { display: "block" }, - "g-emoji": { - fontFamily: '"Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"', - fontSize: "1em", - fontStyle: "normal !important", - fontWeight: 400, - lineHeight: 1, - verticalAlign: "-0.075em", - img: { width: "1em", height: "1em" }, - }, - "&::before": { display: "table", content: '""' }, - "&::after": { display: "table", clear: "both", content: '""' }, - "> *": { - "&:first-of-type": { marginTop: "0 !important" }, - "&:last-child": { marginBottom: "0 !important" }, - }, - "a:not([href])": { color: "inherit", textDecoration: "none" }, - ".absent": { color: "#cf222e" }, - ".anchor": { - cssFloat: "left", - paddingRight: "4px", - marginLeft: "-20px", - lineHeight: 1, - "&:focus": { outline: "none" }, - }, - "p,\n blockquote,\n ul,\n ol,\n dl,\n table,\n pre,\n details": { - marginTop: "0", - marginBottom: "16px", - }, - "blockquote >": { - ":first-of-type": { marginTop: "0" }, - ":last-child": { marginBottom: "0" }, - }, - "sup > a": { - "&::before": { content: '"["' }, - "&::after": { content: '"]"' }, - }, - "h1 .octicon-link,\n h2 .octicon-link,\n h3 .octicon-link,\n h4 .octicon-link,\n h5 .octicon-link,\n h6 .octicon-link": { - color: "#24292f", - verticalAlign: "middle", - visibility: "hidden", - }, - "h1:hover .anchor,\n h2:hover .anchor,\n h3:hover .anchor,\n h4:hover .anchor,\n h5:hover .anchor,\n h6:hover .anchor": { - textDecoration: "none", - }, - "h1:hover .anchor .octicon-link,\n h2:hover .anchor .octicon-link,\n h3:hover .anchor .octicon-link,\n h4:hover .anchor .octicon-link,\n h5:hover .anchor .octicon-link,\n h6:hover .anchor .octicon-link": - { - visibility: "visible", - }, - "ul.no-list": { padding: "0", listStyleType: "none" }, - "div > ol:not([type])": { listStyleType: "decimal" }, - li: { "> p": { marginTop: "16px" }, "+ li": { marginTop: "0.25em" } }, - dl: { - padding: "0", - dt: { - padding: "0", - marginTop: "16px", - fontSize: "1em", - fontStyle: "italic", - fontWeight: 600, - }, - dd: { padding: "0 16px", marginBottom: "16px" }, - }, - ".emoji": { - maxWidth: "none", - verticalAlign: "text-top", - backgroundColor: "transparent", - }, - span: { - "&.frame": { - display: "block", - overflow: "hidden", - "> span": { - display: "block", - cssFloat: "left", - width: "auto", - padding: "7px", - margin: "13px 0 0", - overflow: "hidden", - border: "1px solid #d0d7de", - }, - span: { - img: { display: "block", cssFloat: "left" }, - span: { - display: "block", - padding: "5px 0 0", - clear: "both", - color: "#24292f", - }, - }, - }, - "&.align-center": { - display: "block", - overflow: "hidden", - clear: "both", - "> span": { - display: "block", - margin: "13px auto 0", - overflow: "hidden", - textAlign: "center", - }, - "span img": { margin: "0 auto", textAlign: "center" }, - }, - "&.align-right": { - display: "block", - overflow: "hidden", - clear: "both", - "> span": { - display: "block", - margin: "13px 0 0", - overflow: "hidden", - textAlign: "right", - }, - "span img": { margin: "0", textAlign: "right" }, - }, - "&.float-left": { - display: "block", - cssFloat: "left", - marginRight: "13px", - overflow: "hidden", - span: { margin: "13px 0 0" }, - }, - "&.float-right": { - display: "block", - cssFloat: "right", - marginLeft: "13px", - overflow: "hidden", - "> span": { - display: "block", - margin: "13px auto 0", - overflow: "hidden", - textAlign: "right", - }, - }, - }, - "code,\n tt": { - padding: "0.2em 0.4em", - margin: "0", - fontSize: "85%", - color: theme.palette.text.primary, - backgroundColor: theme.palette.background.paper, - borderRadius: "6px", - }, - "code br,\n tt br": { display: "none" }, - "del code": { textDecoration: "inherit" }, - ".highlight": { - marginBottom: "16px", - pre: { - marginBottom: "0", - wordBreak: "normal", - padding: "16px", - overflow: "auto", - fontSize: "85%", - lineHeight: 1.45, - color: theme.palette.text.primary, - backgroundColor: theme.palette.background.paper, - borderRadius: "6px", - }, - }, - ".csv-data": { - "td,\n th": { - padding: "5px", - overflow: "hidden", - fontSize: "12px", - lineHeight: 1, - textAlign: "left", - whiteSpace: "nowrap", - }, - ".blob-num": { - padding: "10px 8px 9px", - textAlign: "right", - background: "#ffffff", - border: "0", - }, - tr: { borderTop: "0" }, - th: { fontWeight: 600, background: "#f6f8fa", borderTop: "0" }, - }, - ".footnotes": { - fontSize: "12px", - color: "#57606a", - borderTop: "1px solid #d0d7de", - ol: { paddingLeft: "16px" }, - li: { - position: "relative", - "&:target": { - "&::before": { - position: "absolute", - top: "-8px", - right: "-8px", - bottom: "-8px", - left: "-24px", - pointerEvents: "none", - content: '""', - border: "2px solid #0969da", - borderRadius: "6px", - }, - color: "#24292f", - }, - }, - ".data-footnote-backref g-emoji": { fontFamily: "monospace" }, - }, - ".task-list-item": { - listStyleType: "none", - label: { fontWeight: 400 }, - "&.enabled label": { cursor: "pointer" }, - "+ .task-list-item": { marginTop: "3px" }, - ".handle": { display: "none" }, - }, - ".task-list-item-checkbox": { - margin: "0 0.2em 0.25em -1.6em", - verticalAlign: "middle", - }, - ".contains-task-list:dir(rtl) .task-list-item-checkbox": { - margin: "0 -1.6em 0.25em 0.2em", - }, - "::-webkit-calendar-picker-indicator": { filter: "invert(50%)" }, - // Highlight.js - ".hljs": { - display: "block", - overflowX: "auto", - padding: "0.5em", - color: theme.palette.text.primary, - backgroundColor: theme.palette.background.paper, - }, - ".hljs-comment,\n.hljs-punctuation": { color: "#768390" }, - ".hljs-attr,\n.hljs-attribute,\n.hljs-meta,\n.hljs-selector-attr,\n.hljs-selector-class,\n.hljs-selector-id": { - color: "#6cb6ff", - }, - ".hljs-variable,\n.hljs-literal,\n.hljs-number,\n.hljs-doctag": { - color: "#f69d50", - }, - ".hljs-params": { color: "#cdd9e5" }, - ".hljs-function": { color: "#dcbdfb" }, - ".hljs-class,\n.hljs-tag,\n.hljs-title,\n.hljs-built_in": { - color: "#8ddb8c", - }, - ".hljs-keyword,\n.hljs-type,\n.hljs-builtin-name,\n.hljs-meta-keyword,\n.hljs-template-tag,\n.hljs-template-variable": { - color: "#f47067", - }, - ".hljs-string,\n.hljs-undefined": { color: "#96d0ff" }, - ".hljs-regexp": { color: "#96d0ff" }, - ".hljs-symbol": { color: "#6cb6ff" }, - ".hljs-bullet": { color: "#f69d50" }, - ".hljs-section": { color: "#6cb6ff", fontWeight: "bold" }, - ".hljs-quote,\n.hljs-name,\n.hljs-selector-tag,\n.hljs-selector-pseudo": { - color: "#8ddb8c", - }, - ".hljs-emphasis": { color: "#f69d50", fontStyle: "italic" }, - ".hljs-strong": { color: "#f69d50", fontWeight: "bold" }, - ".hljs-deletion": { color: "#ff938a", backgroundColor: "#78191b" }, - ".hljs-addition": { color: "#8ddb8c", backgroundColor: "#113417" }, - ".hljs-link": { color: "#96d0ff", fontStyle: "underline" }, - }; -}); diff --git a/src/components/Markdown/index.tsx b/src/components/Markdown/index.tsx deleted file mode 100644 index dcbe2672..00000000 --- a/src/components/Markdown/index.tsx +++ /dev/null @@ -1,243 +0,0 @@ -import Markdown, { MarkdownToJSX, RuleType, compiler } from "markdown-to-jsx"; -import { Video } from "@Components/dapi/Video"; -import React from "react"; -import { Alert, AlertTitle, Divider, Grid, Paper, Stack, SxProps, Theme } from "@mui/material"; -import styled from "@emotion/styled"; -import hljs from "highlight.js"; -import { Image } from "@Components/dapi/Image"; -import { StyledMarkdown } from "./StyledMarkdown"; -import { DiscordWidget } from "@Components/dapi/DiscordWidget"; -import { AlertIcon, BugIcon, CheckIcon, IssueClosedIcon, IssueOpenedIcon, IssueReopenedIcon, XIcon } from "@primer/octicons-react"; -import { Code } from "@Components/dapi/Code"; -import { Pre } from "@Components/dapi/Pre"; -import { Anchor } from "@Components/dapi/Anchor"; -import { useFetch } from "@Hooks/useFetch"; - -export type AlertType = { - title: string; - render: (content: React.ReactNode) => JSX.Element; -}; -const sx = { - mb: 2, -}; -export const admonitionTypes = { - "[!NOTE]": { - title: "Note", - render: (content: string) => ( - - Note - {content} - - ), - }, - "[!TIP]": { - title: "Tip", - render: (content: string) => ( - - Tip - {content} - - ), - }, - "[!IMPORTANT]": { - title: "Important", - render: (content: string) => ( - - Important - {content} - - ), - }, - "[!WARNING]": { - title: "Warning", - render: (content: string) => ( - - Warning - {content} - - ), - }, - "[!CAUTION]": { - title: "Caution", - render: (content: string) => ( - - Caution - {content} - - ), - }, -}; - -type Props = { - fetch?: string; - children?: string; - sx?: SxProps; - styleMd?: React.CSSProperties; -}; - -const StyledDivider = styled(Divider)({ - "h1, & h2, & h3, & h4, & h5, & h6": { - border: "none", - }, -}); - -export const MarkdownOverrides: MarkdownToJSX.Overrides | undefined = { - // Icons - BugIcon: { - component: BugIcon, - }, - IssueOpenedIcon: { - component: IssueOpenedIcon, - }, - IssueClosedIcon: { - component: IssueClosedIcon, - }, - IssueReopenedIcon: { - component: IssueReopenedIcon, - }, - CheckIcon: { - component: CheckIcon, - }, - XIcon: { - component: XIcon, - }, - AlertIcon: { - component: AlertIcon, - }, - alert: { - component: (props) => { - return ( - - {props.children} - - ); - }, - }, - img: { - component: Image, - }, - video: { - component: Video, - }, - divider: { - component: StyledDivider, - }, - paper: { - component: Paper, - }, - stack: { - component: Stack, - }, - code: { - component: Code, - }, - pre: { - component: Pre, - }, - discordwidget: { - component: DiscordWidget, - }, - grid: { - component: Grid, - }, -}; - -export const Markup = (props: Props) => { - const ref = React.useRef(null); - - const [fetchedText] = useFetch(props.fetch, { type: "text" }); - const text = fetchedText || props.children; - - React.useEffect(() => { - if (ref.current) { - ref.current.querySelectorAll("pre code").forEach((block) => { - block.removeAttribute("data-highlighted"); - hljs.highlightElement(block); - }); - } - }, []); - - return ( - - p.text).join(""); - - let title: string; - let admonitionType: AlertType | null = null; - // A link break after the title is explicitly required by GitHub - const titleEnd = text.indexOf("\n"); - if (titleEnd < 0) { - // But if the following one is a block, the newline would be trimmed by the upstream. - // To start a new block, a newline is required. - // So we just need to addtionally check if the following one is a block. - // The legacy title variant is not affected since it checks an inline and does not care about the newline. - - // Considering the reason why the paragraph ends here, the following one should be a children of the blockquote, which means it is always a block. - // So no more check is required. - title = text; - admonitionType = admonitionTypes[title]; - - if (!admonitionType) { - return next(); - } - - // No addtional inlines can exist in this paragraph for the title... - if (paragraph.children.length > 1) { - // Unless it is an inline break, which can be transformed to from 2 spaces with a newline. - if (paragraph.children.at(1)?.type == RuleType.breakLine) { - // When it is, we actually have already found the line break required by GitHub. - // So we just strip the additional `
` element. - // The title element will be removed later. - paragraph.children.splice(1, 1); - } else { - return next(); - } - } - // strip the title - paragraph.children.shift(); - } else { - const textBody = text.substring(titleEnd + 1); - title = text.substring(0, titleEnd); - // Handle whitespaces after the title. - // Whitespace characters are defined by GFM. - const m = /[ \t\v\f\r]+$/.exec(title); - if (m) { - title = title.substring(0, title.length - m[0].length); - } - - admonitionType = admonitionTypes[title]; - if (!admonitionType) return next(); - // Update the text body to remove the title - text = textBody; - } - - return admonitionType.render(text); - }, - }} - children={text || ""} - /> -
- ); -}; - -export const MarkUpCompile = (children: string) => { - return compiler(children, { overrides: MarkdownOverrides }); -}; diff --git a/src/components/MissingInternet.tsx b/src/components/MissingInternet.tsx deleted file mode 100644 index fbb9f0df..00000000 --- a/src/components/MissingInternet.tsx +++ /dev/null @@ -1,30 +0,0 @@ -import Stack from "@mui/material/Stack"; -import WifiOffOutlinedIcon from "@mui/icons-material/WifiOffOutlined"; -import Box from "@mui/material/Box"; -import Icon from "./Icon"; - -export const MissingInternet = () => { - return ( - ({ - color: theme.palette.secondary.dark, - width: "100%", - height: "100%", - m: "unset", - })} - direction="column" - justifyContent="center" - alignItems="center" - spacing={2} - > - - Please check your internet connection - - ); -}; diff --git a/src/components/ModulesQueue.tsx b/src/components/ModulesQueue.tsx deleted file mode 100644 index d9c36279..00000000 --- a/src/components/ModulesQueue.tsx +++ /dev/null @@ -1,165 +0,0 @@ -import { Activities } from "@Activitys/index"; -import { TerminalActivityExtra } from "@Activitys/InstallTerminalV2Activity"; -import { ActivityContext } from "@Hooks/useActivity"; -import { useFormatBytes } from "@Hooks/useFormatBytes"; -import { useStrings } from "@Hooks/useStrings"; -import CloseIcon from "@mui/icons-material/Close"; -import { Box, Button, Drawer, IconButton, List, ListItem, ListItemText, Stack, Typography } from "@mui/material"; -import { os } from "@Native/Os"; -import { Shell } from "@Native/Shell"; -import { view } from "@Native/View"; -import { useConfirm } from "material-ui-confirm"; -import React from "react"; - -interface ModulesQueueContext { - addModule: (queue: Queue) => void; - removeModule: (index: any) => void; - toggleQueueView: () => void; - isQueueOpen: boolean; -} -const ModulesQueueContext = React.createContext({ - addModule(queue) {}, - removeModule(index) {}, - toggleQueueView() {}, - isQueueOpen: false, -}); - -interface Queue { - name: string; - url: string; - size?: number; -} - -interface ModulesQueueProps extends React.PropsWithChildren { - context: ActivityContext; -} - -const QueueItem = ({ module, onClick }: any) => { - const [moduleFileSize, moduleFileSizeByteText] = useFormatBytes(module.size); - const { strings } = useStrings(); - - return ( - - - - - ); -}; - -export const ModulesQueue = (props: ModulesQueueProps) => { - const [queue, setQueue] = React.useState([]); - const [open, setOpen] = React.useState(false); - const { strings } = useStrings(); - const confirm = useConfirm(); - const { context } = props; - - const addModule = (queue: Queue) => { - setQueue((qu) => { - if (qu.some((g) => g.url === queue.url)) { - os.toast(strings("alr_add_queue") as string, Toast.LENGTH_SHORT); - return qu; - } - - os.toast(strings("add_t_queue") as string, Toast.LENGTH_SHORT); - return [...qu, queue]; - }); - }; - - const removeModule = (index: number) => { - setQueue(queue.filter((_, i) => i !== index)); - }; - - const toggleDrawer = () => { - setOpen(!open); - }; - - const value = React.useMemo( - () => ({ - addModule, - removeModule, - toggleQueueView: toggleDrawer, - isQueueOpen: open, - }), - [addModule, removeModule, toggleDrawer, open] - ); - - const isQueueNotEmpty = React.useMemo(() => queue.length !== 0, [queue]); - - return ( - - {props.children} - - - - - - {strings("install_queue")} - - {strings("install_queue_notice")} - - - - - - - - - - {queue.length !== 0 ? ( - queue.map((module, index) => removeModule(index)} />) - ) : ( - <> - {strings("install_queue_empty")} - - )} - - - - - - - - ); -}; - -export const useModuleQueue = () => React.useContext(ModulesQueueContext); diff --git a/src/components/Searchbar.tsx b/src/components/Searchbar.tsx deleted file mode 100644 index f747433b..00000000 --- a/src/components/Searchbar.tsx +++ /dev/null @@ -1,73 +0,0 @@ -import { Divider, IconButton, Paper, InputBase, SxProps } from "@mui/material"; -import { Search, Clear, FilterList } from "@mui/icons-material"; -import React from "react"; -import { OverridableComponent } from "@mui/material/OverridableComponent"; -import { SvgIconTypeMap } from "@mui/material/SvgIcon"; - -interface SearchBarProps { - placeholder?: string; - filterIcon?: OverridableComponent; - onFilterIconClick?: (event: React.MouseEvent) => void; - onSearch: (value: string) => void; - sx?: SxProps; -} - -const SearchBar = React.forwardRef((props, ref) => { - const [searchTerm, setSearchTerm] = React.useState(""); - const isNotEmpty = React.useMemo(() => searchTerm.trim().length !== 0, [searchTerm]); - - const handleInputChange = (event: React.ChangeEvent) => { - setSearchTerm(event.target.value); - }; - - const handleClear = React.useCallback(() => { - setSearchTerm(""); - if (props.onSearch) { - props.onSearch(""); - } - }, [searchTerm]); - - const handleKeyPress = (event: React.KeyboardEvent) => { - if (event.key === "Enter") { - handleSearch(); - } - }; - - const handleSearch = React.useCallback(() => { - if (props.onSearch && isNotEmpty) { - props.onSearch(searchTerm); - } - }, [searchTerm, isNotEmpty]); - - return ( - - {props.onFilterIconClick && ( - - {React.createElement(props.filterIcon || FilterList)} - - )} - - - - - - {isNotEmpty && ( - <> - - - - - - )} - - ); -}); - -export { SearchBar, SearchBarProps }; diff --git a/src/components/dapi/Anchor.tsx b/src/components/dapi/Anchor.tsx deleted file mode 100644 index eba0154e..00000000 --- a/src/components/dapi/Anchor.tsx +++ /dev/null @@ -1,183 +0,0 @@ -import { useTheme } from "@Hooks/useTheme"; -import { Box, Stack, Tooltip, Typography, createSvgIcon } from "@mui/material"; -import { os } from "@Native/Os"; -import { useRepos } from "@Hooks/useRepos"; -import React from "react"; -import { useConfirm } from "material-ui-confirm"; -import { createRegexURL } from "@Util/createRegexURL"; - -import { - VolunteerActivism, - LaunchRounded, - Extension, - GitHub, - Telegram, - YouTube, - X, - Facebook, - Instagram, - Email, - LocalPhone, -} from "@mui/icons-material"; -import { useStrings } from "@Hooks/useStrings"; -import { useActivity } from "@Hooks/useActivity"; -import { Activities } from "@Activitys/index"; -import { useSettings } from "@Hooks/useSettings"; -import { GestureDetector } from "@Components/onsenui/GestureDetector"; -import { Xda } from "@Components/icons/Xda"; -import { CodeBlock } from "@Components/CodeBlock"; - -type AnchorProps = React.JSX.IntrinsicElements["a"] & { - noIcon?: boolean; - icon?: typeof Xda; - module?: string; -}; - -function useIcon(link) { - if (createRegexURL("github", "com").test(link)) { - return GitHub; - } else if (createRegexURL(["xdaforums", "forum.xda-developers"], "com").test(link)) { - return Xda; - } else if (createRegexURL("(\\/[w-]+\\.)?t", "me").test(link)) { - return Telegram; - } else if (createRegexURL("paypal", ["me", "com"]).test(link)) { - return VolunteerActivism; - } else if (createRegexURL(["youtube", "youtu"], ["com", "be"]).test(link)) { - return YouTube; - } else if (createRegexURL(["x", "twitter"], "com").test(link)) { - return X; - } else if (createRegexURL("facebook", "com").test(link)) { - return Facebook; - } else if (createRegexURL(["instagram", "ig"], ["com", "me"]).test(link)) { - return Instagram; - } else if (/mailto:[\w-]+/i.test(link)) { - return Email; - } else if (/tel:\+?[\d-]+/i.test(link)) { - return LocalPhone; - } else { - return LaunchRounded; - } -} - -function increase_brightness(hex, percent) { - // strip the leading # if it's there - hex = hex.replace(/^\s*#|\s*$/g, ""); - - // convert 3 char codes --> 6, e.g. `E0F` --> `EE00FF` - if (hex.length == 3) { - hex = hex.replace(/(.)/g, "$1$1"); - } - - var r = parseInt(hex.substr(0, 2), 16), - g = parseInt(hex.substr(2, 2), 16), - b = parseInt(hex.substr(4, 2), 16); - - return ( - "#" + - (0 | ((1 << 8) + r + ((256 - r) * percent) / 100)).toString(16).substr(1) + - (0 | ((1 << 8) + g + ((256 - g) * percent) / 100)).toString(16).substr(1) + - (0 | ((1 << 8) + b + ((256 - b) * percent) / 100)).toString(16).substr(1) - ); -} - -const Anchor: React.FC = (props) => { - const confirm = useConfirm(); - const { theme } = useTheme(); - const { context } = useActivity(); - const { strings } = useStrings(); - const { href, children, noIcon, module, color = theme.palette.text.link, target = os.WindowMMRLOwn } = props; - - const { modules } = useRepos(); - const findModule = React.useMemo(() => modules.find((m) => m.id === module), [module]); - const icon = !props.icon ? useIcon(href) : props.icon; - - const [linkProtection] = useSettings("link_protection"); - - const s = React.useMemo( - () => ({ - display: "inline-block", - "& a[href]": { - cursor: "pointer", - color: color, - textDecoration: "none", - display: "flex", - alignItems: "center", - fontWeight: "unset", - ":hover": { - background: `linear-gradient(${color}, ${color}) 0 100% / 0.1em 0.1em repeat-x`, - }, - "& code": { - color: increase_brightness(color, 75.09), - backgroundColor: `${color}4d`, - }, - }, - }), - [color] - ); - - const __href = React.useMemo(() => (!(module && findModule) ? href : module), [href]); - - const openLink = React.useCallback(() => { - os.openURL(__href, target, `color=${theme.palette.background.default}`); - }, [__href]); - - const handleLinkClick = React.useCallback( - (e: React.MouseEvent) => { - e.stopPropagation(); - e.preventDefault(); - if (__href && module && findModule) { - context.pushPage({ - component: Activities.ModuleView, - key: "ModuleViewActivity", - extra: findModule, - }); - } else { - if (linkProtection) { - confirm({ - title: strings("anchor_confirm_title"), - description: strings("anchor_confirm_desc", { - codeblock: ( - - {__href} - - ), - }), - confirmationText: strings("yes"), - }).then(() => openLink()); - } else { - openLink(); - } - } - }, - [__href] - ); - - return ( - - - - {children} - - {!noIcon && ( - - )} - - - ); -}; - -export { Anchor }; diff --git a/src/components/dapi/Code.tsx b/src/components/dapi/Code.tsx deleted file mode 100644 index 155f95e1..00000000 --- a/src/components/dapi/Code.tsx +++ /dev/null @@ -1,40 +0,0 @@ -import { SxProps } from "@mui/material"; -import Box from "@mui/material/Box"; -import React, { useRef } from "react"; - -export type CodeProps = React.JSX.IntrinsicElements["code"] & { - sx?: SxProps; -}; - -const Code = React.forwardRef((props, _ref) => { - const ref = (_ref as React.MutableRefObject) || useRef(null); - - React.useEffect(() => { - if (ref.current) { - ref.current.addEventListener( - "contextmenu", - function (event) { - event.returnValue = true; - if (typeof event.stopPropagation === "function") { - event.stopPropagation(); - } - if (typeof event.cancelBubble === "function") { - (event as any).cancelBubble(); - } - }, - true - ); - } - }, []); - - return ( - - ); -}); - -export { Code }; diff --git a/src/components/dapi/DiscordWidget.tsx b/src/components/dapi/DiscordWidget.tsx deleted file mode 100644 index 144e87e3..00000000 --- a/src/components/dapi/DiscordWidget.tsx +++ /dev/null @@ -1,35 +0,0 @@ -import { util } from "googlers-tools"; - -type Theme = "light" | "dark"; - -interface DiscordWidgetProps { - token: string | number; - width?: string | number | undefined; - height?: string | number | undefined; - theme?: Theme | undefined; -} - -const DiscordWidget = (props: DiscordWidgetProps) => { - const { token, width, height, theme } = props; - - return ( - <> - - - ); -}; - -export { DiscordWidget }; diff --git a/src/components/dapi/Image.tsx b/src/components/dapi/Image.tsx deleted file mode 100644 index 28d55e25..00000000 --- a/src/components/dapi/Image.tsx +++ /dev/null @@ -1,68 +0,0 @@ -import { Activities } from "@Activitys/index"; -import { useActivity } from "@Hooks/useActivity"; -import { ModFS, useModFS } from "@Hooks/useModFS"; -import { useTheme } from "@Hooks/useTheme"; -import { SuFile } from "@Native/SuFile"; -import { formatString } from "@Util/stringFormat"; -import { Box, BoxProps } from "@mui/material"; -import React from "react"; - -type Props = BoxProps<"img", JSX.IntrinsicElements["img"]> & { - type?: string; - shadow?: string; - title?: string; - caption?: string; - noOutline?: boolean; - noOpen?: boolean; - blur?: number; - modFSAdds?: Partial; -}; - -function Image(props: Props) { - const { theme } = useTheme(); - const { _modFS } = useModFS(); - const { context } = useActivity(); - const { type = "image/png", src, shadow, noOpen, sx, blur, modFSAdds, noOutline, ...rest } = props; - - const newSrc = React.useMemo(() => { - if (src) { - const file = new SuFile(formatString(src, Object.assign(_modFS, modFSAdds))); - if (file.exist()) { - return `data:${type};base64,${file.readAsBase64()}`; - } else { - return src; - } - } - }, [src]); - - return ( - { - if (!noOpen) { - context.pushPage({ - component: Activities.PicturePreview, - key: "PicturePreviewActivity", - extra: { - picture: newSrc, - }, - }); - } - }} - {...rest} - /> - ); -} - -export { Image }; - diff --git a/src/components/dapi/Pre.tsx b/src/components/dapi/Pre.tsx deleted file mode 100644 index ca3027fd..00000000 --- a/src/components/dapi/Pre.tsx +++ /dev/null @@ -1,40 +0,0 @@ -import { SxProps } from "@mui/material"; -import Box from "@mui/material/Box"; -import React, { useRef } from "react"; - -export type PreProps = React.JSX.IntrinsicElements["pre"] & { - sx?: SxProps; -}; - -const Pre = React.forwardRef((props, _ref) => { - const ref = (_ref as React.MutableRefObject) || useRef(null); - - React.useEffect(() => { - if (ref.current) { - ref.current.addEventListener( - "contextmenu", - function (event) { - event.returnValue = true; - if (typeof event.stopPropagation === "function") { - event.stopPropagation(); - } - if (typeof event.cancelBubble === "function") { - (event as any).cancelBubble(); - } - }, - true - ); - } - }, []); - - return ( - - ); -}); - -export { Pre }; diff --git a/src/components/dapi/Video.tsx b/src/components/dapi/Video.tsx deleted file mode 100644 index 8e9810d1..00000000 --- a/src/components/dapi/Video.tsx +++ /dev/null @@ -1,54 +0,0 @@ -import { util } from "googlers-tools"; -import { CSSProperties } from "react"; -import { isDesktop } from "react-device-detect"; - -declare type Type = `video/${string}`; - -interface VideoProps { - src: string; - type: Type; - controls?: boolean; - poster?: string; - noSupportText?: string; - style?: CSSProperties | undefined; -} - -interface State {} - -type E = HTMLVideoElement | HTMLIFrameElement; - -const Video = (props: VideoProps) => { - const { src, type, controls, noSupportText, style, poster } = props; - const Style = { - width: "100%", - height: isDesktop ? "445px" : "181.500px", - padding: "0px", - margin: "0px", - }; - - switch (type) { - case "video/youtube": - return ( - - ); - - default: - return ( - - ); - } -}; - -export { Video }; diff --git a/src/components/icons/MMRL.tsx b/src/components/icons/MMRL.tsx deleted file mode 100644 index 7c110ece..00000000 --- a/src/components/icons/MMRL.tsx +++ /dev/null @@ -1,5 +0,0 @@ -import CodeIcon from "@mui/icons-material/Code"; - -const MMRL = CodeIcon; - -export { MMRL }; diff --git a/src/components/icons/MRepo.tsx b/src/components/icons/MRepo.tsx deleted file mode 100644 index 53da178b..00000000 --- a/src/components/icons/MRepo.tsx +++ /dev/null @@ -1,23 +0,0 @@ -import { createSvgIcon } from "@mui/material"; - -const MRepo = createSvgIcon( - - - - - - - - , - "MRepo" -); - -export { MRepo }; diff --git a/src/components/icons/VerifiedIcon.tsx b/src/components/icons/VerifiedIcon.tsx deleted file mode 100644 index 141be96c..00000000 --- a/src/components/icons/VerifiedIcon.tsx +++ /dev/null @@ -1,43 +0,0 @@ -import { useStrings } from "@Hooks/useStrings"; -import { useTheme } from "@Hooks/useTheme"; -import { Tooltip, SvgIcon } from "@mui/material"; -import { useId } from "react"; - -interface VerifiedIconProps { - isVerified?: boolean; -} - -const VerifiedIcon = (props: VerifiedIconProps) => { - const { isVerified } = props; - - const { strings } = useStrings(); - const { theme } = useTheme(); - - const verifiedId = useId(); - - if (isVerified) { - return ( - - - - - - - - - - - - - - ); - } else { - return null; - } -}; - -export { VerifiedIcon }; diff --git a/src/components/icons/Xda.tsx b/src/components/icons/Xda.tsx deleted file mode 100644 index 8380209e..00000000 --- a/src/components/icons/Xda.tsx +++ /dev/null @@ -1,10 +0,0 @@ -import { createSvgIcon } from "@mui/material"; - -const Xda = createSvgIcon( - - - , - "Xda" -); - -export { Xda }; diff --git a/src/components/module/DeviceModule.tsx b/src/components/module/DeviceModule.tsx deleted file mode 100644 index c3e2523c..00000000 --- a/src/components/module/DeviceModule.tsx +++ /dev/null @@ -1,266 +0,0 @@ -import React from "react"; -import Typography from "@mui/material/Typography"; -import Card from "@mui/material/Card"; -import Stack from "@mui/material/Stack"; -import Chip from "@mui/material/Chip"; -import Divider from "@mui/material/Divider"; -import Button from "@mui/material/Button"; - -import { ModConfActivityExtra } from "@Activitys/ModConfActivity"; - -import { Delete, Settings, RefreshRounded, Loop } from "@mui/icons-material"; - -import { useTheme } from "@Hooks/useTheme"; -import { useSettings } from "@Hooks/useSettings"; -import { useLowQualityModule } from "@Hooks/useLowQualityModule"; -import { useStrings } from "@Hooks/useStrings"; -import { useActivity } from "@Hooks/useActivity"; -import { useLog } from "@Hooks/native/useLog"; -import { ModFS, useModFS } from "@Hooks/useModFS"; - -import { SuFile } from "@Native/SuFile"; -// @ts-ignore -import { useConfirm } from "material-ui-confirm"; -import Switch from "@mui/material/Switch"; -import { Image } from "@Components/dapi/Image"; -import { AntifeatureButton } from "@Components/AntifeaturesButton"; -import { useBlacklist } from "@Hooks/useBlacklist"; -import { Activities } from "@Activitys/index"; - -interface Props { - module: Module; -} - -const DeviceModule = React.memo((props) => { - const { theme } = useTheme(); - const { modFS } = useModFS(); - const { strings } = useStrings(); - const { context } = useActivity(); - - const log = useLog("DeviceModule"); - const confirm = useConfirm(); - - const { id, name, author, version, versionCode, timestamp, description, cover } = props.module; - - const format = React.useCallback<(key: K) => ModFS[K]>((key) => modFS(key, { MODID: id }), []); - - const remove = new SuFile(format("REMOVE")); - const disable = new SuFile(format("DISABLE")); - const has_update = SuFile.exist(format("UPDATE")); - - const [isEnabled, setIsEnabled] = React.useState(!disable.exist()); - const [isSwitchDisabled, setIsSwitchDisabled] = React.useState(remove.exist()); - - const [lowQualityModule] = useSettings("_low_quality_module"); - const isLowQuality = useLowQualityModule(props.module, !lowQualityModule); - const isNew = React.useMemo(() => new Date().getTime() - timestamp! < 60 * 60 * 1000, [timestamp]); - const isDisabledStyle = React.useMemo(() => (isSwitchDisabled ? { textDecoration: "line-through" } : {}), [isSwitchDisabled]); - - const post_service = SuFile.exist(format("POSTSERVICE")); - const late_service = SuFile.exist(format("LATESERVICE")); - const post_mount = SuFile.exist(format("POSTMOUNT")); - const boot_complete = SuFile.exist(format("BOOTCOMP")); - const module_config_file = SuFile.exist(format("CONFINDEX")); - - const blacklistedModules = useBlacklist(); - const findHardCodedAntifeature = React.useMemo(() => { - return blacklistedModules.find((mod) => mod.id === id)?.antifeatures || []; - }, [id]); - - return ( - - - {cover && ( - {name} - )} - - - {isLowQuality && ( - { - confirm({ title: strings("low_quality_module"), description: strings("low_quality_module_warn") }).then(() => {}); - }} - label={{strings("low_quality_module")}} - size="small" - sx={{ borderRadius: theme.shape.borderRadius / theme.shape.borderRadius }} - /> - )} - {isNew && ( - {strings("new")}} - size="small" - sx={{ borderRadius: theme.shape.borderRadius / theme.shape.borderRadius }} - /> - )} - - - - - {name} - - - {version} ({versionCode}) / {author} - - - - { - const checked = e.target.checked; - - if (checked) { - if (disable.exist()) { - if (disable.delete()) { - log.d( - strings("module_enabled_LOG", { - name: id, - }) as string - ); - } - } - } else { - if (!disable.exist()) { - if (disable.create()) { - log.d( - strings("module_disabled_LOG", { - name: id, - }) as string - ); - } - } - } - setIsEnabled(checked); - }} - sx={{ - right: -8, - }} - /> - - - - {description} - - - - {post_service && } - {late_service && } - {post_mount && } - {boot_complete && } - - - - {findHardCodedAntifeature && findHardCodedAntifeature.length !== 0 && ( - - )} - - - {isSwitchDisabled ? ( - - ) : ( - - )} - - - - - {has_update && ( - - )} - - ); -}); - -export default DeviceModule; diff --git a/src/components/module/ExploreModule.tsx b/src/components/module/ExploreModule.tsx deleted file mode 100644 index a4e2a81b..00000000 --- a/src/components/module/ExploreModule.tsx +++ /dev/null @@ -1,147 +0,0 @@ -import { Activities } from "@Activitys/index"; -import { AntifeatureButton } from "@Components/AntifeaturesButton"; -import { Image } from "@Components/dapi/Image"; -import { VerifiedIcon } from "@Components/icons/VerifiedIcon"; -import { useActivity } from "@Hooks/useActivity"; -import { useBlacklist } from "@Hooks/useBlacklist"; -import { useFormatDate } from "@Hooks/useFormatDate"; -import { useModFS } from "@Hooks/useModFS"; -import { useModuleInfo } from "@Hooks/useModuleInfo"; -import { useStrings } from "@Hooks/useStrings"; -import { useSupportedRoot } from "@Hooks/useSupportedRoot"; -import { useTheme } from "@Hooks/useTheme"; -import { CalendarMonth, PersonOutline, Source, Tag } from "@mui/icons-material"; -import Card from "@mui/material/Card"; -import Chip from "@mui/material/Chip"; -import Stack from "@mui/material/Stack"; -import Typography from "@mui/material/Typography"; -import { SuFile } from "@Native/SuFile"; -import React from "react"; - -interface Props { - module: Module; -} - -const ExploreModule = React.memo((props) => { - const { id, name, author, description, track, timestamp, version, versions, versionCode, features, __mmrl_repo_source } = props.module; - const { cover, verified, root } = useModuleInfo(props.module); - const [isModuleSupported] = useSupportedRoot(root, []); - - const { context } = useActivity(); - const { strings } = useStrings(); - const { theme } = useTheme(); - const { modFS } = useModFS(); - - const formatLastUpdate = useFormatDate(timestamp ? timestamp : versions[versions.length - 1].timestamp); - - const blacklistedModules = useBlacklist(); - const findHardCodedAntifeature = React.useMemo(() => { - return [...(track.antifeatures || []), ...(blacklistedModules.find((mod) => mod.id === id)?.antifeatures || [])]; - }, [id, track.antifeatures]); - - const handleOpenModule = () => { - context.pushPage({ - component: Activities.ModuleView, - key: "ModuleViewActivity", - extra: props.module, - }); - }; - - return ( - - {cover && ( - {name} - )} - - - - - - {name} - - - - - {author} - - - - - - - {SuFile.exist(modFS("PROPS", { MODID: id })) && ( - - )} - - {features && Object.keys(features).length !== 0 && ( - - )} - - {!isModuleSupported && } - - {findHardCodedAntifeature && findHardCodedAntifeature.length !== 0 && ( - - )} - - - - {description} - - - - - - - {strings("last_updated", { date: formatLastUpdate })} - - - - {__mmrl_repo_source && __mmrl_repo_source.join(", ")} - - - - {versionCode} - - - - - ); -}); - -export default ExploreModule; diff --git a/src/components/module/UpdateModule.tsx b/src/components/module/UpdateModule.tsx deleted file mode 100644 index 35ff7853..00000000 --- a/src/components/module/UpdateModule.tsx +++ /dev/null @@ -1,143 +0,0 @@ -import React from "react"; - -import { ArrowRightAlt } from "@mui/icons-material"; - -import Button from "@mui/material/Button"; -import ButtonGroup from "@mui/material/ButtonGroup"; -import Card from "@mui/material/Card"; -import Chip from "@mui/material/Chip"; -import Divider from "@mui/material/Divider"; -import Stack from "@mui/material/Stack"; -import Typography from "@mui/material/Typography"; - -import { useLog } from "@Hooks/native/useLog"; -import { useActivity } from "@Hooks/useActivity"; -import { useRepos } from "@Hooks/useRepos"; -import { useStrings } from "@Hooks/useStrings"; -import { useTheme } from "@Hooks/useTheme"; - - -import { Activities } from "@Activitys/index"; -import { link } from "googlers-tools"; - -interface Props { - module: Module; -} - -const UpdateModule = React.memo((props) => { - const { theme } = useTheme(); - const { strings } = useStrings(); - const { context } = useActivity(); - const { modules, repos } = useRepos(); - - const { id, name, author, version, versionCode, updateJson: __updateJson } = props.module; - - const log = useLog("UpdateModule"); - - const [updateJson, setUpdateJson] = React.useState(null); - - if (__updateJson && link.validURL(__updateJson)) { - React.useEffect(() => { - fetch(__updateJson) - .then((res) => res.json()) - .then((json: UpdateJson) => setUpdateJson(json)); - }, [repos]); - } else { - log.d(strings("dm_update_json_fetch_warn", { id: id }) as string); - } - - const hasUpdate = React.useMemo(() => { - const onlineModule = modules.find((module) => module.id === id); - if (__updateJson && updateJson) { - return versionCode < Number(updateJson.versionCode); - } else { - return onlineModule && versionCode < onlineModule.versionCode; - } - }, [updateJson, modules, repos]); - - const updatedModule = React.useMemo(() => { - const onlineModule = modules.find((module) => module.id === id); - if (__updateJson && updateJson) { - return updateJson; - } else { - return onlineModule && onlineModule.versions[onlineModule.versions.length - 1]; - } - }, [updateJson, modules, repos]); - - if (!hasUpdate) return null; - - return ( - - - - {name} - - {author} - - - - - - - Version: - - - - - - - - - Version code: - - - - - - - - - - - - - - - - - ); -}); - -export default UpdateModule; diff --git a/src/components/onsenui/BottomToolbar.tsx b/src/components/onsenui/BottomToolbar.tsx deleted file mode 100644 index 512fc51e..00000000 --- a/src/components/onsenui/BottomToolbar.tsx +++ /dev/null @@ -1,10 +0,0 @@ -import onsCustomElement from "@Util/onsCustomElement"; -import "onsenui/esm/elements/ons-bottom-toolbar"; - -interface HTMLBottomToolbar { - modifier?: string; -} - -const BottomToolbar = onsCustomElement("ons-bottom-toolbar")({}); - -export { HTMLBottomToolbar, BottomToolbar }; diff --git a/src/components/onsenui/Carousel.tsx b/src/components/onsenui/Carousel.tsx deleted file mode 100644 index 83416b97..00000000 --- a/src/components/onsenui/Carousel.tsx +++ /dev/null @@ -1,51 +0,0 @@ -import onsCustomElement from "@Util/onsCustomElement"; -import { SxProps } from "@mui/material"; -import React from "react"; - -const deprecated = { - index: "activeIndex", -}; - -const Element = onsCustomElement("ons-carousel", { deprecated })({}); - -const Carousel = React.forwardRef((props, ref) => { - const { itemWidth, itemHeight, ...rest } = props; - - // string values for itemWidth and itemHeight are deprecated but handle them - // safely anyway to avoid breaking user code - const stringify = (x) => (typeof x === "number" ? `${x}px` : x); - const realItemWidth = stringify(itemWidth); - const realItemHeight = stringify(itemHeight); - - return ( - - {props.children} - - ); -}); - -interface HTMLCarousel extends React.PropsWithChildren { - sx?: SxProps; - direction?: "horizontal" | "vertical"; - fullscreen?: boolean; - overscrollable?: boolean; - centered?: boolean; - itemWidth?: string; - itemHeight?: string; - autoScroll?: boolean; - autoScrollRatio?: number; - swipeable?: boolean; - disabled?: boolean; - activeIndex?: number; - index?: number; - autoRefresh?: boolean; - onPreChange?: Function; - onPostChange?: Function; - onRefresh?: Function; - onOverscroll?: Function; - animation?: string; - animationOptions?: object; - onSwipe?: Function; -} - -export { Carousel }; diff --git a/src/components/onsenui/CarouselItem.tsx b/src/components/onsenui/CarouselItem.tsx deleted file mode 100644 index 9b56533e..00000000 --- a/src/components/onsenui/CarouselItem.tsx +++ /dev/null @@ -1,11 +0,0 @@ -import onsCustomElement from "@Util/onsCustomElement"; -import { SxProps } from "@mui/material"; - -const CarouselItem = onsCustomElement("ons-carousel-item")({}); - -interface HTMLCarouselItem extends React.PropsWithChildren { - sx?: SxProps; - modifier?: string; -} - -export { HTMLCarouselItem, CarouselItem }; diff --git a/src/components/onsenui/Fab.tsx b/src/components/onsenui/Fab.tsx deleted file mode 100644 index 3253b53e..00000000 --- a/src/components/onsenui/Fab.tsx +++ /dev/null @@ -1,14 +0,0 @@ -import "onsenui/esm/elements/ons-fab"; -import onsCustomElement from "@Util/onsCustomElement"; - -export interface FabProps extends React.PropsWithChildren { - modifier?: string; - ripple?: boolean; - position?: string; - disabled?: boolean; - onClick?: React.MouseEventHandler; -} - -export const Fab = onsCustomElement("ons-fab")({}); - -export default Fab; diff --git a/src/components/onsenui/GestureDetector.tsx b/src/components/onsenui/GestureDetector.tsx deleted file mode 100644 index 49abbc4b..00000000 --- a/src/components/onsenui/GestureDetector.tsx +++ /dev/null @@ -1,29 +0,0 @@ -import "onsenui/esm/elements/ons-gesture-detector"; -import onsCustomElement from "@Util/onsCustomElement"; - -export type HTMLGestureDetectorEvent = (e?: Event) => void; - -interface HTMLGestureDetector extends React.PropsWithChildren { - onDrag?: HTMLGestureDetectorEvent; - onDragLeft?: HTMLGestureDetectorEvent; - onDragRight?: HTMLGestureDetectorEvent; - onDragUp?: HTMLGestureDetectorEvent; - onDragDown?: HTMLGestureDetectorEvent; - onHold?: HTMLGestureDetectorEvent; - onRelease?: HTMLGestureDetectorEvent; - onSwipe?: HTMLGestureDetectorEvent; - onSwipeLeft?: HTMLGestureDetectorEvent; - onSwipeRight?: HTMLGestureDetectorEvent; - onSwipeUp?: HTMLGestureDetectorEvent; - onSwipeDown?: HTMLGestureDetectorEvent; - onTap?: HTMLGestureDetectorEvent; - onDoubleTap?: HTMLGestureDetectorEvent; - onPinch?: HTMLGestureDetectorEvent; - onPinchIn?: HTMLGestureDetectorEvent; - onPinchOut?: HTMLGestureDetectorEvent; - onTouch?: HTMLGestureDetectorEvent; - onTransform?: HTMLGestureDetectorEvent; - onRotate?: HTMLGestureDetectorEvent; -} - -export const GestureDetector = onsCustomElement("ons-gesture-detector")({}); diff --git a/src/components/onsenui/Page.tsx b/src/components/onsenui/Page.tsx deleted file mode 100644 index 4ffd5fb7..00000000 --- a/src/components/onsenui/Page.tsx +++ /dev/null @@ -1,105 +0,0 @@ -import React from "react"; -import "onsenui/esm/elements/ons-page"; -import onsCustomElement from "@Util/onsCustomElement"; -import { Box, SxProps, styled } from "@mui/material"; -import { os } from "@Native/Os"; -import { useTheme } from "@Hooks/useTheme"; - -export type RenderFunction = (ref: React.ForwardedRef, context: ActivityContext) => JSX.Element | null | undefined; - -interface NativeUIColors { - mount: string; - unmount: string; -} - -interface HTMLPage { - /** - * Styles that are passed to plain component - */ - compSx?: SxProps; - sx?: SxProps; - backgroundStyle?: SxProps; - modifier?: string; - renderModal?: RenderFunction; - renderToolbar?: RenderFunction; - renderBottomToolbar?: RenderFunction; - renderFixed?: RenderFunction; - onInit?: void | Function; - onShow?: void | Function; - onHide?: void | Function; - onInfiniteScroll?: () => void; - onDeviceBackButton?: (event: DeviceBackButtonEvent) => void; - children?: React.ReactNode; - statusbarColor?: string; - setStatusBarColor?: string; - setNavigationBarColor?: string; -} - -const HTMLPage = onsCustomElement("ons-page", { - notAttributes: ["onInfiniteScroll", "onDeviceBackButton"], -})({}); - -const _Page = React.forwardRef((props, ref) => { - const { theme } = useTheme(); - const { context } = useActivity(); - const { renderToolbar, renderBottomToolbar, renderModal, renderFixed, sx, compSx, children, backgroundStyle, ...rest } = props; - - return ( - - {renderToolbar && renderToolbar(ref, context)} - - - {children} - - - {renderModal && renderModal(ref, context)} - - {renderFixed && renderFixed(ref, context)} - {renderBottomToolbar && renderBottomToolbar(ref, context)} - - ); -}); - -import useMediaQuery from "@mui/material/useMediaQuery"; -import { ActivityContext, useActivity } from "@Hooks/useActivity"; - -interface ContentProps { - /** - * This property affects only small screens - */ - zeroMargin?: boolean; - minWidth?: number; - maxWidth?: number; -} - -interface IntrinsicElements extends Omit { - section: React.DetailedHTMLProps & ContentProps, HTMLElement>; -} - -const Content = styled("section")((props: ContentProps) => ({ - display: "flex", - flexDirection: "column", - margin: props.zeroMargin ? 0 : 8, -})); - -const RelativeContent = styled(Content)((props: ContentProps) => { - const matches = useMediaQuery("(max-width: 767px)"); - - return { - boxSizing: "border-box", - minWidth: props.minWidth ? props.minWidth : 200, - maxWidth: props.maxWidth ? props.maxWidth : 980, - margin: "0 auto", - ...(matches ? { padding: props.zeroMargin ? 0 : 8 } : { padding: "8px 45px 8px 45px" }), - }; -}); - -const Page = Object.assign(_Page, { - Content: Content, - /** - * Used for large screen to prevent content stretching - */ - RelativeContent: RelativeContent, -}); - -export { Page }; diff --git a/src/components/onsenui/RouterNavigator.tsx b/src/components/onsenui/RouterNavigator.tsx deleted file mode 100644 index 388ca428..00000000 --- a/src/components/onsenui/RouterNavigator.tsx +++ /dev/null @@ -1,271 +0,0 @@ -import React from "react"; -import ReactDOM from "react-dom"; -import "onsenui/esm/elements/ons-navigator"; -import onsCustomElement from "@Util/onsCustomElement"; -import { Context, Extra } from "@Hooks/useActivity"; - -interface HTMLNavigator { - renderPage: (route: object, props: any) => JSX.Element; - routeConfig: { - routeStack: any[]; - processStack: any[]; - }; - onPrePush?: Function; - onPostPush?: Function; - onPrePop?: Function; - onPostPop?: Function; - animation?: string; - animationOptions?: object; - swipeable?: boolean | string; - swipePop?: Function; - onDeviceBackButton?: Function; -} - -interface HTMLNavigatorClass extends HTMLNavigator { - innerRef: any; -} - -interface State { - internalStack: { route: any; props?: any; context?: any; extra?: any }[]; -} - -type Noop = () => void; - -const HTMLNavigator = onsCustomElement>("ons-navigator")({}); - -class RouterNavigatorClass extends React.Component { - private cancelUpdate: boolean; - private ref: React.RefObject; - private onPrePush: (event: any) => any; - private onPostPush: (event: any) => any; - private onPrePop: (event: any) => any; - private onPostPop: (event: any) => any; - private _url: URL; - - public constructor(props: HTMLNavigatorClass | Readonly) { - super(props); - - this.cancelUpdate = false; - - const callback = (name, event) => { - if (this.props[name]) { - return this.props[name](event); - } - }; - this.onPrePush = callback.bind(this, "onPrePush"); - this.onPostPush = callback.bind(this, "onPostPush"); - this.onPrePop = callback.bind(this, "onPrePop"); - this.onPostPop = callback.bind(this, "onPostPop"); - - this.ref = React.createRef(); - - this._url = new URL(window.location.href); - - this.state = { - internalStack: [], - }; - } - - private update(cb?: () => void) { - if (!this.cancelUpdate) { - this.setState({}, cb); - } - } - - private resetPageStack(routes: object[], options = {}, props = {}) { - if (this.isRunning()) { - return; - } - - const update = () => { - return new Promise((resolve) => { - this.setState({ internalStack: [...this.state.internalStack, { route: routes[routes.length - 1] }] }, resolve as Noop); - }); - }; - - return this.ref.current._pushPage(options, update).then(() => { - this.setState({ internalStack: [{ route: [...routes] }] }); - }); - } - - private pushPage(route: any, options = {}, props = {}, context = {}, extra = {}) { - if (this.isRunning()) { - return; - } - - const update = () => { - return new Promise((resolve) => { - this.setState( - (prevState) => { - return { internalStack: [...prevState.internalStack, { route: route, props: props, context: context, extra: extra }] }; - }, - - resolve as Noop - ); - }); - }; - - return this.ref.current._pushPage(options, update); - } - - private isRunning() { - return this.ref.current._isRunning; - } - - private replacePage(route: object, options = {}, props = {}, context = {}, extra = {}) { - if (this.isRunning()) { - return; - } - - const update = () => { - return new Promise((resolve) => { - this.setState((prevState) => { - return { internalStack: [...prevState.internalStack, { route: route, props: props, context: context, extra: extra }] }; - }, resolve as Noop); - }); - }; - - return this.ref.current._pushPage(options, update).then(() => { - this.setState((prevState) => { - return { internalStack: [...prevState.internalStack.slice(0, -2), { route: route, props: props, context: context, extra: extra }] }; - }); - }); - } - - private popPage(options?: any) { - if (this.isRunning()) { - return; - } - - const update = () => { - return new Promise((resolve) => { - ReactDOM.flushSync(() => { - // prevents flickering caused by React 18 batching - this.setState((prevState) => { - return { internalStack: prevState.internalStack.slice(0, -1) }; - }, resolve as Noop); - }); - }); - }; - - return this.ref.current._popPage(options, update); - } - - private _onDeviceBackButton(event?: any) { - if (this.props.routeConfig.routeStack.length > 1) { - this.popPage(); - } else { - event.callParentHandler(); - } - } - - public componentDidMount() { - const node = this.ref.current; - - this.cancelUpdate = false; - - node.addEventListener("prepush", this.onPrePush); - node.addEventListener("postpush", this.onPostPush); - node.addEventListener("prepop", this.onPrePop); - node.addEventListener("postpop", this.onPostPop); - - if (!this.props.routeConfig) { - throw new Error("In RouterNavigator the property routeConfig needs to be set"); - } - - node.swipeMax = this.props.swipePop; - node.onDeviceBackButton = this.props.onDeviceBackButton || this._onDeviceBackButton.bind(this); - - this.setState({ internalStack: this.props.routeConfig.routeStack }); - } - - public componentWillUnmount() { - const node = this.ref.current; - node.removeEventListener("prepush", this.onPrePush); - node.removeEventListener("postpush", this.onPostPush); - node.removeEventListener("prepop", this.onPrePop); - node.removeEventListener("postpop", this.onPostPop); - this.cancelUpdate = true; - } - - public componentDidUpdate(prevProps: Readonly) { - if (this.props.onDeviceBackButton !== undefined) { - this.ref.current.onDeviceBackButton = this.props.onDeviceBackButton; - } - - const processStack = [...this.props.routeConfig.processStack]; - - /** - * Fix for Redux Timetravel. - */ - if ( - prevProps.routeConfig.processStack.length < this.props.routeConfig.processStack.length && - prevProps.routeConfig.routeStack.length > this.props.routeConfig.routeStack.length - ) { - return; - } - - if (processStack.length > 0) { - const { type, route, options, props, context, extra } = processStack[0]; - switch (type) { - case "push": - this.pushPage(route, options, props, context, extra); - break; - case "pop": - this.popPage(options); - break; - case "reset": - if (Array.isArray(route)) { - this.resetPageStack(route, options); - } else { - this.resetPageStack([route], options); - } - break; - case "replace": - this.replacePage(route, options, props, context, extra); - break; - default: - throw new Error(`Unknown type ${type} in processStack`); - } - } - } - - public render() { - const { - innerRef, - renderPage, - - // these props should not be passed down - onPrePush, - onPostPush, - onPrePop, - onPostPop, - swipePop, - onDeviceBackButton, - - ...rest - } = this.props; - - const pagesToRender = this.state.internalStack.map((item) => { - return ( - - - {renderPage(item.route, item.props)} - - - ); - }); - - if (innerRef && innerRef !== this.ref) { - this.ref = innerRef; - } - - return ; - } -} - -const _RouterNavigator = React.forwardRef((props, ref) => ); - -const RouterNavigator = Object.assign(_RouterNavigator, {}); - -export { RouterNavigator }; diff --git a/src/components/onsenui/Splitter.tsx b/src/components/onsenui/Splitter.tsx deleted file mode 100644 index 7359fbf4..00000000 --- a/src/components/onsenui/Splitter.tsx +++ /dev/null @@ -1,59 +0,0 @@ -import onsCustomElement from "@Util/onsCustomElement"; -import { SxProps } from "@mui/material"; -import "onsenui/esm/elements/ons-splitter"; -import React from "react"; - -interface HTMLSplitter { - onDeviceBackButton?: Function; -} - -interface HTMLSplitterContent {} - -interface HTMLSplitterSide { - sx?: SxProps; - collapse?: boolean | "portrait" | "landscape"; - swipeable?: boolean; - isOpen?: boolean; - onPostOpen?: Function; - onOpen?: Function; - onPostClose?: Function; - onClose?: Function; - side?: "left" | "right"; - swipeTargetWidth?: number; - width?: number | string; - animation?: string; - animationOptions?: object; - openThreshold?: number; - onPreOpen?: Function; - onPreClose?: Function; - onModeChange?: Function; - children?: React.ReactNode; -} - -const HTMLSplitter = onsCustomElement("ons-splitter", { - notAttributes: ["onDeviceBackButton"], -})({}); -const HTMLSplitterContent = onsCustomElement("ons-splitter-content")({}); -const HTMLSplitterSide = onsCustomElement("ons-splitter-side", { - deprecated: { - onOpen: "onPostOpen", - onClose: "onPostClose", - }, - notAttributes: ["isOpen"], -})({}); - -const _SplitterSide = React.forwardRef((props, ref) => { - const { width, ...rest } = props; - - // number values for width are deprecated but handle them safely to avoid breaking user code - const realWidth = typeof width === "number" ? `${width}px` : width; - - return ; -}); - -const Splitter = Object.assign(HTMLSplitter, { - Content: HTMLSplitterContent, - Side: _SplitterSide, -}); - -export { Splitter }; diff --git a/src/components/onsenui/Tabbar.tsx b/src/components/onsenui/Tabbar.tsx deleted file mode 100644 index 9d567c10..00000000 --- a/src/components/onsenui/Tabbar.tsx +++ /dev/null @@ -1,81 +0,0 @@ -import React, { useEffect, useState } from "react"; -import "onsenui/esm/elements/ons-tab"; -import "onsenui/esm/elements/ons-tabbar"; -import onsCustomElement from "@Util/onsCustomElement"; -import { SxProps } from "@mui/material"; - -export interface TabbarRenderTab { - content: JSX.Element; - tab: JSX.Element; -} - -interface HTMLTabbar { - sx?: SxProps; - activeIndex?: number; - index?: number; - renderTabs: (index?: number, ref?: React.ForwardedRef) => TabbarRenderTab[]; - position?: string; - swipeable?: boolean; - ignoreEdgeWidth?: number; - animation?: "none" | "slide"; - animationOptions?: object; - tabBorder?: boolean; - onPreChange?: Function; - onPostChange?: Function; - onReactive?: Function; - onSwipe?: Function; - hideTabs?: boolean; - visible?: boolean; - modifier?: string; -} - -const HTMLTabbar = onsCustomElement>("ons-tabbar", { - deprecated: { - index: "activeIndex", - }, -})({}); - -const _Tabbar = React.forwardRef((props, ref) => { - const { visible, hideTabs, renderTabs, ...rest } = props; - const [reallyHideTabs, setReallyHideTabs] = useState(); - - const tabs = renderTabs(props.activeIndex, ref); - - React.useEffect(() => { - // visible is deprecated in favour of hideTabs, but if visible is defined and - // hideTabs is not, we use its negation as the value of hideTabs - if (hideTabs === undefined && visible !== undefined) { - setReallyHideTabs(!visible); - } else { - setReallyHideTabs(hideTabs); - } - }, [hideTabs, visible]); - - return ( - -
-
{tabs.map((tab) => tab.content)}
-
-
-
- {tabs.map((tab) => tab.tab)} -
-
-
- ); -}); - -interface HTMLTab { - icon?: string; - activeIcon?: string; - label?: React.ReactNode; - badge?: string; -} - -const HTMLTab = onsCustomElement("ons-tab")({}); - -const Tabbar = Object.assign(_Tabbar, { - Tab: HTMLTab, -}); - -export { Tabbar }; diff --git a/src/components/onsenui/Toolbar.tsx b/src/components/onsenui/Toolbar.tsx deleted file mode 100644 index 7a4c43ea..00000000 --- a/src/components/onsenui/Toolbar.tsx +++ /dev/null @@ -1,75 +0,0 @@ -import "onsenui/esm/elements/ons-toolbar"; -import onsCustomElement from "@Util/onsCustomElement"; -import Icon from "@Components/Icon"; -import React from "react"; -import { OverridableComponent } from "@mui/material/OverridableComponent"; -import ArrowBackIcon from "@mui/icons-material/ArrowBack"; -import Button from "@mui/material/Button"; -import { SvgIconTypeMap } from "@mui/material/SvgIcon"; -import { SxProps, Theme } from "@mui/material/styles"; -import { Box, BoxProps } from "@mui/material"; - -interface HTMLToolbar { - modifier?: string; - visible?: boolean; - static?: boolean; - inline?: boolean; - children?: React.ReactNode; -} - -interface HTMLToolbarButton { - children?: React.ReactNode; - sx?: SxProps; - modifier?: string; - disabled?: boolean; - onClick?: React.MouseEventHandler; - keepLight?: boolean; - id?: string; - icon?: OverridableComponent>; - iconProps?: any; -} - -const HTMLToolbar = onsCustomElement("ons-toolbar", { notAttributes: ["visible"] })({}); -const HTMLToolbarButton = onsCustomElement("ons-toolbar-button")({}); - -const ToolbarButton = React.forwardRef((props: HTMLToolbarButton, ref: React.Ref) => { - const { icon, iconProps, keepLight, children, ...rest } = props; - return ( - - {icon ? : <>{children}} - - ); -}); - -interface ToolbarElementsProps extends React.PropsWithChildren { - sx?: SxProps; -} - -const ToolbarLeft = React.forwardRef>((props, ref) => { - const { component = "div", ...rest } = props; - return ; -}); - -const ToolbarCenter = React.forwardRef>((props, ref) => { - const { component = "div", ...rest } = props; - return ; -}); - -const ToolbarRight = React.forwardRef>((props, ref) => { - const { component = "div", ...rest } = props; - return ; -}); - -const ToolbarBackButton = React.forwardRef>((props, ref) => ( - -)); - -const Toolbar = Object.assign(HTMLToolbar, { - Button: ToolbarButton, - Left: ToolbarLeft, - Center: ToolbarCenter, - Right: ToolbarRight, - BackButton: ToolbarBackButton, -}); - -export { Toolbar }; diff --git a/src/custom-elements/anchor.ts b/src/custom-elements/anchor.ts deleted file mode 100644 index 9b9a8665..00000000 --- a/src/custom-elements/anchor.ts +++ /dev/null @@ -1,9 +0,0 @@ -export class MMRLAnchor extends HTMLElement { - constructor() { - super(); - } - - static get observedAttributes() { - return ["class", "href", "page", "noicon", "onclick"]; - } -} diff --git a/src/custom-elements/app.ts b/src/custom-elements/app.ts deleted file mode 100644 index 122bafe9..00000000 --- a/src/custom-elements/app.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { BuildConfig } from "@Native/BuildConfig"; - -export class MMRLApp extends HTMLElement { - public constructor() { - super(); - this.initConfigStats([ - { - key: "package", - value: BuildConfig.APPLICATION_ID, - }, - { - key: "version-name", - value: BuildConfig.VERSION_NAME, - }, - { - key: "version-code", - value: BuildConfig.VERSION_CODE, - }, - { - key: "debug", - value: BuildConfig.DEBUG, - }, - { - key: "build-type", - value: BuildConfig.BUILD_TYPE, - }, - ]); - } - - private initConfigStats(data: any) { - return data.map((element: { key: string; value: any }) => { - return this.set(element.key, element.value); - }); - } - - private set(qualifiedName: string, value: string) { - this.setAttribute(qualifiedName, value); - } -} diff --git a/src/external/telegram-pusher/App.tsx b/src/external/telegram-pusher/App.tsx deleted file mode 100644 index 9e1f0158..00000000 --- a/src/external/telegram-pusher/App.tsx +++ /dev/null @@ -1,592 +0,0 @@ -import { - TextField, - Stack, - Button, - Box, - ButtonGroup, - Typography, - MenuItem, - Select, - InputLabel, - FormControl, - ImageListItem, - ImageList, - Alert, - AlertTitle, - Card, - CardActions, - CardContent, -} from "@mui/material"; -import { Send, Add, Delete, Save } from "@mui/icons-material"; -import React from "react"; -import { useNativeStorage } from "@Hooks/useNativeStorage"; -import { os } from "@Native/Os"; -import { Toolbar } from "@Components/onsenui/Toolbar"; -import { Page } from "@Components/onsenui/Page"; -import { Chooser } from "@Native/Chooser"; -import { Image } from "@Components/dapi/Image"; - -const App: React.FC = () => { - const [content, setContent] = useNativeStorage("tgs_content", ""); - const [botToken, setBotToken] = useNativeStorage("tgs_bot_token", ""); - const [chatId, setChatId] = useNativeStorage("tgs_chat_id", ""); - - const [buttonsPresets, setButtonsPresets] = useNativeStorage("tgs_buttons_presets", []); - const [buttons, setButtons] = useNativeStorage("tgs_buttons", [ - [{ text: "📦 Download", url: "https://google.com" }], - [ - { text: "Source", url: "https://google.com" }, - { text: "Support", url: "https://google.com" }, - ], - [{ text: "Donate", url: "https://google.com" }], - ]); - - const [newButtonText, setNewButtonText] = React.useState(""); - const [newButtonUrl, setNewButtonUrl] = React.useState(""); - const [selectedRow, setSelectedRow] = React.useState(0); - const [images, setImages] = React.useState([]); - const [document, setDocument] = React.useState(null); - - const validBotToken = React.useMemo(() => { - return botToken.length >= 43; - }, [botToken]); - - const validChatId = React.useMemo(() => { - return chatId.length >= 1; - }, [chatId]); - - const newButtonValidText = React.useMemo(() => { - return newButtonText.length >= 1; - }, [newButtonText]); - - const newButtonValidUrl = React.useMemo(() => { - const regex = /^(https?:\/\/)?([a-zA-Z0-9-]+\.)+[a-zA-Z]{2,}(:[0-9]{1,5})?(\/.*)?$/; - return regex.test(newButtonUrl); - }, [newButtonUrl]); - - const handleRowChange = (event) => { - setSelectedRow(event.target.value); - }; - - const addNewButton = () => { - if (newButtonText && newButtonUrl) { - const newButton = { text: newButtonText, url: newButtonUrl }; - const updatedButtons = [...buttons]; - - // Check if selected row is valid - if (selectedRow >= updatedButtons.length) { - // If selected row is out of bounds, add new rows if needed - updatedButtons.push([newButton]); - } else { - // Add button to the selected row - updatedButtons[selectedRow] = [...updatedButtons[selectedRow], newButton]; - } - - setButtons(updatedButtons); - setNewButtonText(""); - setNewButtonUrl(""); - } - }; - - const saveButtonsPreset = () => { - setButtonsPresets((p) => [...p, buttons]); - }; - - const isMoreThanOneImage = React.useMemo(() => images.length > 1, [images]); - const isImage = React.useMemo(() => images.length !== 0, [images]); - - const removeButton = (rowIndex, buttonIndex) => { - const updatedButtons = buttons - .map((row, i) => (i === rowIndex ? row.filter((_, j) => j !== buttonIndex) : row)) - .filter((row) => row.length > 0); // Filter out empty rows - - setButtons(updatedButtons); - }; - - const handleSave = React.useCallback(async () => { - if (!(validBotToken || validBotToken)) { - os.toast("Cannot send a message without Chat ID or Bot Token", Toast.LENGTH_SHORT); - return; - } - - const sendType = document ? "sendDocument" : isImage ? (isMoreThanOneImage ? "sendMediaGroup" : "sendPhoto") : "sendMessage"; - - const data = new FormData(); - - data.append("chat_id", chatId); - // data.append("disable_notification", "true"); - - // disable for meadia groups - if (!isMoreThanOneImage) { - data.append("parse_mode", "markdown"); - if (buttons) { - data.append( - "reply_markup", - JSON.stringify({ - inline_keyboard: buttons, - }) - ); - } - } - - if (isImage) { - if (images.length > 1) { - const mediaGroup = images.map((image, index) => ({ - type: "photo", - media: `attach://photo${index}`, // Reference to the appended Blob - caption: index === 0 ? content : undefined, // Optionally add caption only to the first image - })); - - // Append each image Blob to FormData with a unique name - images.forEach((image, index) => { - data.append(`photo${index}`, new Blob([image], { type: "image/png" }), `photo${index}.png`); - }); - - data.append("media", JSON.stringify(mediaGroup)); - } else { - data.append("caption", content); - data.append("photo", new Blob([images[0]], { type: "image/png" }), "photo.png"); - } - } else if (document) { - data.append("caption", content); - data.append("document", new Blob([document], { type: document.type }), document.name); - } else { - data.append("text", content); - } - - data.append("disable_web_page_preview", "false"); - - try { - const response = await fetch(`https://api.telegram.org/bot${botToken}/${sendType}`, { - method: "POST", - body: data, - }); - - if (response.ok) { - console.log("Message sent successfully"); - } else { - console.warn("Error sending message: " + response.statusText); - } - } catch (error) { - console.warn("Error sending message: " + (error as Error).message); - } - }, [botToken, chatId, content, buttons, images]); - - const renderToolbar = () => { - return ( - - Telegram Pusher - - - - - ); - }; - - return ( - - - - { - setBotToken(e.target.value); - }} - variant="filled" - /> - { - setChatId(e.target.value); - }} - variant="filled" - /> - - - {isMoreThanOneImage && ( - - Warning - You're about to send a media group. Markdown and inline buttons are not available. - - )} - - { - setContent(e.target.value); - }} - value={content} - sx={{ - mt: 2, - height: "25% !important", - "& .MuiFilledInput-root": { - height: "100% !important", - }, - }} - inputProps={{ - style: { - height: "100% !important", - fontFamily: "monospace", - }, - }} - multiline - /> - - - Document - - - - - - - - - - - - - Images - - - - - {" "} - - - - {images.length !== 0 && ( - - {images.map((image, i) => ( - - - - ))} - - )} - - - - - Buttons - - - - - Choose Row - - - - - - {buttons.map((buttonRow, rowIndex) => ( - - {buttonRow.map((button, buttonIndex) => ( - - - - - ))} - - ))} - - - - - setNewButtonText(e.target.value)} - fullWidth - /> - setNewButtonUrl(e.target.value)} - fullWidth - /> - - - - - - - - - - - Buttons Presets - - - - {buttonsPresets.map((preset, presetIndex) => ( - - - - {preset.map((buttonRow, rowIndex) => ( - - {buttonRow.map((button, buttonIndex) => ( - - - - - ))} - - ))} - - - - - - - - - ))} - - - - - ); -}; - -export default App; diff --git a/src/external/telegram-pusher/index.tsx b/src/external/telegram-pusher/index.tsx deleted file mode 100644 index 70be2b44..00000000 --- a/src/external/telegram-pusher/index.tsx +++ /dev/null @@ -1,37 +0,0 @@ -import React from "react"; -import ons from "onsenui"; -import { CssBaseline } from "@mui/material"; -import { LightTheme } from "@Styles/light_theme"; -import { ConfirmProvider } from "material-ui-confirm"; -import { ThemeProvider } from "@Hooks/useTheme"; -import { Preventer, render } from "react-render-tools"; -import { ModFSProvider } from "@Hooks/useModFS"; - -import "@Styles/onsenui.scss"; -import "@Styles/default.scss"; - -ons.platform.select("android"); - -import { configureSingle } from "@zenfs/core"; -import { IndexedDB } from "@zenfs/dom"; -import App from "./App"; -await configureSingle({ backend: IndexedDB }); - -ons.ready(() => { - render( - - - - - - - - - - - - - , - "div" - ); -}); diff --git a/src/hoc/withRequireNewVersion.tsx b/src/hoc/withRequireNewVersion.tsx deleted file mode 100644 index 19a1bc1f..00000000 --- a/src/hoc/withRequireNewVersion.tsx +++ /dev/null @@ -1,67 +0,0 @@ -import React from "react"; -import { Anchor } from "@Components/dapi/Anchor"; -import { Page } from "@Components/onsenui/Page"; -import { Toolbar } from "@Components/onsenui/Toolbar"; -import { useActivity } from "@Hooks/useActivity"; -import { BuildConfig } from "@Native/BuildConfig"; -import Box from "@mui/material/Box"; -import { useStrings } from "@Hooks/useStrings"; - -interface HOC_Options

{ - versionCode?: number; - component: React.FunctionComponent

| React.ComponentType

; - title?: string; - text?: React.ReactNode; -} - -function withRequireNewVersion

(opt: HOC_Options

): HOC_Options

["component"] { - const { strings } = useStrings(); - const { - versionCode = BuildConfig.VERSION_CODE, - component, - title = "New version required!", - text = strings("hoc_with_require_new_version", { - versionCode, - br:
, - url: release, - }), - } = opt; - const { context } = useActivity(); - - if (BuildConfig.VERSION_CODE < versionCode) { - return () => { - return ( - { - return ( - - - - - {title} - - ); - }} - > - - {text} - - - ); - }; - } else { - return component; - } -} - -export { withRequireNewVersion }; diff --git a/src/hooks/native/useLog.ts b/src/hooks/native/useLog.ts deleted file mode 100644 index dd1a47b1..00000000 --- a/src/hooks/native/useLog.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { os } from "@Native/Os"; - -export function useLog(tag: string) { - const DEBUG = 3; - const INFO = 4; - const WARN = 5; - const ERROR = 6; - - return { - d: (msg: string) => { - if (os.isAndroid) { - window.__log__.native_log(DEBUG, String(tag), msg); - } else { - console.debug(`D/${tag}: ${msg}`); - } - }, - i: (msg: string) => { - if (os.isAndroid) { - window.__log__.native_log(INFO, String(tag), msg); - } else { - console.info(`D/${tag}: ${msg}`); - } - }, - w: (msg: string) => { - if (os.isAndroid) { - window.__log__.native_log(WARN, String(tag), msg); - } else { - console.warn(`D/${tag}: ${msg}`); - } - }, - e: (msg: string) => { - if (os.isAndroid) { - window.__log__.native_log(ERROR, String(tag), msg); - } else { - console.error(`D/${tag}: ${msg}`); - } - }, - }; -} diff --git a/src/hooks/useActivity.ts b/src/hooks/useActivity.ts deleted file mode 100644 index d7836a2a..00000000 --- a/src/hooks/useActivity.ts +++ /dev/null @@ -1,35 +0,0 @@ -import React from "react"; - -export const Context = React.createContext({}); -export const Extra = React.createContext({}); - -export interface IntentPusher { - key: string; - /** - * Prevents the activity from being memoized - */ - noMemo?: boolean; - component: React.FunctionComponent

| React.ComponentType

; - options?: any; - extra?: E; - props?: React.Attributes & P; -} - -export interface ActivityContext { - readonly popPage: (options?: any) => void; - readonly pushPage: (props: IntentPusher) => void; - readonly replacePage: (props: IntentPusher) => void; - readonly splitter: { - readonly show: () => void; - readonly hide: () => void; - }; -} - -export function useActivity() { - const ctx = React.useContext(Context) as ActivityContext; - const etx = React.useContext(Extra) as E; - return { - context: ctx, - extra: etx, - }; -} diff --git a/src/hooks/useBlacklist.ts b/src/hooks/useBlacklist.ts deleted file mode 100644 index 3af187a8..00000000 --- a/src/hooks/useBlacklist.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { SuFile } from "@Native/SuFile"; -import { useFetch } from "./useFetch"; - -interface BlacklistedModule { - id: string; - source: string; - hidden: boolean; - notes: string; - antifeatures: Track["antifeatures"]; -} - -export const useBlacklist = () => { - const [bl] = useFetch("https://gr.dergoogler.com/gmr/json/blacklist.json", { type: "json" }); - return bl ? bl : []; -}; diff --git a/src/hooks/useCategories.ts b/src/hooks/useCategories.ts deleted file mode 100644 index 2685006f..00000000 --- a/src/hooks/useCategories.ts +++ /dev/null @@ -1,45 +0,0 @@ -export function useCategories(input?: arr) { - const categories = [ - "Tools", - "Boot", - "Coding", - "Configurable", - "Management", - "System", - "Apps", - "Gaming", - "Other", - // New categroies - "Magisk", - "KernelSU", - "Zygisk", - "LSPosed", - "Xposed", - "Performance Optimization", - "Battery Life", - "Customization", - "Audio Enhancements", - "Security", - "Camera Enhancements", - "SystemUI Mods", - "Tweaks and Hacks", - "Modifications for Root Apps", - "System Fonts and Emojis", - // Same as "Other" - "Miscellaneous", - "ROM-Specific Modules", - "Gamepad and Controller Support", - "App Additions and Features", - "Adblocking and Hosts Files", - "Navigation Bar and Gesture Customization", - "Advanced Audio Mods", - "Custom Kernels", - "Boot Animation", - "Privacy Enhancements", - ]; - if (input) { - return { allCategories: categories, filteredCategories: categories.filter((i) => input.indexOf(i) !== -1) }; - } else { - return { allCategories: categories, filteredCategories: [] }; - } -} diff --git a/src/hooks/useDownloadModule.ts b/src/hooks/useDownloadModule.ts deleted file mode 100644 index 1f35e805..00000000 --- a/src/hooks/useDownloadModule.ts +++ /dev/null @@ -1,47 +0,0 @@ -import React from "react"; -import { useStrings } from "./useStrings"; -import { useConfirm } from "material-ui-confirm"; -import { os } from "@Native/Os"; -import { Download } from "@Native/Download"; - -const useDownloadModule = (): [(url?: string, dest?: string) => void, number] => { - const { strings } = useStrings(); - const konfirm = useConfirm(); - - const [progress, setProgress] = React.useState(0); - - const start = (url?: string, dest?: string) => { - if (!url || !dest) return; - - const dl = new Download(url, dest); - - dl.onChange = (obj) => { - switch (obj.type) { - case "downloading": - setProgress(obj.state); - break; - case "finished": - setProgress(0); - konfirm({ - title: strings("download"), - description: strings("file_downloaded", { path: dest }), - }) - .then(() => {}) - .catch(() => {}); - - break; - } - }; - - dl.onError = (err) => { - setProgress(0); - os.toast(err, Toast.LENGTH_SHORT); - }; - - dl.start(); - }; - - return [start, progress]; -}; - -export { useDownloadModule }; diff --git a/src/hooks/useFetch.ts b/src/hooks/useFetch.ts deleted file mode 100644 index 4ba166f5..00000000 --- a/src/hooks/useFetch.ts +++ /dev/null @@ -1,89 +0,0 @@ -import { useEffect, useReducer, useRef } from "react"; -// https://usehooks-ts.com/react-hook/use-fetch - -interface State { - data?: T; - error?: Error; -} - -type Cache = { [url: string]: T }; - -interface FetchOptions extends ResponseInit { - type?: "json" | "text" | "blob"; -} - -// discriminated union type -type Action = { type: "loading" } | { type: "fetched"; payload: T } | { type: "error"; payload: Error }; - -export function useFetch(url?: string, options?: FetchOptions): [T | undefined, Error | undefined] { - const cache = useRef>({}); - - // Used to prevent state update if the component is unmounted - const cancelRequest = useRef(false); - - const initialState: State = { - error: undefined, - data: undefined, - }; - - // Keep state logic separated - const fetchReducer = (state: State, action: Action): State => { - switch (action.type) { - case "loading": - return { ...initialState }; - case "fetched": - return { ...initialState, data: action.payload }; - case "error": - return { ...initialState, error: action.payload }; - default: - return state; - } - }; - - const [state, dispatch] = useReducer(fetchReducer, initialState); - - useEffect(() => { - // Do nothing if the url is not given - if (!url) return; - - cancelRequest.current = false; - - const fetchData = async () => { - dispatch({ type: "loading" }); - - // If a cache exists for this url, return it - if (cache.current[url]) { - dispatch({ type: "fetched", payload: cache.current[url] }); - return; - } - - try { - const response = await fetch(url, options); - if (!response.ok) { - throw new Error(response.statusText); - } - - let data: T = (await response[options?.type || "json"]()) as T; - cache.current[url] = data; - if (cancelRequest.current) return; - - dispatch({ type: "fetched", payload: data }); - } catch (error) { - if (cancelRequest.current) return; - - dispatch({ type: "error", payload: error as Error }); - } - }; - - void fetchData(); - - // Use the cleanup function for avoiding a possibly... - // ...state update after the component was unmounted - return () => { - cancelRequest.current = true; - }; - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [url]); - - return [state.data, state.error]; -} diff --git a/src/hooks/useFormatBytes.ts b/src/hooks/useFormatBytes.ts deleted file mode 100644 index 0572999a..00000000 --- a/src/hooks/useFormatBytes.ts +++ /dev/null @@ -1,17 +0,0 @@ -import React from "react"; - -/** - * A function to format file sizes - * @param bytes - * @param decimalPoint Default is `2` - * @returns - */ -function useFormatBytes(bytes?: number, decimalPoint: number = 2) { - if (!bytes || bytes == 0) return React.useMemo(() => ["0", "Bytes"], [bytes]); - var k = 1000, - sizes = ["Bytes", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"], - i = Math.floor(Math.log(bytes) / Math.log(k)); - return React.useMemo(() => [parseFloat((bytes / Math.pow(k, i)).toFixed(decimalPoint)), sizes[i]], [bytes]); -} - -export { useFormatBytes }; diff --git a/src/hooks/useFormatDate.ts b/src/hooks/useFormatDate.ts deleted file mode 100644 index ee9498dc..00000000 --- a/src/hooks/useFormatDate.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { useMemo } from "react"; -import { useSettings } from "./useSettings"; - -export const useFormatDate = (date: int, multiply: boolean = true) => { - const [language] = useSettings("language"); - - return useMemo( - () => - Intl.DateTimeFormat(language.value, { - year: "numeric", - day: "2-digit", - month: "short", - hour12: true, - }).format(new Date(multiply ? date * 1000 : date)), - [date] - ); -}; diff --git a/src/hooks/useHover.ts b/src/hooks/useHover.ts deleted file mode 100644 index 3fedb893..00000000 --- a/src/hooks/useHover.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { useState } from "react"; - -import type { RefObject } from "react"; - -import { useEventListener } from "usehooks-ts"; - -export function useHover(elementRef: RefObject): boolean { - const [value, setValue] = useState(false); - - const handleEnter = () => { - setValue(true); - }; - const handleLeave = () => { - setValue(false); - }; - - useEventListener("touchstart", handleEnter, elementRef); - useEventListener("touchend", handleLeave, elementRef); - useEventListener("mouseenter", handleEnter, elementRef); - useEventListener("mouseleave", handleLeave, elementRef); - - return value; -} diff --git a/src/hooks/useLocalModules.ts b/src/hooks/useLocalModules.ts deleted file mode 100644 index 6f67a382..00000000 --- a/src/hooks/useLocalModules.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { os } from "@Native/Os"; -import { SuFile } from "@Native/SuFile"; -import { Properties } from "properties-file"; -import React from "react"; -import { useModFS } from "./useModFS"; - -export function useLocalModules() { - const { modFS } = useModFS(); - const [localModules, setLocalModules] = React.useState([]); - - if (os.isAndroid) { - React.useEffect(() => { - const folders = new SuFile(modFS("MODULES")); - if (folders.exist()) { - folders.list().forEach((module) => { - const properties = new SuFile(modFS("PROPS", { MODID: module })); - if (properties.exist()) { - setLocalModules((prev) => { - // Preventing duplicates - const ids = new Set(prev.map((d) => d.id)); - const merged = [ - ...prev, - ...[ - { - ...(new Properties(properties.read()).toObject() as unknown as Module), - timestamp: properties.lastModified(), - __mmrl__local__module__: true, - }, - ].filter((d) => !ids.has(d.id)), - ]; - return merged; - }); - } - }); - } - }, []); - } - - return localModules; -} diff --git a/src/hooks/useLowQualityModule.ts b/src/hooks/useLowQualityModule.ts deleted file mode 100644 index 945505ea..00000000 --- a/src/hooks/useLowQualityModule.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { useMemo } from "react"; - -export const useLowQualityModule = (props: Module, disable?: boolean) => { - const requiredProp = ["id", "name", "version", "versionCode", "author", "description"]; - - const res = useMemo( - () => - requiredProp.reduce(function (i, j) { - return i && props[j] && j in props; - }, true), - [props] - ); - - return disable ? false : !res; -}; diff --git a/src/hooks/useModFS.tsx b/src/hooks/useModFS.tsx deleted file mode 100644 index 402f37ba..00000000 --- a/src/hooks/useModFS.tsx +++ /dev/null @@ -1,147 +0,0 @@ -import { defaultComposer } from "default-composer"; -import React, { createContext, useContext } from "react"; -import { useNativeFileStorage } from "./useNativeFileStorage"; -import { SetStateAction } from "./useStateCallback"; - -import { default as PModFS } from "modfs"; - -export interface ModFS { - //cli - MSUINI: string; - KSUINI: string; - ASUINI: string; - - // default paths - ADB: string; - MMRLFOL: string; - MODULES: string; - MODULECWD: string; - PROPS: string; - SYSTEM: string; - SEPOLICY: string; - CONFIG: string; - - // service paths - LATESERVICE: string; - POSTSERVICE: string; - POSTMOUNT: string; - BOOTCOMP: string; - - // status paths - SKIPMOUNT: string; - DISABLE: string; - REMOVE: string; - UPDATE: string; - - //modconf - CONFCWD: string; - CONFINDEX: string; - MODCONF_PLAYGROUND: string; - MODCONF_PLAYGROUND_MODID: string; - - // modconf standalone - MCALONE: string; - MCALONECWD: string; - MCALONEMETA: string; - MCALONEIDX: string; - - // Installer - EXPLORE_INSTALL: string; - LOCAL_INSTALL: string; -} - -export const INITIAL_MOD_CONF: ModFS = { - //cli - MSUINI: '")>', - KSUINI: '")>', - ASUINI: '")>', - - // default paths - ADB: "/data/adb", - MMRLFOL: "/mmrl", - MODULES: "/modules", - MODULECWD: "/", - PROPS: "/module.prop", - SYSTEM: "/system.prop", - SEPOLICY: "/sepolicy.rule", - CONFIG: `/system/usr/share/mmrl/config/.mdx`, - - // service paths - LATESERVICE: "/service.sh", - POSTSERVICE: "/post-fs-data.sh", - POSTMOUNT: "/post-mount.sh", - BOOTCOMP: "/boot-completed.sh", - - // status paths - SKIPMOUNT: "/skip_mount", - DISABLE: "/disable", - REMOVE: "/remove", - UPDATE: "/update", - - // modconf - CONFCWD: "/system/usr/share/mmrl/config/", - CONFINDEX: "/index.jsx", - MODCONF_PLAYGROUND: "/mmrl/modconf-playground.jsx", - MODCONF_PLAYGROUND_MODID: "playground", - - // modconf standalone - MCALONE: "/modconf", - MCALONECWD: "/", - MCALONEMETA: "/modconf.json", - MCALONEIDX: "/index.jsx", - - // Installer - EXPLORE_INSTALL: 'mmrl install -y ""', - LOCAL_INSTALL: 'mmrl install local -y ', -}; - -export interface ModConfContext { - _modFS: ModFS; - __modFS: ModFS; - modFS(key: K, adds?: Record): ModFS[K]; - modFSParse: (text: string, adds?: ModFS | object) => string; - setModFS(key: K, state: SetStateAction): void; -} - -export const ModConfContext = createContext({ - _modFS: INITIAL_MOD_CONF, - __modFS: INITIAL_MOD_CONF, - modFSParse: (text: string, adds?: ModFS | object) => "", - modFS(key: K, adds?: Record) { - return key; - }, - setModFS(key: K, state: SetStateAction) {}, -}); - -export const useModFS = () => { - return useContext(ModConfContext); -}; - -export const ModFSProvider = (props: React.PropsWithChildren) => { - const [modFS, setModFS] = useNativeFileStorage("/data/adb/mmrl/modfs.v8.json", INITIAL_MOD_CONF, { loader: "json" }); - - const pmodFS = React.useMemo(() => new PModFS(defaultComposer(INITIAL_MOD_CONF, modFS)), [modFS]); - - const contextValue = React.useMemo( - () => ({ - _modFS: defaultComposer(INITIAL_MOD_CONF, modFS), - __modFS: pmodFS.formatEntries(), - modFSParse: (text: string, adds?: ModFS | object) => PModFS.format(text, { ...modFS, ...adds }), - modFS(key: K, adds: ModFS | object): ModFS[K] { - return PModFS.format(pmodFS.get(key)!, { ...modFS, ...adds }); - }, - setModFS: (name, state) => { - setModFS((prev) => { - const newValue = state instanceof Function ? state(prev[name]) : state; - return { - ...prev, - [name]: newValue, - }; - }); - }, - }), - [modFS] - ); - - return ; -}; diff --git a/src/hooks/useModuleInfo.ts b/src/hooks/useModuleInfo.ts deleted file mode 100644 index 7e063ebd..00000000 --- a/src/hooks/useModuleInfo.ts +++ /dev/null @@ -1,46 +0,0 @@ -import React from "react"; - -type PickedModule = - | "timestamp" - | "verified" - | "license" - | "homepage" - | "support" - | "donate" - | "cover" - | "icon" - | "require" - | "screenshots" - | "categories" - | "readme" - | "size" - | "root"; - -type ModuleInfo = Pick & { latestVersion: Version }; - -/** - * Used to handle undefined properties - */ -export const useModuleInfo = (extra: Module): ModuleInfo => { - const { track } = extra; - - const latestVersion = React.useMemo(() => extra.versions[extra.versions.length - 1], [extra.versions]); - - return { - timestamp: extra.timestamp || latestVersion.timestamp, - verified: extra.verified, - license: extra.license || track.license, - homepage: extra.homepage || track.homepage, - support: extra.support || track.support, - donate: extra.donate || track.donate, - cover: extra.cover || track.cover, - icon: extra.icon || track.icon, - require: extra.require || track.require, - screenshots: extra.screenshots || track.screenshots, - categories: extra.categories || track.categories, - readme: extra.readme || track.readme, - latestVersion: latestVersion, - size: extra.size || latestVersion.size, - root: extra.root, - }; -}; diff --git a/src/hooks/useModulesFilter.tsx b/src/hooks/useModulesFilter.tsx deleted file mode 100644 index b53697fb..00000000 --- a/src/hooks/useModulesFilter.tsx +++ /dev/null @@ -1,120 +0,0 @@ -import { SetValue, useNativeStorage } from "./useNativeStorage"; -import CalendarMonthIcon from "@mui/icons-material/CalendarMonth"; -import AbcIcon from "@mui/icons-material/Abc"; -import { useTheme } from "./useTheme"; -import Avatar from "@mui/material/Avatar"; -import List from "@mui/material/List"; -import ListItem from "@mui/material/ListItem"; -import ListItemAvatar from "@mui/material/ListItemAvatar"; -import ListItemButton from "@mui/material/ListItemButton"; -import ListItemText from "@mui/material/ListItemText"; -import Dialog from "@mui/material/Dialog"; -import DialogTitle from "@mui/material/DialogTitle"; -import StarIcon from "@mui/icons-material/Star"; -import StarBorderIcon from "@mui/icons-material/StarBorder"; -import FilterListOff from "@mui/icons-material/FilterListOff"; -import React from "react"; - -export const filters = [ - { - name: "No filter", - icon: FilterListOff, - value: "none", - allowedIds: ["explore", "local", "update"], - }, - { - name: "Newest date first", - icon: CalendarMonthIcon, - value: "date_newest", - allowedIds: ["explore", "local", "update"], - }, - { - name: "Oldest date first", - icon: CalendarMonthIcon, - value: "date_oldest", - allowedIds: ["explore", "local", "update"], - }, - { - name: "Name (A to Z)", - icon: AbcIcon, - value: "alphabetically", - allowedIds: ["explore", "local", "update"], - }, - { - name: "Name (Z to A)", - icon: AbcIcon, - value: "alphabetically_reverse", - allowedIds: ["explore", "local", "update"], - }, - // { - // name: "Most stars", - // icon: StarIcon, - // value: "most_stars", - // allowedIds: ["explore"], - // }, - // { - // name: "Least stars", - // icon: StarBorderIcon, - // value: "least_stars", - // allowedIds: ["explore"], - // }, -]; - -export const useModuleFilter = (key: string): [Array, string, SetValue] => { - const [filter, setFilter] = useNativeStorage(key, filters[0].value); - - const f = React.useMemo( - () => ({ - none: [{}], - date_oldest: [{ key: "timestamp", descending: false }], - date_newest: [{ key: "timestamp", descending: true }], - alphabetically: [{ key: "name", descending: false }], - alphabetically_reverse: [{ key: "name", descending: true }], - // least_stars: [{ key: "stars", descending: false }], - // most_stars: [{ key: "stars", descending: true }], - }), - [] - ); - - return [f[filter], filter, setFilter]; -}; - -interface FilterDialogProps { - id: string; - open: boolean; - selectedValue: string; - onClose: (value: string) => void; -} - -export const FilterDialog = (props: FilterDialogProps) => { - const { theme } = useTheme(); - const { onClose, selectedValue, open } = props; - - const handleClose = () => { - onClose(selectedValue); - }; - - const handleListItemClick = (value: string) => { - onClose(value); - }; - - return ( -

- Apply filter - - {filters.map((filter) => ( - - handleListItemClick(filter.value)}> - - - - - - - - - ))} - - - ); -}; diff --git a/src/hooks/useNativeFileStorage.tsx b/src/hooks/useNativeFileStorage.tsx deleted file mode 100644 index ac0679b7..00000000 --- a/src/hooks/useNativeFileStorage.tsx +++ /dev/null @@ -1,138 +0,0 @@ -import { SetStateAction, useCallback, useEffect, useState } from "react"; -import { defaultComposer } from "default-composer"; -import { SuFile } from "@Native/SuFile"; -import INI from "ini"; -import YAML from "yaml"; -import { os } from "@Native/Os"; -import { SetValue } from "./useNativeStorage"; -import React from "react"; -import { INITIAL_MOD_CONF } from "./useModFS"; -import { path } from "@Util/path"; - -type Loader = "json" | "yaml" | "yml" | "prop" | "properties" | "ini" | null; - -export function useNativeFileStorage( - key: string, - initialValue: T, - opt: { loader: Loader } = { loader: null } -): [T, SetValue] { - const { loader } = opt; - - const dir = React.useMemo(() => new SuFile(path.dirname(key), { readDefaultValue: JSON.stringify(INITIAL_MOD_CONF) }), [key]); - const file = React.useMemo(() => new SuFile(key, { readDefaultValue: JSON.stringify(INITIAL_MOD_CONF) }), [key]); - - React.useEffect(() => { - if (!dir.exist()) { - dir.create(SuFile.NEW_FOLDERS); - } - }, [key]); - - const readValue = useCallback((): T => { - try { - if (file.exist()) { - switch (loader) { - case "json": - return JSON.parse(file.read()); - case "properties": - case "prop": - case "ini": - return INI.parse(file.read()) as T; - case "yml": - case "yaml": - return YAML.parse(file.read()); - default: - return file.read() as T; - } - } else { - return initialValue; - } - } catch (error) { - return initialValue; - } - }, [initialValue, key]); - - const [storedValue, setStoredValue] = useState(readValue); - - const setValue: SetValue = (value) => { - try { - const newValue = value instanceof Function ? value(storedValue) : value; - switch (loader) { - case "json": - file.write(JSON.stringify(newValue, null, 4)); - break; - case "properties": - case "prop": - case "ini": - file.write(INI.stringify(newValue, { whitespace: true, newline: true })); - break; - case "yml": - case "yaml": - file.write(YAML.stringify(newValue)); - break; - default: - file.write(String(newValue)); - break; - } - setStoredValue(newValue); - } catch (error) { - throw new Error(`Error writing file “${key}”: ${error}`); - } - }; - - useEffect(() => { - setStoredValue(readValue()); - }, []); - - return [storedValue, setValue]; -} - -export type ConfigContext = [object, (key: string, state: SetStateAction) => void, SetValue]; - -export const ConfigContext = React.createContext([ - {}, - (key: string, state: SetStateAction) => {}, - (state: SetStateAction) => {}, -]); - -export const useConfig = () => { - return React.useContext(ConfigContext); -}; - -export interface ConfigProvider extends React.PropsWithChildren { - loadFromFile: string; - loader: Loader; - initialConfig: object; -} - -export const ConfigProvider = (props: ConfigProvider) => { - const { loadFromFile, loader = "json", initialConfig } = props; - - if (!loadFromFile) { - throw new TypeError('"loadFromFile" is undefined'); - } - - if (!initialConfig) { - throw new TypeError('"initialConfig" is undefined'); - } - - const [config, setConfig] = useNativeFileStorage(loadFromFile, initialConfig, { loader: loader }); - - const contextValue = React.useMemo( - () => [ - defaultComposer(initialConfig, config), - (name, state) => { - setConfig((prev) => { - const newValue = state instanceof Function ? state(prev[name]) : state; - return { - ...prev, - [name]: newValue, - }; - }); - }, - setConfig, - ], - [config] - ); - - return ; -}; diff --git a/src/hooks/useNativeProperties.tsx b/src/hooks/useNativeProperties.tsx deleted file mode 100644 index e6ef9ef4..00000000 --- a/src/hooks/useNativeProperties.tsx +++ /dev/null @@ -1,77 +0,0 @@ -/** - * FORK (https://usehooks-ts.com/react-hook/use-local-storage) to use our native storage - */ - -import { useCallback, useEffect } from "react"; - -import { os } from "@Native/Os"; -import { Dispatch, SetStateAction, useStateCallback } from "./useStateCallback"; -import { useLog } from "./native/useLog"; -import { Shell } from "@Native/Shell"; -import { parseJSON, useNativeStorage } from "./useNativeStorage"; -import { Properties } from "@Native/Properties"; - -declare global { - interface WindowEventMap { - "native-storage": CustomEvent; - } -} - -export type SetValue = Dispatch, T>; - -function convertToProperType(value: string) { - if (/^(true|1|y|yes|on)$/i.test(value)) { - return /^(true|1|y|yes|on)$/i.test(value); // Convert to boolean true - } else if (!isNaN(value as unknown as number)) { - return parseFloat(value); // Convert to number if it's a valid number - } else { - return value; // Return the original string if no conversion is possible - } -} - -type KJGHKSJFDHGIUDHGKJHFDG = string | boolean | number; - -export function useNativeProperties( - key: string, - initialValue: KJGHKSJFDHGIUDHGKJHFDG -): [KJGHKSJFDHGIUDHGKJHFDG, SetValue] { - const log = useLog("useNativeProperties"); - - const readValue = useCallback((): KJGHKSJFDHGIUDHGKJHFDG => { - // Prevent build error "window is undefined" but keeps working - - if (typeof window === "undefined") { - return initialValue; - } - - try { - return parseJSON(Properties.get(key, JSON.stringify(initialValue))) as KJGHKSJFDHGIUDHGKJHFDG; - } catch (error) { - log.w(`Error reading nativeStorage key “${key}”: ${error}`); - - return initialValue; - } - }, [initialValue, key]); - - const [storedValue, setStoredValue] = useStateCallback(readValue); - - const setValue: SetValue = (value, callback) => { - if (typeof window === "undefined") { - log.w(`Tried setting nativeProperties key “${key}” even though environment is not a client`); - } - - try { - const newValue = value instanceof Function ? value(storedValue) : value; - Properties.set(key, JSON.stringify(JSON.stringify(newValue))); - setStoredValue(newValue, callback); - } catch (error) { - log.w(`Error setting localStorage key “${key}”: ${error}`); - } - }; - - useEffect(() => { - setStoredValue(readValue()); - }, []); - - return [storedValue, setValue]; -} diff --git a/src/hooks/useNativeStorage.tsx b/src/hooks/useNativeStorage.tsx deleted file mode 100644 index a5eeca0a..00000000 --- a/src/hooks/useNativeStorage.tsx +++ /dev/null @@ -1,122 +0,0 @@ -/** - * FORK (https://usehooks-ts.com/react-hook/use-local-storage) to use our native storage - */ - -import { useCallback, useEffect } from "react"; - -import { os } from "@Native/Os"; -import { Dispatch, SetStateAction, useStateCallback } from "./useStateCallback"; -import { useLog } from "./native/useLog"; - -declare global { - interface WindowEventMap { - "native-storage": CustomEvent; - } -} - -export type SetValue = Dispatch, T>; - -export const nativeStorage = os.isAndroid ? window.__nativeStorage__ : window.localStorage; - -export function useNativeStorage(key: string, initialValue: T): [T, SetValue] { - const log = useLog("useNativeStorage"); - // Get from local storage then - - // parse stored json or return initialValue - - const readValue = useCallback((): T => { - // Prevent build error "window is undefined" but keeps working - - if (typeof window === "undefined") { - return initialValue; - } - - try { - const item = nativeStorage.getItem(key); - - return item ? (parseJSON(item) as T) : initialValue; - } catch (error) { - log.w(`Error reading nativeStorage key “${key}”: ${error}`); - - return initialValue; - } - }, [initialValue, key]); - - // State to store our value - - // Pass initial state function to useState so logic is only executed once - - const [storedValue, setStoredValue] = useStateCallback(readValue); - - // Return a wrapped version of useState's setter function that ... - - // ... persists the new value to localStorage. - - const setValue: SetValue = (value, callback) => { - // Prevent build error "window is undefined" but keeps working - - if (typeof window === "undefined") { - log.w(`Tried setting localStorage key “${key}” even though environment is not a client`); - } - - try { - // Allow value to be a function so we have the same API as useState - - const newValue = value instanceof Function ? value(storedValue) : value; - - // Save to local storage - - nativeStorage.setItem(key, JSON.stringify(newValue)); - - // Save state - - setStoredValue(newValue, callback); - - // We dispatch a custom event so every useLocalStorage hook are notified - - // window.dispatchEvent(new Event("local-storage")); - } catch (error) { - log.w(`Error setting localStorage key “${key}”: ${error}`); - } - }; - - useEffect(() => { - setStoredValue(readValue()); - }, []); - - // const handleStorageChange = useCallback( - // (event: StorageEvent | CustomEvent) => { - // if ((event as StorageEvent)?.key && (event as StorageEvent).key !== key) { - // return; - // } - - // setStoredValue(readValue()); - // }, - - // [key, readValue] - // ); - - // // this only works for other documents, not the current one - - // useEventListener("storage", handleStorageChange); - - // // this is a custom event, triggered in writeValueToLocalStorage - - // // See: useLocalStorage() - - // useEventListener("native-storage", handleStorageChange); - - return [storedValue, setValue]; -} - -// A wrapper for "JSON.parse()"" to support "undefined" value - -export function parseJSON(value: string | null): T | Error { - try { - return value === "undefined" ? undefined : JSON.parse(value ?? ""); - } catch (e) { - console.log("parsing error on", { value }); - - return e as Error; - } -} diff --git a/src/hooks/useNetwork.ts b/src/hooks/useNetwork.ts deleted file mode 100644 index 621e3cd5..00000000 --- a/src/hooks/useNetwork.ts +++ /dev/null @@ -1,20 +0,0 @@ -import React from "react"; - -export const useNetwork = () => { - const [isOnline, setNetwork] = React.useState(window.navigator.onLine); - - const updateNetwork = () => { - setNetwork(window.navigator.onLine); - }; - - React.useEffect(() => { - window.addEventListener("offline", updateNetwork); - window.addEventListener("online", updateNetwork); - return () => { - window.removeEventListener("offline", updateNetwork); - window.removeEventListener("online", updateNetwork); - }; - }, [isOnline]); - - return { isNetworkAvailable: isOnline }; -}; diff --git a/src/hooks/useNewerVersion.tsx b/src/hooks/useNewerVersion.tsx deleted file mode 100644 index e6d1e18c..00000000 --- a/src/hooks/useNewerVersion.tsx +++ /dev/null @@ -1,13 +0,0 @@ -import { BuildConfig } from "@Native/BuildConfig"; - -export const useNewerVersion = (ver: VersionType) => { - const oldParts = BuildConfig.VERSION_NAME.split("."); - const newParts = ver.split("."); - for (var i = 0; i < newParts.length; i++) { - const a = ~~newParts[i]; // parse int - const b = ~~oldParts[i]; // parse int - if (a > b) return true; - if (a < b) return false; - } - return false; -}; diff --git a/src/hooks/useOnScreen.ts b/src/hooks/useOnScreen.ts deleted file mode 100644 index 6db09bb7..00000000 --- a/src/hooks/useOnScreen.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { RefObject, useEffect, useMemo, useState } from "react"; - -export function useOnScreen(ref: RefObject) { - const [isIntersecting, setIntersecting] = useState(false); - - const observer = useMemo(() => new IntersectionObserver(([entry]) => setIntersecting(entry.isIntersecting)), [ref]); - - useEffect(() => { - if (ref.current) observer.observe(ref.current); - return () => observer.disconnect(); - }, []); - - return isIntersecting; -} diff --git a/src/hooks/useOpenModuleSearch.tsx b/src/hooks/useOpenModuleSearch.tsx deleted file mode 100644 index aad1b50e..00000000 --- a/src/hooks/useOpenModuleSearch.tsx +++ /dev/null @@ -1,81 +0,0 @@ -import { Activities } from "@Activitys/index"; -import { SearchActivity } from "@Activitys/SearchActivity"; -import { VerifiedIcon } from "@Components/icons/VerifiedIcon"; -import Avatar from "@mui/material/Avatar"; -import Chip from "@mui/material/Chip"; -import ListItemAvatar from "@mui/material/ListItemAvatar"; -import ListItemButton from "@mui/material/ListItemButton"; -import ListItemText from "@mui/material/ListItemText"; -import Stack from "@mui/material/Stack"; -import Typography from "@mui/material/Typography"; -import { SuFile } from "@Native/SuFile"; -import { useActivity } from "./useActivity"; -import { useModFS } from "./useModFS"; - -export function useOpenModuleSearch(list: L) { - const { context } = useActivity(); - const { modFS } = useModFS(); - - return (initialSearch: string = "") => { - context.pushPage({ - component: SearchActivity, - key: "SearchActivity", - props: { - list: list, - initialSearch: initialSearch, - search: { - by: ["id", "name", "author"], - //onEveryWord: true, - caseInsensitive: true, - }, - - renderList(item: Module, index) { - return ( - { - context.pushPage({ - component: Activities.ModuleView, - key: "ModuleViewActivity", - extra: item, - }); - }} - > - - ({ - bgcolor: theme.palette.primary.dark, - boxShadow: "0 -1px 5px rgba(0,0,0,.09), 0 3px 5px rgba(0,0,0,.06), 0 1px 2px rgba(0,0,0,.3), 0 1px 3px rgba(0,0,0,.15)", - borderRadius: "20%", - mr: 1.5, - })} - src={item.icon} - > - {item.name.charAt(0).toUpperCase()} - - - - {item.name} - - - - } - secondary={ - - {item.version} - - {SuFile.exist(modFS("PROPS", { MODID: item.id })) && } - - - } - /> - - ); - }, - }, - }); - }; -} diff --git a/src/hooks/usePagination.ts b/src/hooks/usePagination.ts deleted file mode 100644 index 693b9a3e..00000000 --- a/src/hooks/usePagination.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { useState } from "react"; - -function usePagination = any>(data: T, itemsPerPage: number) { - const [currentPage, setCurrentPage] = useState(1); - const maxPage = Math.ceil(data.length / itemsPerPage); - - function currentData(): T { - const begin = (currentPage - 1) * itemsPerPage; - const end = begin + itemsPerPage; - // @ts-ignore - return data.slice(begin, end); - } - - function next() { - setCurrentPage((currentPage) => Math.min(currentPage + 1, maxPage)); - } - - function prev() { - setCurrentPage((currentPage) => Math.max(currentPage - 1, 1)); - } - - function jump(page: number) { - const pageNumber = Math.max(1, page); - setCurrentPage((currentPage) => Math.min(pageNumber, maxPage)); - } - - return { next, prev, jump, currentData, currentPage, maxPage }; -} - -export { usePagination }; \ No newline at end of file diff --git a/src/hooks/useRepos.tsx b/src/hooks/useRepos.tsx deleted file mode 100644 index fd4d327d..00000000 --- a/src/hooks/useRepos.tsx +++ /dev/null @@ -1,159 +0,0 @@ -import React from "react"; -import { SetValue, useNativeStorage } from "./useNativeStorage"; -import { link } from "googlers-tools"; -import _ from "underscore"; -import { useSettings } from "./useSettings"; -import { os } from "@Native/Os"; -import { useLog } from "./native/useLog"; - -export interface RepoContextActions { - addRepo: (data: AddRepoData) => void; - removeRepo: (data: RemoveRepoData) => void; - setRepoEnabled: (data: SetRepoStateData) => void; - isRepoEnabled: (repo: string) => boolean; -} - -interface RepoContextInterface { - repos: RepoConfig[]; - setRepos: SetValue; - modules: Module[]; - actions: RepoContextActions; -} - -export const RepoContext = React.createContext({ - repos: [], - setRepos: () => {}, - modules: [], - actions: { - addRepo: (data: AddRepoData) => {}, - removeRepo: (data: RemoveRepoData) => {}, - setRepoEnabled: (data: SetRepoStateData) => {}, - isRepoEnabled: (repo: string) => false, - }, -}); - -type AddRepoData = { - url: string; - callback?: (state: RepoConfig[]) => void; - error?: (error: Error) => void; -}; - -type RemoveRepoData = { - id: string; - callback?: (state: RepoConfig[]) => void; -}; - -type SetRepoStateData = { - id: string; - callback?: (state: string[]) => void; -}; - -export const RepoProvider = (props: React.PropsWithChildren) => { - const TAG = "RepoProvider"; - const log = useLog(TAG); - - const [disabledRepos, setDisabledRepos] = useSettings("disabled_repos"); - const [repos, setRepos] = useSettings("repos"); - - const [modules, setModules] = React.useState([]); - - const addRepo = (data: AddRepoData) => { - if (!repos.some((repo) => repo.base_url === data.url)) { - if (repos.length <= 4) { - if (link.validURL(data.url)) { - fetch(`${data.url.slice(-1) != "/" ? data.url + "/" : data.url}json/config.json`) - .then((response) => { - if (response.status == 200) { - return response.json(); - } else { - data.error && data.error(Error("Cannot find given repo link or your link isn't valid")); - } - }) - .then((response) => { - setRepos((prev) => [...prev, response], data.callback); - }) - .catch((e) => (data.callback ? data.callback(e) : log.e(e))); - } else { - os.toast("The given link isn't valid", Toast.LENGTH_SHORT); - } - } else { - os.toast("You can't add more than 5 repos", Toast.LENGTH_SHORT); - } - } else { - os.toast("This repo already exists", Toast.LENGTH_SHORT); - } - }; - - const removeRepo = (data: RemoveRepoData) => { - setRepos((tmp) => { - tmp = tmp.filter((remv) => remv.base_url != data.id); - return tmp; - }, data.callback); - }; - - const setRepoEnabled = (data: SetRepoStateData) => { - setDisabledRepos((prev) => { - if (prev.some((elem) => elem === data.id)) { - return prev.filter((item) => item !== data.id); - } else { - return [...prev, data.id]; - } - }, data.callback); - }; - - const isRepoEnabled = React.useCallback( - (repo: string) => { - return !disabledRepos.includes(repo); - }, - [disabledRepos] - ); - - React.useEffect(() => { - setModules([]); - const fetchData = async () => { - const combinedModulesMap = new Map(); - - for (const repo of repos) { - if (disabledRepos.includes(repo.base_url)) continue; - - try { - const res = await fetch(`${repo.base_url.slice(-1) !== "/" ? repo.base_url + "/" : repo.base_url}json/modules.json`); - if (!res.ok) throw new Error(res.statusText); - - const json: Repo = await res.json(); - - json.modules.forEach((mod) => { - if (!combinedModulesMap.has(mod.id)) { - // Add module with repo source if not already in map - combinedModulesMap.set(mod.id, { ...mod, __mmrl_repo_source: [repo.name] }); - } else { - // If already in map, append source repo name - const existingModule = combinedModulesMap.get(mod.id); - if (!existingModule.__mmrl_repo_source.includes(repo.name)) { - existingModule.__mmrl_repo_source.push(repo.name); - } - } - }); - } catch (error) { - log.e((error as Error).message); - } - } - - // Convert map to array for final combined list - setModules(Array.from(combinedModulesMap.values())); - }; - - void fetchData(); - }, [disabledRepos, repos]); - - const contextValue = React.useMemo( - () => ({ repos, setRepos, modules, actions: { addRepo, removeRepo, setRepoEnabled, isRepoEnabled } }), - [repos, modules] - ); - - return ; -}; - -export const useRepos = () => { - return React.useContext(RepoContext); -}; diff --git a/src/hooks/useScramble.ts b/src/hooks/useScramble.ts deleted file mode 100644 index d86593e6..00000000 --- a/src/hooks/useScramble.ts +++ /dev/null @@ -1,14 +0,0 @@ -import React, { useState, useMemo } from "react"; - -function scrambleString(str: string) { - const arr = str.split(""); - for (let i = arr.length - 1; i > 0; i--) { - const j = Math.floor(Math.random() * (i + 1)); - [arr[i], arr[j]] = [arr[j], arr[i]]; - } - return arr.join(""); -} - -export function useScrambledString(str: string) { - return useMemo(() => scrambleString(str), [str]); -} diff --git a/src/hooks/useSettings.tsx b/src/hooks/useSettings.tsx deleted file mode 100644 index 22edc9b5..00000000 --- a/src/hooks/useSettings.tsx +++ /dev/null @@ -1,87 +0,0 @@ -import { SetValue, useNativeStorage } from "@Hooks/useNativeStorage"; -import { useLanguageMap } from "./../locales/declaration"; -import React from "react"; - -export interface Picker { - name: N; - value: V; -} - -export interface StorageDeclaration { - landingEnabled: boolean; - language: Picker; - eruda_console_enabled: boolean; - disabled_repos: string[]; - _low_quality_module: boolean; - _invald_module: boolean; - term_scroll_bottom: boolean; - term_scroll_behavior: { name: string; value: ScrollBehavior }; - link_protection: boolean; - swipeable_tabs: boolean; - print_terminal_error: boolean; - terminal_word_wrap: boolean; - terminal_numberic_lines: boolean; - repos: RepoConfig[]; -} - -export const termScrollBehaviors: StorageDeclaration["term_scroll_behavior"][] = [ - { - name: "Smooth", - value: "smooth", - }, - { - name: "Instant", - value: "instant" as "smooth", - }, -]; - -export const useSettings = (key: K): [StorageDeclaration[K], SetValue] => { - const availableLangs = useLanguageMap(); - - const INITIAL_SETTINGS = React.useMemo( - () => ({ - landingEnabled: true, - language: availableLangs[0], - eruda_console_enabled: false, - disabled_repos: [], - _low_quality_module: true, - _invald_module: false, - term_scroll_bottom: true, - term_scroll_behavior: termScrollBehaviors[0], - link_protection: true, - swipeable_tabs: false, - print_terminal_error: false, - terminal_word_wrap: true, - terminal_numberic_lines: true, - repos: [ - { - name: "Googlers Magisk Repo", - website: "https://mmrl.dergoogler.com", - support: "https://github.com/Googlers-Repo/gmr/issues", - donate: "https://github.com/sponsors/DerGoogler", - submission: - "https://github.com/Googlers-Repo/gmr/issues/new?assignees=&labels=module&projects=&template=submission.yml&title=%5BModule%5D%3A+", - base_url: "https://gr.dergoogler.com/gmr/", - max_num: 3, - enable_log: true, - log_dir: "log", - }, - { - name: "Magisk Modules Alt Repo", - website: undefined, - support: undefined, - donate: undefined, - submission: - "https://github.com/Magisk-Modules-Alt-Repo/submission/issues/new?assignees=&labels=module&projects=&template=module-submission.yml&tit", - base_url: "https://magisk-modules-alt-repo.github.io/json-v2/", - max_num: 3, - enable_log: true, - log_dir: "log", - }, - ], - }), - [] - ); - - return useNativeStorage(key, INITIAL_SETTINGS[key]); -}; diff --git a/src/hooks/useStateCallback.ts b/src/hooks/useStateCallback.ts deleted file mode 100644 index 272960b5..00000000 --- a/src/hooks/useStateCallback.ts +++ /dev/null @@ -1,33 +0,0 @@ -import React from "react"; -import { useCallback, useEffect, useRef, useState } from "react"; - -export type Dispatch = (value: A, callback?: (state: B) => void) => void; -export type SetStateAction = S | ((prevState: S) => S); - -export function useStateCallback(initialState: S | (() => S)): [S, Dispatch, S>] { - const [state, setState] = useState(initialState); - const cbRef = useRef<((state: S) => void) | undefined>(undefined); // init mutable ref container for callbacks - - const [uniqueState, setUniqueState] = useState(Symbol()); - - const setStateCallback = useCallback((state: SetStateAction, callback?: (state: S) => void) => { - cbRef.current = callback; // store current, passed callback in ref - setState(state); - - // Prevent unnecessary firing of the useEffect if there is no callback to fire - if (callback) { - setUniqueState(Symbol()); - } - }, []); // keep object reference stable, exactly like `useState` - - useEffect(() => { - // cb.current is `undefined` on initial render, - // so we only invoke callback on state *updates* - if (cbRef.current) { - cbRef.current(state); - cbRef.current = undefined; // reset callback after execution - } - }, [state, uniqueState]); - - return [state, setStateCallback]; -} diff --git a/src/hooks/useStrings.tsx b/src/hooks/useStrings.tsx deleted file mode 100644 index ba1dd5a7..00000000 --- a/src/hooks/useStrings.tsx +++ /dev/null @@ -1,107 +0,0 @@ -import * as React from "react"; -import { useLocalStorage } from "usehooks-ts"; -import { SetValue, useNativeStorage } from "./useNativeStorage"; -import { StringDeclaration, Strs, useLanguageMap } from "./../locales/declaration"; -import { useSettings } from "./useSettings"; - -export type ReplacementObject = { - [key: string]: React.ReactNode; -}; - -interface StrContext { - strings>(key: K, fmt?: ReplacementObject): React.ReactNode; - format(template: str, object?: object): React.ReactNode; - readonly currentLanguage: str; -} - -const StringsContext = React.createContext({ - strings>(key: K, fmt?: ReplacementObject): React.ReactNode { - return <>; - }, - format(template: str, object?: object): React.ReactNode { - return ""; - }, - currentLanguage: "en", -}); - -export interface AvailableLangs { - name: str; - value: str; -} - -interface StringsProviderProps extends React.PropsWithChildren { - data: Strs; -} - -/** - * - * @param strings The first element is the default language - */ -export const StringsProvider = (props: StringsProviderProps) => { - const [language] = useSettings("language"); - - const defaultLanguage = Object.keys(props.data)[0]; - const currentLanguage = React.useMemo(() => language.value, [language]); - - // const format = React.useCallback((template: string, object?: object) => { - // return template.replace(/\{(\w+(\.\w+)*)\}/gi, (match, key) => { - // const keys = key.split("."); - // let value = object || {}; - // for (const k of keys) { - // if (k in value) { - // value = value[k]; - // } else { - // return match; - // } - // } - // return format(String(value), object); - // }); - // }, []); - - const format = React.useCallback((str: string, replacement?: ReplacementObject) => { - if (!replacement) { - return str; - } - - const result: React.ReactNode[] = []; - const keys = Object.keys(replacement); - const getRegExp = () => { - const regexp: React.ReactNode[] = []; - keys.forEach((key) => regexp.push(`{${key}}`)); - return new RegExp(regexp.join("|")); - }; - str.split(getRegExp()).forEach((item, i) => { - result.push(item, replacement[keys[i]]); - }); - return result; - }, []); - - const strings = React.useCallback( - (key: string, fmt?: ReplacementObject) => { - const currentLang = props.data[currentLanguage]; - const defaultLang = props.data[defaultLanguage]; - - if (currentLang[key] !== undefined) { - return format(currentLang[key], fmt); - } else if (defaultLang[key] !== undefined) { - return format(defaultLang[key], fmt); - } else { - return ""; - } - }, - [currentLanguage] - ); - - const contextValue = React.useMemo( - () => ({ - strings, - format, - currentLanguage, - }), - [strings, format, currentLanguage, language] - ); - - return ; -}; - -export const useStrings = () => React.useContext(StringsContext); diff --git a/src/hooks/useSupportIconForUrl.tsx b/src/hooks/useSupportIconForUrl.tsx deleted file mode 100644 index b77a828f..00000000 --- a/src/hooks/useSupportIconForUrl.tsx +++ /dev/null @@ -1,47 +0,0 @@ -import SupportIcon from "@mui/icons-material/Support"; -import GitHubIcon from "@mui/icons-material/GitHub"; -import TelegramIcon from "@mui/icons-material/Telegram"; -import { useRef } from "react"; - -interface Support { - SupportIcon: typeof SupportIcon; - supportText: string; -} - -export function useSupportIconForUrl(url: string | undefined) { - const icon = useRef({ - SupportIcon: SupportIcon, - supportText: "Support", - }); - - if (!url) { - return icon.current; - } else if (url.startsWith("https://t.me/")) { - icon.current = { - SupportIcon: SupportIcon, - supportText: "Telegram", - }; - } else if (url.startsWith("https://discord.gg/") || url.startsWith("https://discord.com/invite/")) { - icon.current = { - SupportIcon: SupportIcon, - supportText: "Discord", - }; - } else if (url.startsWith("https://github.com/")) { - icon.current = { - SupportIcon: GitHubIcon, - supportText: "GitHub", - }; - } else if (url.startsWith("https://gitlab.com/")) { - icon.current = { - SupportIcon: SupportIcon, - supportText: "GitLab", - }; - } else if (url.startsWith("https://xdaforums.com/") || url.startsWith("https://forum.xda-developers.com/")) { - icon.current = { - SupportIcon: SupportIcon, - supportText: "XDA Developers", - }; - } - - return icon.current; -} diff --git a/src/hooks/useSupportedRoot.ts b/src/hooks/useSupportedRoot.ts deleted file mode 100644 index 3534db81..00000000 --- a/src/hooks/useSupportedRoot.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { Shell } from "@Native/Shell"; -import React from "react"; -import { valid, satisfies } from "semver"; - -const currentRootVersion = Shell.VERSION_NAME(); -const rootManager = Shell.getRootManagerV2().toLowerCase(); - -const useSupportedRoot = (roots: Module["root"], deps: React.DependencyList): [boolean, string] => { - const cleanSemver = React.useCallback((version: string) => { - return version.replace(/:.*$/, ""); - }, deps); - - const isSupported = React.useMemo(() => { - if (!roots) { - return true; - } - - if (roots[rootManager]) { - const cleanedVer = cleanSemver(currentRootVersion); - if (valid(cleanedVer)) { - return satisfies(cleanedVer, roots[rootManager]); - } - } - - return true; - }, deps); - - return [isSupported, currentRootVersion]; -}; - -export { useSupportedRoot }; diff --git a/src/hooks/useTheme.tsx b/src/hooks/useTheme.tsx deleted file mode 100644 index da1c1be0..00000000 --- a/src/hooks/useTheme.tsx +++ /dev/null @@ -1,207 +0,0 @@ -import React from "react"; -import { useTheme as useMom, createTheme, ThemeProvider as MumProvider } from "@mui/material"; - -export const useTheme = () => { - const theme = useMom(); - return { - theme: theme as MMRLTheme, - }; -}; - -const THIS_IS_THE_THEME_OBJECT_OF_THIS_F_APP = createTheme({ - components: { - MuiListItemText: { - styleOverrides: { - root: { - wordWrap: "break-word", - }, - }, - }, - MuiListSubheader: { - styleOverrides: { - root: ({ theme }) => ({ - backgroundColor: theme.palette.background.default, - }), - }, - }, - MuiSwitch: { - styleOverrides: { - root: ({ theme }) => ({ - "& .MuiSwitch-switchBase": { - color: theme.palette.background.default, - }, - "& .MuiSwitch-switchBase.Mui-checked": { - color: theme.palette.background.default, - }, - "& .MuiSwitch-switchBase.Mui-checked + .MuiSwitch-track": { - opacity: "unset", - backgroundColor: theme.palette.primary.main, - }, - "& .MuiSwitch-switchBase + .MuiSwitch-track": { - backgroundColor: theme.palette.text.secondary, - }, - - padding: 8, - "& .MuiSwitch-track": { - borderRadius: 22 / 2, - "&:before, &:after": { - content: '""', - position: "absolute", - top: "50%", - transform: "translateY(-50%)", - width: 16, - height: 16, - }, - }, - "& .MuiSwitch-thumb": { - boxShadow: "none", - width: 16, - height: 16, - margin: 2, - }, - }), - }, - }, - MuiPopover: { - styleOverrides: { - root: { - "& .MuiBackdrop-root": { - position: "fixed", - display: "flex", - alignItems: "center", - justifyContent: "center", - right: "0", - bottom: "0", - top: "0", - left: "0", - zIndex: "-1", - backdropFilter: "blur(4px)", - backgroundColor: "rgba(0, 0, 0, 0.5)", - }, - }, - }, - }, - MuiDrawer: { - styleOverrides: { - root: { - "& .MuiDrawer-paper": { - backgroundImage: "none", - borderTop: `1px solid #333638`, - }, - "& .MuiModal-backdrop": { - backgroundColor: "rgba(0, 0, 0, 0.5)", - WebkitTapHighlightColor: "transparent", - backdropFilter: "blur(4px)", - }, - }, - }, - }, - MuiDialog: { - styleOverrides: { - root: { - "& .MuiModal-backdrop": { - // position: "fixed", - position: "absolute", - display: "flex", - WebkitBoxAlign: "center", - alignItems: "center", - WebkitBoxPack: "center", - justifyContent: "center", - inset: "0px", - backgroundColor: "rgba(0, 0, 0, 0.5)", - WebkitTapHighlightColor: "transparent", - zIndex: "-1", - backdropFilter: "blur(4px)", - }, - "& .MuiDialog-paper": { - backgroundColor: "#101010", - border: `1px solid #333638`, - backgroundImage: "none", - }, - "& .MuiDialogContent-root": { - borderTop: "none", - borderBottom: "none", - }, - "& .MuiButtonBase-root": { - color: "#f3f5f7", - }, - }, - }, - }, - MuiCard: { - defaultProps: { - elevation: 0, - }, - }, - MuiTabs: { - styleOverrides: { - root: { - "& .MuiButtonBase-root": { - borderBottom: "1px solid #f3f5f726", - }, - "& .MuiTabs-indicator": { - height: 1, - }, - }, - }, - }, - MuiButton: { - styleOverrides: { - root: ({ theme, ownerState }) => ({ - ...(ownerState.variant === "outlined" && { - color: "white", - border: `1px solid ${theme.palette.divider}`, - boxShadow: "none", - }), - ...(ownerState.variant === "contained" && { - color: "black", - ":disabled": { - cursor: "not-allowed", - color: "black", - opacity: ".3", - backgroundColor: "#ffffff", - }, - ":hover": { - cursor: "pointer", - backgroundColor: "#999999", - }, - }), - }), - }, - defaultProps: { - disableElevation: true, - }, - }, - }, - shape: { - borderRadius: 8, - }, - palette: { - mode: "dark", - primary: { - header: "#101010d9", - main: "#ffffff", - dark: "#353535", - }, - secondary: { - main: "#ffffff", - light: "#0a0a0a", - dark: "#1e1e1e", - }, - background: { - paper: "#181818", - default: "#101010", - }, - text: { - link: "#0095F6", - primary: "#f3f5f7", - secondary: "#777777", - }, - divider: "#f3f5f726", - menuoutline: "#333638", - }, -} as unknown as MMRLTheme); - -export const ThemeProvider = (props: React.PropsWithChildren) => ( - -); diff --git a/src/index.tsx b/src/index.tsx deleted file mode 100644 index 5a51ec7d..00000000 --- a/src/index.tsx +++ /dev/null @@ -1,124 +0,0 @@ -import React from "react"; -import ons from "onsenui"; -import { Button, CssBaseline, Divider } from "@mui/material"; -import { LightTheme } from "@Styles/light_theme"; -import { ConfirmProvider } from "material-ui-confirm"; -import { ThemeProvider, useTheme } from "@Hooks/useTheme"; -import { StringsProvider } from "@Hooks/useStrings"; -import { Preventer, render } from "react-render-tools"; -import { TransitionGroup } from "react-transition-group"; -import { MainActivity } from "@Activitys/MainActivity"; -import { RepoProvider } from "@Hooks/useRepos"; - -import { ModFSProvider } from "@Hooks/useModFS"; - -import { MMRLApp } from "./custom-elements/app"; -import { MMRLAnchor } from "./custom-elements/anchor"; - -import "@Styles/onsenui.scss"; -import "@Styles/default.scss"; -import { strs } from "./locales/declaration"; -import { ErrorBoundary } from "@Components/ErrorBoundary"; -import { Page } from "@Components/onsenui/Page"; -import { Toolbar } from "@Components/onsenui/Toolbar"; -import { Pre } from "@Components/dapi/Pre"; -import { Code } from "@Components/dapi/Code"; -import { Anchor } from "@Components/dapi/Anchor"; - -ons.platform.select("android"); - -/** - * This component is non translatable - * ThemeProvider won't get catched - */ -const Fallback = React.memo((props: any) => { - const { theme } = useTheme(); - - const style = { - backgroundColor: theme.palette.background.paper, - color: theme.palette.text.primary, - borderRadius: theme.shape.borderRadius / theme.shape.borderRadius, - lineHeight: 1.45, - overflow: "auto", - padding: 2, - }; - - return ( - { - return ( - - Global App Error - - ); - }} - > - -
-          
-            {props.error.message}
-            
- - If this problem persists, please reach out -
- to our{" "} - - GitHub issues - {" "} - or visit our{" "} - - FAQ page - - . -
-
- - - -
-          {props.errorInfo.componentStack}
-        
-
-
- ); -}); - -import { configureSingle } from "@zenfs/core"; -import { IndexedDB } from "@zenfs/dom"; -await configureSingle({ backend: IndexedDB }); - -ons.ready(() => { - customElements.define("mmrl-app", MMRLApp); - customElements.define("mmrl-anchor", MMRLAnchor); - - render( - - - - - - { - return ; - }} - > - - - - - - - - - - - - - - - , - "mmrl-app" - ); -}); diff --git a/src/locales/antifeatures/az.ts b/src/locales/antifeatures/az.ts deleted file mode 100644 index 748e48aa..00000000 --- a/src/locales/antifeatures/az.ts +++ /dev/null @@ -1,62 +0,0 @@ -export const en_antifeatures = [ - { - id: "Ads", - name: "Reklam", - desc: "Bu modul reklamlar ehtiva edir.", - }, - { - id: "KnownVuln", - name: "Məlum Təhlükəsizlik Açığı", - desc: "Bu modul məlum bir təhlükəsizlik açığı ehtiva edir", - }, - { - id: "NSFW", - name: "İÜTD", - desc: "Bu modul ictimailəşdirilməməli və hər yerdə görünməməli olan məzmunu ehtiva edir", - }, - { - id: "NoSourceSince", - name: "Əvvəlcədən Olmayan Mənbə", - desc: "Mənbə kodu artıq mövcud deyil, yeniləmələr mümkün deyil.", - }, - { - id: "NonFreeAdd", - name: "Qeyri-Azad Əlavələr", - desc: "Bu modul azad olmayan əlavələri təşviq edir", - }, - { - id: "NonFreeAssets", - name: "Qeyri-Azad Varlıqlar", - desc: "Bu modul azad olmayan varlıqları ehtiva edir", - }, - { - id: "NonFreeDep", - name: "Qeyri-Azad Asılılıqlar", - desc: "Bu modul digər azad olmayan modullardan asılıdır", - }, - { - id: "NonFreeNet", - name: "Qeyri-Azad Şəbəkə Xidmətləri", - desc: "Bu modul dəyişdirilə bilməyən və ya azad olmayan şəbəkə xidmətini təşviq edir və ya ondan tamamilə asılıdır", - }, - { - id: "Tracking", - name: "İzləmə", - desc: "Bu modul fəaliyyətinizi izləyir və hesabat verir", - }, - { - id: "UpstreamNonFree", - name: "Qeyri-Azad yuxarı axın", - desc: "Bu yuxarı axın mənbə kodu tamamilə azad deyil", - }, - { - id: "Obfuscation", - name: "Çaşqınlıq", - desc: "Bu modulun mənbə kodu anlaşılmaz ola bilər, bu, başa düşməyi, yoxlamağı və ya dəyişdirilməsini çətinləşdirir.", - }, - { - id: "UnaskedRemoval", - name: "Tələbsiz Silinmə", - desc: "Modul istifadəçi razılığı olmadan proqramları, faylları və ya digər modulları silir.", - }, -]; diff --git a/src/locales/antifeatures/en.ts b/src/locales/antifeatures/en.ts deleted file mode 100644 index ff789cf9..00000000 --- a/src/locales/antifeatures/en.ts +++ /dev/null @@ -1,62 +0,0 @@ -export const en_antifeatures = [ - { - id: "Ads", - name: "Advertising", - desc: "The module contains advertisements.", - }, - { - id: "KnownVuln", - name: "Known Vulnerability", - desc: "This module contains a known security vulnerability", - }, - { - id: "NSFW", - name: "NSFW", - desc: "This module contains content that should not be publicized or visible everywhere", - }, - { - id: "NoSourceSince", - name: "No Source Since", - desc: "The source code is no longer available, no updates possible.", - }, - { - id: "NonFreeAdd", - name: "Non-Free Addons", - desc: "This module promotes non-free add-ons", - }, - { - id: "NonFreeAssets", - name: "Non-Free Assets", - desc: "This module contains non-free assets", - }, - { - id: "NonFreeDep", - name: "Non-Free Dependencies", - desc: "This module depends on other non-free modules", - }, - { - id: "NonFreeNet", - name: "Non-Free Network Services", - desc: "This module promotes or depends entirely on a non-changeable or non-free network service", - }, - { - id: "Tracking", - name: "Tracking", - desc: "This module tracks and reports your activity", - }, - { - id: "UpstreamNonFree", - name: "Upstream Non-Free", - desc: "The upstream source code is not entirely Free", - }, - { - id: "Obfuscation", - name: "Obfuscation", - desc: "The module source code is may obfuscated, making it difficult to understand, audit, or modify.", - }, - { - id: "UnaskedRemoval", - name: "Unasked Removal", - desc: "The module removes apps, files, or other modules without user consent.", - }, -]; diff --git a/src/locales/antifeatures/ja.ts b/src/locales/antifeatures/ja.ts deleted file mode 100644 index 4a3528c7..00000000 --- a/src/locales/antifeatures/ja.ts +++ /dev/null @@ -1,62 +0,0 @@ -export const en_antifeatures = [ - { - id: "Ads", - name: "広告", - desc: "モジュールには広告が含まれています。", - }, - { - id: "KnownVuln", - name: "既知の脆弱性", - desc: "このモジュールには、既知の脆弱性が含まれています。", - }, - { - id: "NSFW", - name: "NSFW", - desc: "このモジュールは、一般的に公開すべきではないコンテンツが含まれています。", - }, - { - id: "NoSourceSince", - name: "ソースが不明", - desc: "ソースコードが利用不能であり、更新もできません。", - }, - { - id: "NonFreeAdd", - name: "無料ではないアドオン", - desc: "このモジュールは、無料ではないアドオンを宣伝します。", - }, - { - id: "NonFreeAssets", - name: "無料ではないアセット", - desc: "このモジュールは、無料ではないアセットが含まれています。", - }, - { - id: "NonFreeDep", - name: "無料ではない依存関係", - desc: "このモジュールは、無料ではない依存関係が含まれています。", - }, - { - id: "NonFreeNet", - name: "無料ではないネットワークサービス", - desc: "このモジュールは、変更不可能または無料ではないネットワークサービスを推進または、完全な依存をしています。", - }, - { - id: "Tracking", - name: "追跡", - desc: "このモジュールは、アクティビティを追跡して報告します。", - }, - { - id: "UpstreamNonFree", - name: "無料ではないアップストリーム", - desc: "アップストリームのソースコードは完全に無料ではありません。", - }, - { - id: "Obfuscation", - name: "難読化", - desc: "モジュールのソースコードが難読化されている可能性があります。ソースの内容を理解、監査、変更が困難になります。", - }, - { - id: "UnaskedRemoval", - name: "同意されていない削除", - desc: "このモジュールは、ユーザーの同意なしにアプリ、ファイル、その他のモジュールを削除します。", - }, -]; diff --git a/src/locales/antifeatures/pt.ts b/src/locales/antifeatures/pt.ts deleted file mode 100644 index c95bcfb7..00000000 --- a/src/locales/antifeatures/pt.ts +++ /dev/null @@ -1,62 +0,0 @@ -export const pt_antifeatures = [ - { - id: "Ads", - name: "Publicidade", - desc: "O módulo contém anúncios.", - }, - { - id: "KnownVuln", - name: "Vulnerabilidade Conhecida", - desc: "Este módulo contém uma vulnerabilidade de segurança conhecida.", - }, - { - id: "NSFW", - name: "NSFW", - desc: "Este módulo contém conteúdo que não deve ser publicizado ou visível em todos os lugares.", - }, - { - id: "NoSourceSince", - name: "Sem Código-Fonte Desde", - desc: "O código-fonte não está mais disponível, impossibilitando atualizações.", - }, - { - id: "NonFreeAdd", - name: "Add-ons Não Livres", - desc: "Este módulo promove add-ons não livres.", - }, - { - id: "NonFreeAssets", - name: "Recursos Não Livres", - desc: "Este módulo contém recursos não livres.", - }, - { - id: "NonFreeDep", - name: "Dependências Não Livres", - desc: "Este módulo depende de outros módulos não livres.", - }, - { - id: "NonFreeNet", - name: "Serviços de Rede Não Livres", - desc: "Este módulo promove ou depende inteiramente de um serviço de rede não livre ou não modificável.", - }, - { - id: "Tracking", - name: "Rastreamento", - desc: "Este módulo rastreia e reporta sua atividade.", - }, - { - id: "UpstreamNonFree", - name: "Código-Fonte Upstream Não Livre", - desc: "O código-fonte upstream não é totalmente livre.", - }, - { - id: "Obfuscation", - name: "Ofuscação", - desc: "O código-fonte do módulo pode estar ofuscado, dificultando a compreensão, auditoria ou modificação.", - }, - { - id: "UnaskedRemoval", - name: "Remoção Sem Solicitação", - desc: "O módulo remove aplicativos, arquivos ou outros módulos sem o consentimento do usuário.", - }, -]; diff --git a/src/locales/az-AZ.ts b/src/locales/az-AZ.ts deleted file mode 100644 index 89036e6b..00000000 --- a/src/locales/az-AZ.ts +++ /dev/null @@ -1,144 +0,0 @@ -/** - * Credits due file rename: GitHub @RashadGasimli - * First PR: https://github.com/DerGoogler/MMRL/pull/199 - */ -export const az_AZ = { - continue: "Davam et", - caution: "Diqqət", - latest: "Ən sonuncu", - security: "Təhlükəsizlik", - changelog: "Dəyişiklik jurnalı", - new: "Yeni", - search: "Axtarış", - updates: "Güncəlləmələr", - versions: "Versiyalar", - licenses: "Lisenziyalar", - license: "Lisenziya", - search_modules: "Axtarış modulları", - settings: "Parametrlər", - repository: "Depo", - repositories: "Depolar", - appearance: "Xarici görünüş", - accent_color: "Vurğu rəngi", - language: "Dil", - dark_theme: "Qaranlıq tema", - bottom_navigation_text: "Alt naviqasiya", - bottom_navigation_subtext: "Tab vərəqləri ekranın aşağısına köçürür.", - not_supported_in_web_version: "Bu veb versiyasında dəstəklənmit.", - source_code: "Mənbə kodu", - acknowledgements: "Təsdiqlər", - issues: "Xətalar", - download: "Yüklə", - install: "Quraşdır", - update: "Güncəllə", - explore: "Kəşf et", - installed: "Quraşdırılıb", - remove: "Sil", - restore: "Bərpa edin", - module_enabled_LOG: "{name} aktivləşdirilib", - module_disabled_LOG: "{name} deaktivləşdirilib", - add: "Əlavə et", - cancel: "Ləğv et", - confirm_repo_delete: "{name} anbarı silmək istədiyinizdən əminsinizmi?", - submit_module: "Bir modul göndərin", - donate: "İanə verin", - support: "Dəstəkləyin", - website: "Vebsayt", - no_root: "Kök girişi yoxdur", - failed: "Uğursuz oldu", - no_root_message: "Zəhmət olmasa ən azı bir kök (root) meneceri istifadə etdiyinzdən əmin olun, əks halda MMRL tətbiqindən istifadə edə bilməzsiniz.", - open_magisk: "Magisk-i açın", - development: "İnkişaf etdirin", - enabled: "Aktivləşdirildi", - comments: "Şərhlər", - configureable: "konfiqurasiya edilə bilər", - change_boot: "Açılışı dəyişir", - need_ramdisk: "Ramdisk lazımdır", - add_repository: "Anbar əlavə edin", - add_repository_description: "Sizin yaxud başqa bir yerdən anbar əlavə edin.", - explore_repositories: "Anbarları kəşf edin", - overview: "Ümumi baxış", - about_this_module: "Bu modul haqqında", - about: "Haqqında", - updated_on: "Güncəlləndi", - requirements: "Tələblər", - access: "Keçid", - minimum: "Minimum", - recommended: "Tövsiyə edilir", - source: "Mənbə", - require_sdk: "Modul {sdk} tələb edir", - unsupported: "Dəstəklənmir", - images: "Şəkillər", - unset: "Təyin etməyin", - yes: "Bəli", - no: "Xeyr", - operating_sys: "Əməliyyat sistemi", - verified_module: "Təsdiqlənmiş modul", - verified_module_desc: - "Bu modul yoxlamadan keçmiş və nüfuzlu tərtibatçı tərəfindən hazırlanmış təhlükəsiz modul kimi təsdiq edilmişdir.", - update_json: "Öz update.json istifadə edir", - update_json_desc: "Bu modul yeniləmə və quraşdırma məqsədilə öz update.json faylını istifadə edir.", - shading: "Kölgə salma", - shading_title: "Xüsusi kölgə salma tətbiq edin", - shading_desc: "Diqqətli istifadə edin, əgər çox qaranlıq olarsa, İstifadəçi İnterfeysini görə bilməyəcəksiniz.", - module: "Modul", - swipeable_tabs: "Sürüşdürülə bilən tab vərəqlər", - swipeable_tabs_subtitle: "Bütün tab vərəqləri sürüşdürülə bilən edin. Bu, istifadəçi təcrübəsini pozur.", - low_quality_module: "Aşağı keyfiyyətli modul", - low_quality_modules: "Aşağı keyfiyyətli modullar", - low_quality_modules_subtitle: "Əgər modulun keyfiyyəti aşağıdırsa, modulun altında xəbərdarlıq göstərin", - low_quality_module_warn: - "Bu Magisk modulunda onun funksionallığına və mənşəyinə təsir edə biləcək id, versiya, versiya kodu, müəllif və s. kimi mühüm xüsusiyyətlər yoxdur.", - invaild_modules: "Etibarsız modullar", - invaild_modules_subtitle: "Etibarsız modulları göstər", - // no translate - modconf: "ModConf", - modconf_playground: "ModConf önplanı", - // no translate - modfs: "ModFS", - enable_install: "Quraşdırmanı aktivləşdirin", - enable_install_subtitle: "1.8.5 versiyadan etibarən MMRL Quraşdırma Alətləri tələb olunur", - scroll_to_bottom: "Aşağı sürüşdürün", - scroll_to_bottom_subtitle: "Terminalda avtomatik olaraq aşağıya sürüşdürün", - scroll_behavior: "Sürüşdürmə davranışı", - terminal: "Terminal", - eruda_console: "Eruda konsol", - eruda_console_subtitle: "İnkişaf və xətaların ovlanması üçün faydalıdır", - share_device_infos: "Cihaz məlumatlarını paylaşın", - share_device_infos_subtilte: "Cihaz xüsusiyyətləri, konfiqurasiya edilmiş ModConf, proqram məlumatları və quraşdırılmış modullar", - storage: "Yaddaş", - clear_repos: "Anbarları təmizləyin", - patch_settings: "Yamaq paramterləri", - patch_settings_subtitle: "Çatışmayan parametr açarlarını əlavə edin", - sticky_search_bar: "Yapışqan axtarış çubuğunu deaktiv edin", - dm_update_json_fetch_warn: "{id} boş „updateJson“ xüsusiyyətinə malikdir və ya keçid etibarlı deyil", - - // Anti-Features - antifeature: "Anti-Xüsusiyyət", - antifeatures: "Anti-Xüsusiyyətlər", - - // Anchor link confirm - anchor_confirm_title: "MMRL-dən çıxılır", - anchor_confirm_desc: "Bu keçid sizi aşağıdakı vebsayta aparır {codeblock}", - link_protection_title: "Keçid qorunması", - link_protection_desc: "Əlavə təsdiq dialoqu ilə təsadüfən kliklənən keçidin qarşısını alın. Bu parametr ModConf-a da təsir edir.", - - // modconf - compile_error: "Tərtib xətası!", - - unverified_host: "Doğrulanmamış host!", - unverified_host_text: - "MMRL-ə doğrulanmış bir host olmayan {url} ünvanından daxil olursunuz. MMRL-ni üçüncü tərəf mənbələrindən deyil, yalnız orijinal mənbədən istifadə edin.", - unverified_host_text_help: "Hər hansı bir problem gördünüz və ya əldə edilmiş hostun istifadə üçün təhlükəsiz olduğunu bilirsiniz? Sonra bunu {issues} altında bildirin", - - // terminal activity - reboot_device: "Cihaz yenidən başladılsın?", - reboot_device_desc: "Cihazınızı yenidən başlatmağa əminsiniz?", - privacy_privacy: "Məxfilik Məxfilik", - hoc_with_require_new_version: "Bu konfiqurasiya {versionCode} üzərində MMRL tələb edir (versionCode){br}Ən sonuncu {url} yoxlayın", - - print_errors: "Xətaları çap edin", - print_errors_desc: "Terminal və MMRL Quraşdırma Alətləri xətasını konsola çap edir", - - blacklisted_modules: "Qara siyahıya alınmış modullar", -}; diff --git a/src/locales/de.ts b/src/locales/de.ts deleted file mode 100644 index b68e2c2b..00000000 --- a/src/locales/de.ts +++ /dev/null @@ -1,99 +0,0 @@ -export const de = { - changelog: "Changelog", - new: "Neu", - search: "Suche", - updates: "Updates", - versions: "Versionen", - licenses: "Lizenzen", - license: "Lizenz", - search_modules: "Module durchsuchen", - settings: "Einstellungen", - repository: "Repository", - repositories: "Repositorys", - accent_color: "Akzentfarbe", - appearance: "Aussehen", - language: "Sprache", - dark_theme: "Dunkles Thema", - bottom_navigation_text: "Navigation unten", - bottom_navigation_subtext: "Bewegt Tabs an den unteren Bildschirmrand.", - not_supported_in_web_version: "In der Web-Version nicht unterstuetzt", - source_code: "Quellcode", - acknowledgements: "Danksagungen/Lizenzen", - issues: "Issues", - module_verified: "Dieses Modul ist verifiziert und vertrauenswürdig", - download: "Herunterladen", - install: "Installieren", - explore: "Erkunden", - installed: "Installiert", - remove: "Entfernen", - restore: "Wiederherstellen", - module_enabled_LOG: "{name} wurde aktiviert", - module_disabled_LOG: "{name} wurde deaktiviert", - add: "Hinzufügen", - cancel: "Abbrechen", - confirm_repo_delete: "Möchten Sie das {name} Repository wirklich entfernen?", - submit_module: "Ein Module einreichen", - donate: "Spenden", - support: "Support", - website: "Webseite", - no_root: "Kein Root", - failed: "Fehlgeschlagen", - no_root_message: - "Bitte stellen Sie sicher, dass Sie über mindestens einen Root-Manager verfügen, andernfalls können Sie MMRL nicht verwenden.", - open_magisk: "Magisk öffnen", - development: "Entwicklung", - enabled: "Aktiviert", - comments: "Kommentare", - verified: "Verifiziert", - configureable: "Anpassbar", - change_boot: "Ändert Boot", - need_ramdisk: "Braucht Ramdisk", - add_repository: "Repository hinzufügen", - add_repository_description: "Fügen Sie Ihr Repository oder ein Repository von einem anderen hinzu.", - explore_repositories: "Entdecke Repositorys", - overview: "Übersicht", - about_this_module: "Über dieses Modul", - about: "Über", - updated_on: "Aktualisiert am", - requirements: "Anforderungen", - access: "Zugriffe", - minimum: "Minimum", - recommended: "Empfohlen", - source: "Quelle", - require_sdk: "Modul erfordert {sdk}", - unsupported: "Nicht unterstützt", - images: "Bilder", - unset: "Unbestimmt", - yes: "Ja", - no: "Nein", - operating_sys: "Betriebssystem", - verified_module: "Verifiziertes Modul", - verified_module_desc: - "Dieses Modul wurde einer Überprüfung unterzogen und wurde als vertrauenswürdiges Modul bestätigt, das von einem seriösen Entwickler entwickelt wurde.", - update_json: "Verwendet eigene Update.json", - update_json_desc: "Dieses Modul verwendet seine eigene update.json für Aktualisierungs- und Installationszwecke.", - shading: "Schattierung", - shading_title: "Benutzerdefinierte Schattierung anwenden", - shading_desc: "Seien Sie vorsichtig, wenn es zu dunkel ist, können Sie die Benutzeroberfläche möglicherweise nicht mehr sehen.", - module: "Modul", - low_quality_module: "Modul von geringer Qualität", - low_quality_modules: "Module von geringer Qualität", - low_quality_modules_subtitle: "Zeigt eine Warnung unterhalb des Moduls an, wenn es eine niedrige Qualität hat", - low_quality_module_warn: - "Diesem Magisk-Modul fehlen wichtige Eigenschaften wie ID, Version, Versionscode, Autor usw., was sich auf seine Funktionalität und Herkunft auswirken kann.", - invaild_modules: "Ungültige Module", - invaild_modules_subtitle: "Ungültige Module anzeigen", - enable_install: "Installation aktivieren", - scroll_to_bottom: "Nach unten scrollen", - scroll_to_bottom_subtitle: "Automatisches Scrollen nach unten im Terminal", - scroll_behavior: "Scrollverhalten", - terminal: "Terminal", - eruda_console: "Eruda Konsole", - eruda_console_subtitle: "Nützlich bei der Entwicklung und Fehlersuche", - share_device_infos: "Informationen über das Gerät teilen", - storage: "Speicher", - clear_repos: "Repositorys löschen", - patch_settings: "Einstellungen patchen", - patch_settings_subtitle: "Fehlende Einstellungsschlüssel hinzufügen", - dm_update_json_fetch_warn: "{id} hat eine leere Eigenschaft „updateJson“ oder der Link ist ungültig", -}; diff --git a/src/locales/declaration.ts b/src/locales/declaration.ts deleted file mode 100644 index 1f050e23..00000000 --- a/src/locales/declaration.ts +++ /dev/null @@ -1,44 +0,0 @@ -import React from "react"; -import { AvailableLangs, useStrings } from "@Hooks/useStrings"; -import { de } from "./de"; -import { en } from "./en"; -import { zh } from "./zh"; -import { pt_BR } from "./pt-BR"; -import { az_AZ } from "./az-AZ"; -import { ja } from "./ja"; - -/** - * English is the default language - */ -export type StringDeclaration = keyof typeof en; - -export type AvailableStrs = "en" | "de" | "zh" | "pt-BR" | "az-AZ" | "ja"; -export type Strs = { - [code in AvailableStrs]: Partial>; -}; - -export const strs: Strs = { - en: en, - de: de, - zh: zh, - "pt-BR": pt_BR, - "az-AZ": az_AZ, - ja: ja, -}; - -export const useLanguageMap = (): arr => { - const { currentLanguage } = useStrings(); - - const regionNames = React.useMemo( - () => - new Intl.DisplayNames([currentLanguage], { - type: "language", - }), - [currentLanguage] - ); - - return Object.keys(strs).map((key) => ({ - name: regionNames.of(key) || "Unknown", - value: key, - })); -}; diff --git a/src/locales/en.ts b/src/locales/en.ts deleted file mode 100644 index 935a06bd..00000000 --- a/src/locales/en.ts +++ /dev/null @@ -1,174 +0,0 @@ -export const en = { - continue: "Continue", - caution: "Caution", - latest: "Latest", - security: "Security", - changelog: "Changelog", - new: "New", - search: "Search", - updates: "Updates", - versions: "Versions", - licenses: "Licenses", - license: "License", - search_modules: "Search modules", - settings: "Settings", - repository: "Repository", - repositories: "Repositories", - appearance: "Appearance", - accent_color: "Accent color", - language: "Language", - dark_theme: "Dark theme", - bottom_navigation_text: "Bottom navigation", - bottom_navigation_subtext: "Moves tabs to the bottom of screen.", - not_supported_in_web_version: "Not supported in web version", - source_code: "Source code", - acknowledgements: "Acknowledgements", - issues: "Issues", - download: "Download", - install: "Install", - update: "Update", - explore: "Explore", - installed: "Installed", - remove: "Remove", - restore: "Restore", - module_enabled_LOG: "{name} has been enabled", - module_disabled_LOG: "{name} has been disabled", - add: "Add", - cancel: "Cancel", - confirm_repo_delete: "Are you sure to remove {name} repository?", - submit_module: "Submit a module", - donate: "Donate", - support: "Support", - website: "Website", - no_root: "No Root", - failed: "Failed", - no_root_message: "Please make sure that you have at least one root manager, otherwise you can't use MMRL.", - open_magisk: "Open Magisk", - development: "Development", - enabled: "Enabled", - comments: "Comments", - configureable: "Configureable", - change_boot: "Changes boot", - need_ramdisk: "Needs Ramdisk", - add_repository: "Add Repository", - add_repository_description: "Add your repository or an repository from some else.", - explore_repositories: "Explore Repositories", - overview: "Overview", - about_this_module: "About this module", - about: "About", - updated_on: "Updated on", - requirements: "Requirements", - access: "Access", - minimum: "Minimum", - recommended: "Recommended", - source: "Source", - require_sdk: "Module requires {sdk}", - unsupported: "Unsupported", - images: "Images", - unset: "Unset", - yes: "Yes", - no: "No", - operating_sys: "Operating System", - verified_module: "Verified module", - verified_module_desc: - "This module has undergone verification and has been confirmed as a trusted module developed by a reputable developer.", - update_json: "Uses own update.json", - update_json_desc: "This module utilizes its own update.json for updating and installation purposes.", - shading: "Shading", - shading_title: "Apply custom shading", - shading_desc: "Use with care, if to dark you may not able to see the UI anymore.", - module: "Module", - swipeable_tabs: "Swipeable tabs", - swipeable_tabs_subtitle: "Make all tabs swipeable. This make break user experience", - low_quality_module: "Low quality module", - low_quality_modules: "Low quality modules", - low_quality_modules_subtitle: "Shows a alert below the module if it has a low quality", - low_quality_module_warn: - "This Magisk module is missing crucial properties, such as id, version, versionCode, author, etc., which may affect its functionality and origin.", - invaild_modules: "Invaild modules", - invaild_modules_subtitle: "Show invaild modules", - // no translate - modconf: "ModConf", - modconf_playground: "ModConf Playground", - // no translate - modfs: "ModFS", - enable_install: "Enable install", - enable_install_subtitle: "Since 1.8.5 the MMRL Install Tools are required", - scroll_to_bottom: "Scroll to bottom", - scroll_to_bottom_subtitle: "Automatically scroll to bottom within the terminal", - scroll_behavior: "Scroll behavior", - terminal: "Terminal", - eruda_console: "Eruda console", - eruda_console_subtitle: "Useful for development and bug hunting", - share_device_infos: "Share device information's", - share_device_infos_subtilte: "Device specs, configured ModConf, app infos and installed modules", - storage: "Storage", - clear_repos: "Clear repositories", - patch_settings: "Patch settings", - patch_settings_subtitle: "Add missing settings keys", - sticky_search_bar: "Disable sticky search bar", - dm_update_json_fetch_warn: "{id} has empty „updateJson“ property or the link isn't valid", - - // Anti-Features - antifeature: "Anti-Feature", - antifeatures: "Anti-Features", - - // Anchor link confirm - anchor_confirm_title: "Leaving MMRL", - anchor_confirm_desc: "This link is taking you the following website {codeblock}", - link_protection_title: "Link protection", - link_protection_desc: "Prevent link that are accidentally clicked with a extra confirm dialog. This setting also affects ModConf.", - - // modconf - compile_error: "Compile error!", - - unverified_host: "Unverified host!", - unverified_host_text: - "You're accessing MMRL from {url} which isn't a verified host. Only use MMRL from it's origial source and not from thrid-party sources.", - unverified_host_text_help: "Noticed any issues or you know that the accessed host safe for use is? Then report it under our {issues}", - - // terminal activity - reboot_device: "Reboot device?", - reboot_device_desc: "Are you sure to reboot your device?", - word_wrap_title: "Word wrap", - numberic_lines_title: "Numberic lines", - numberic_lines_desc: "Adds a number to every line in the installer", - - privacy_privacy: "Privacy Policy", - hoc_with_require_new_version: "This config requires MMRL above {versionCode} (versionCode){br}Check the latest {url}", - - print_errors: "Print errors", - print_errors_desc: "Prints terminal and MMRL Install Tools error to the console", - - blacklisted_modules: "Blacklisted modules", - - install_module: "Install {name}?", - install_module_dialog_desc: "Are you sure that you what to install {name}?", - - exit_app: "Exit app?", - exit_app_desc: "Are you sure that you want to leave the app?", - - module_require_android_ver: "This module requires {andro_ver}. It may doesn't work for your device.", - - we_hit_a_brick: "We hit a brick!", - open_settings: "Open settings", - open_logcat: "Open Logcat", - try_again: "Try again", - - file_downloaded: "File has been downloaded to {path}", - - features: "Features", - feature: "Feature", - verified: "Verified", - last_updated: "Last updated: {date}", - telegram_channel: "Telegram channel", - install_queue: "Install queue", - install_queue_notice: "The queue will be deleted when you close the app", - install_queue_empty: "Your queue is empty. Add some modules!", - alr_add_queue: "This module has been already added to your queue", - add_t_queue: "Module has been added to your queue", - start_mod_ini_queue: "Start install queue?", - start_mod_ini_queue_desc: "Are you sure that you want to start this queue?", - unsupported_root: - "This module doesn't supports your current root version! Detected {manager} with version {version}. Install at your own risk!", -}; diff --git a/src/locales/ja.ts b/src/locales/ja.ts deleted file mode 100644 index 0a95dd6c..00000000 --- a/src/locales/ja.ts +++ /dev/null @@ -1,172 +0,0 @@ -export const ja = { - continue: "続行", - caution: "注意", - latest: "最新", - security: "セキュリティ", - changelog: "更新履歴", - new: "新着", - search: "検索", - updates: "更新", - versions: "バージョン", - licenses: "ライセンス", - license: "ライセンス", - search_modules: "モジュールを検索", - settings: "設定", - repository: "リポジトリ", - repositories: "リポジトリ", - appearance: "外観", - accent_color: "アクセントカラー", - language: "言語", - dark_theme: "ダークテーマ", - bottom_navigation_text: "ボトムナビゲーション", - bottom_navigation_subtext: "タブを画面の下部に移動します。", - not_supported_in_web_version: "Web 版ではサポートされていません", - source_code: "ソースコード", - acknowledgements: "謝辞", - issues: "問題", - download: "ダウンロード", - install: "インストール", - update: "更新", - explore: "探す", - installed: "インストール済み", - remove: "削除", - restore: "復元", - module_enabled_LOG: "{name} は有効化されています。", - module_disabled_LOG: "{name} は無効化されています。", - add: "追加", - cancel: "キャンセル", - confirm_repo_delete: "{name} のリポジトリを削除してもよろしいですか?", - submit_module: "モジュールを送信", - donate: "寄付", - support: "サポート", - website: "Web サイト", - no_root: "root 権限なし", - failed: "失敗", - no_root_message: "少なくとも 1 個の root マネージャーがあることを確認してください。そうでない場合、MMRL は使用できません。", - open_magisk: "Magisk を開く", - development: "開発", - enabled: "有効化", - comments: "コメント", - configureable: "設定可能", - change_boot: "boot の変更", - need_ramdisk: "Ramdisk が必須", - add_repository: "リポジトリを追加", - add_repository_description: "自分のリポジトリまたは、他のリポジトリを追加します。", - explore_repositories: "リポジトリを探す", - overview: "概要", - about_this_module: "このモジュールについて", - about: "情報", - updated_on: "更新日", - requirements: "要件", - access: "アクセス", - minimum: "最小", - recommended: "おすすめ", - source: "ソース", - require_sdk: "モジュールには {sdk} が必要です。", - unsupported: "非対応", - images: "画像", - unset: "設定を解除", - yes: "はい", - no: "いいえ", - operating_sys: "オペレーティングシステム", - verified_module: "検証済みのモジュール", - verified_module_desc: - "このモジュールは検証済みであり、評判の良い開発者によって開発された信頼できるモジュールであることが確認されています。", - update_json: "独自の update.json を使用する", - update_json_desc: "このモジュールは更新とインストールの目的でのみ、独自の update.json を使用します。", - shading: "シェーディング", - shading_title: "カスタムシェーディングを適用", - shading_desc: "注意して使用をしてください。暗すぎると UI 見えなくなる可能性があります。", - module: "モジュール", - swipeable_tabs: "スワイプ可能なタブ", - swipeable_tabs_subtitle: "すべてのタブをスワイプ可能にします。これにより、ユーザーエクスペリエンスが損なわれます。", - low_quality_module: "低品質なモジュール", - low_quality_modules: "低品質なモジュール", - low_quality_modules_subtitle: "モジュールの品質が低い場合、モジュールの下に警告が表示されます。", - low_quality_module_warn: - "この Magisk モジュールは id、version、versionCode、author などの重要なプロパティが欠落しており、機能や起源に影響する可能性があります。", - invaild_modules: "無効なモジュール", - invaild_modules_subtitle: "無効なモジュールを表示", - // no translate - modconf: "ModConf", - modconf_playground: "ModConf Playground", - // no translate - modfs: "ModFS", - enable_install: "インストールを有効化", - enable_install_subtitle: "バージョン 1.8.5 以降では MMRL Install Tools が必要です。", - scroll_to_bottom: "下までスクロール", - scroll_to_bottom_subtitle: "ターミナル内で自動的に下までスクロールします。", - scroll_behavior: "スクロールの動作", - terminal: "ターミナル", - eruda_console: "Eruda コンソール", - eruda_console_subtitle: "開発やバグハンティングに役立ちます。", - share_device_infos: "デバイス情報を共有する", - share_device_infos_subtilte: "デバイスの仕様、構成された ModConf、アプリ情報、インストールされたモジュールを共有します。", - storage: "ストレージ", - clear_repos: "リポジトリを消去", - patch_settings: "パッチの設定", - patch_settings_subtitle: "不足している設定キーを追加", - sticky_search_bar: "固定検索バーを無効化する", - dm_update_json_fetch_warn: "{id} 内の update.json プロパティが空であるか、リンクが無効です。", - - // Anti-Features - antifeature: "アンチ機能", - antifeatures: "アンチ機能", - - // Anchor link confirm - anchor_confirm_title: "MMRL を離れる", - anchor_confirm_desc: "このリンクは次の Web サイトに移動します。{codeblock}", - link_protection_title: "リンクの保護", - link_protection_desc: "追加の確認ダイアログを表示して、誤ってクリックされるリンクを防止します。この設定は ModConf にも影響します。", - - // modconf - compile_error: "コンパイルエラー!", - - unverified_host: "未確認のホストです!", - unverified_host_text: - "検証されていないホストである {url} から MMRL にアクセスしています。サードパーティーのソースではなく、元のソースからでのみ MMRL を使用してください。", - unverified_host_text_help: "何かの問題に気づいた場合や、アクセスしたホストが安全に使用できるかどうかご存知の場合は、 {issues} で報告してください。", - - // terminal activity - reboot_device: "デバイスを再起動しますか?", - reboot_device_desc: "デバイスを再起動してもよろしいですか?", - word_wrap_title: "文章の折り返し", - numberic_lines_title: "行の番号", - numberic_lines_desc: "インストーラーの各行に番号を追加します。", - - privacy_privacy: "プライバシーポリシー", - hoc_with_require_new_version: "この構成には、{versionCode} 以上の MMRL が必要です。 (versionCode){br}最新の {url} を確認してください。", - - print_errors: "プリントエラー", - print_errors_desc: "ターミナルと MMRL Install Tools のエラーをコンソールに出力します。", - - blacklisted_modules: "ブラックリストされたモジュール", - - install_module: "{name} をインストールしますか?", - install_module_dialog_desc: "{name} をインストールしてもよろしいですか?", - - exit_app: "アプリを終了しますか?", - exit_app_desc: "アプリを終了してもよろしいですか?", - - module_require_android_ver: "このモジュールには {andro_ver} が必要です。使用しているデバイスでは動作しない可能性があります。", - - we_hit_a_brick: "失敗しました!", - open_settings: "設定を開く", - open_logcat: "Logcat を開く", - try_again: "再試行", - - file_downloaded: "ファイルは {path} にダウンロードされました。", - - features: "機能", - feature: "機能", - verified: "検証済み", - last_updated: "最終更新: {date}", - telegram_channel: "Telegram チャンネル", - install_queue: "インストールのキュー", - install_queue_notice: "アプリを閉じるとキューは削除されます", - install_queue_empty: "キューは空です。モジュールを追加してください!", - alr_add_queue: "このモジュールはキューに追加されています", - add_t_queue: "モジュールがキューに追加されました", - start_mod_ini_queue: "インストールを開始しますか?", - start_mod_ini_queue_desc: "キューに追加したモジュールをインストールしますか?", -}; diff --git a/src/locales/pt-BR.ts b/src/locales/pt-BR.ts deleted file mode 100644 index 0937230f..00000000 --- a/src/locales/pt-BR.ts +++ /dev/null @@ -1,144 +0,0 @@ -/** - * Credits due file rename: GitHub @Vaz15k - * First PR: https://github.com/DerGoogler/MMRL/pull/176 - */ -export const pt_BR = { - continue: "Continuar", - caution: "Cuidado", - latest: "Mais recente", - security: "Segurança", - changelog: "Changelog", - new: "Novo", - search: "Buscar", - updates: "Atualizações", - versions: "Versões", - licenses: "Licença", - license: "Licenças", - search_modules: "Pesquisar Módulos", - settings: "Configurações", - repository: "Repositório", - repositories: "Repositorios", - appearance: "Aparência", - accent_color: "Cor de Destaque", - language: "Idioma", - dark_theme: "Tema Escuro", - bottom_navigation_text: "Botão de Navegação", - bottom_navigation_subtext: "Move as guias para a parte inferior da tela.", - not_supported_in_web_version: "Não suportado na versão WEB", - source_code: "Código Fonte", - acknowledgements: "Reconhecimentos", - issues: "Problemas", - download: "Download", - install: "Instalar", - update: "Atualizar", - explore: "Explorar", - installed: "Instalado", - remove: "Remover", - restore: "Restaurar", - module_enabled_LOG: "{name} está ativo", - module_disabled_LOG: "{name} esta desativado", - add: "Adicionar", - cancel: "Cancelar", - confirm_repo_delete: "Tem certeza de que deseja remover o repositório {name}?", - submit_module: "Submeter Módulo", - donate: "Doação", - support: "Suporte", - website: "Website", - no_root: "Não Root", - failed: "Falhou", - no_root_message: "Certifique-se de ter pelo menos um gerenciador root, caso contrário, você não poderá usar o MMRL", - open_magisk: "Abrir Magisk", - development: "Desenvolvimento", - enabled: "Ativado", - comments: "Comentários", - configureable: "Configurável", - change_boot: "Altera a inicialização", - need_ramdisk: "Necessita de Ramdisk", - add_repository: "Adicionar Repositório", - add_repository_description: "Adicione seu repositório ou um repositório de outro.", - explore_repositories: "Explorar Repositórios", - overview: "Visão Geral", - about_this_module: "Sobre este módulo", - about: "Sobre", - updated_on: "Atualizado em", - requirements: "Requisitos", - access: "Acesso", - minimum: "Mínimo", - recommended: "Recomendado", - source: "Fonte", - require_sdk: "Módulo requer {sdk}", - unsupported: "Não Suportado", - images: "Imagen", - unset: "Desativar", - yes: "Sim", - no: "Não", - operating_sys: "Sistema Operacional", - verified_module: "Módulo Verificado", - verified_module_desc: - "Este módulo passou por verificação e foi confirmado como um módulo confiável desenvolvido por um desenvolvedor confiável.", - update_json: "Usa o próprio update.json", - update_json_desc: "Este módulo utiliza seu próprio update.json para fins de atualização e instalação.", - shading: "Sombreamento", - shading_title: "Aplicar sombreamento personalizado", - shading_desc: "Use com cuidado, se estiver muito escuro, você não conseguirá mais ver a IU.", - module: "Módulo", - swipeable_tabs: "Guias deslizantes", - swipeable_tabs_subtitle: "Torne todas as guias deslizáveis. Isso quebra a experiência do usuário", - low_quality_module: "Módulo de baixa qualidade", - low_quality_modules: "Módulos de baixa qualidade", - low_quality_modules_subtitle: "Mostra um alerta abaixo do módulo se ele estiver com baixa qualidade", - low_quality_module_warn: - "Este módulo Magisk não possui propriedades cruciais, como id, versão, versionCode, autor, etc., o que pode afetar sua funcionalidade e origem.", - invaild_modules: "Módulos inválidos", - invaild_modules_subtitle: "Mostrar módulos inválidos", - // no translate - modconf: "ModConf", - modconf_playground: "ModConf Playground", - // no translate - modfs: "ModFS", - enable_install: "Habilitar instalação", - enable_install_subtitle: "Desde 1.8.5 'MMRL Install Tools' são necessárias", - scroll_to_bottom: "Role para baixo", - scroll_to_bottom_subtitle: "Rolar automaticamente para baixo no terminal", - scroll_behavior: "Comportamento de rolagem", - terminal: "Terminal", - eruda_console: "Eruda console", - eruda_console_subtitle: "Útil para desenvolvimento e caça a bugs", - share_device_infos: "Compartilhe informações do dispositivo", - share_device_infos_subtilte: "Especificações do dispositivo, ModConf configurado, informações do aplicativo e módulos instalados", - storage: "Armazenamento", - clear_repos: "Limpar repositórios", - patch_settings: "Configurações de patches", - patch_settings_subtitle: "Adicionar chaves de configurações ausentes", - sticky_search_bar: "Desativar barra de pesquisa fixa", - dm_update_json_fetch_warn: "{id} tem a propriedade “updateJson” vazia ou link não válido", - - // Anti-Features - antifeature: "Anti-Feature", - antifeatures: "Anti-Features", - - // Anchor link confirm - anchor_confirm_title: "Saindo MMRL", - anchor_confirm_desc: "Este link leva você ao site {codeblock}", - link_protection_title: "Proteção de Links", - link_protection_desc: "Evite links clicados acidentalmente com uma caixa de diálogo de confirmação extra. Esta configuração também afeta o ModConf.", - - // modconf - compile_error: "Erro de compilação!", - - unverified_host: "Host não verificado!", - unverified_host_text: - "Você está acessando o MMRL de {url}, que não é um host verificado. Use MMRL apenas de sua fonte original e não de fontes de terceiros.", - unverified_host_text_help: "Notou algum problema ou sabe que o host acessado é seguro para uso? Em seguida, relate-o em nossos {issues}", - - // terminal activity - reboot_device: "Reiniciar dispositivo?", - reboot_device_desc: "Tem certeza de reiniciar seu dispositivo?", - privacy_privacy: "Privacidade Privacidade", - hoc_with_require_new_version: "Esta configuração requer MMRL acima de {versionCode} (versionCode){br}Verifique o {url} mais recente", - - print_errors: "Mostrar Erros", - print_errors_desc: "Mostra os erros de terminal e MMRL Install Tools no console", - - blacklisted_modules: "Módulos na lista negra", -}; diff --git a/src/locales/zh.ts b/src/locales/zh.ts deleted file mode 100644 index 08efe03c..00000000 --- a/src/locales/zh.ts +++ /dev/null @@ -1,90 +0,0 @@ -export const zh = { - search_modules: "模块搜索", - settings: "设置", - repository: "仓库", - repositories: "仓库", - appearance: "外观", - accent_color: "强调颜色", - language: "语言", - dark_theme: "暗黑主题", - bottom_navigation_text: "底部导航", - bottom_navigation_subtext: "将选项卡移动到屏幕底部。", - not_supported_in_web_version: "Web 版本不支持", - source_code: "源代码", - acknowledgements: "鸣谢", - issues: "问题", - download: "下载", - install: "安装", - update: "更新", - explore: "浏览", - installed: "已安装", - remove: "移除", - restore: "恢复", - module_enabled_LOG: "{name} 已启用", - module_disabled_LOG: "{name} 已禁用", - add: "添加", - cancel: "取消", - confirm_repo_delete: "确定要移除 {name} 仓库吗?", - submit_module: "提交模块", - donate: "捐赠", - support: "支持", - website: "网站", - no_root: "无 Root 权限", - failed: "失败", - no_root_message: "请确保您至少有一个 root 管理器,否则无法使用 MMRL。", - open_magisk: "打开 Magisk", - development: "开发", - enabled: "已启用", - comments: "评论", - configureable: "可配置", - change_boot: "更改引导", - need_ramdisk: "需要 Ramdisk", - add_repository: "添加仓库", - add_repository_description: "添加您自己的仓库或其他来源的仓库。", - explore_repositories: "浏览仓库", - overview: "概览", - about_this_module: "关于此模块", - about: "关于", - updated_on: "更新于", - requirements: "要求", - access: "访问", - minimum: "最低要求", - recommended: "推荐", - source: "来源", - require_sdk: "模块要求 {sdk}", - unsupported: "不支持", - images: "图片", - unset: "未设置", - yes: "是", - no: "否", - operating_sys: "操作系统", - verified_module: "已验证模块", - verified_module_desc: "此模块经过验证,已确认为由值得信赖的开发人员开发的受信任模块。", - update_json: "使用自带的 update.json", - update_json_desc: "此模块使用自带的 update.json 进行更新和安装。", - shading: "着色", - shading_title: "应用自定义着色", - shading_desc: "小心使用,如果太暗可能无法看到 UI。", - module: "模块", - low_quality_module: "低质量模块", - low_quality_modules: "低质量模块", - low_quality_modules_subtitle: "如果模块质量低,则显示警报", - low_quality_module_warn: "此 Magisk 模块缺少关键属性,如 id、version、versionCode、author 等,可能会影响其功能和来源。", - invaild_modules: "无效模块", - invaild_modules_subtitle: "显示无效模块", - enable_install: "启用安装", - enable_install_subtitle: "从 1.8.5 版本开始,MMRL 安装工具是必需的", - scroll_to_bottom: "滚动到底部", - scroll_to_bottom_subtitle: "在终端中自动滚动到底部", - scroll_behavior: "滚动行为", - terminal: "终端", - eruda_console: "Eruda 控制台", - eruda_console_subtitle: "用于开发和查找错误", - share_device_infos: "共享设备信息", - share_device_infos_subtilte: "设备规格、配置的 ModConf、应用信息和已安装的模块", - storage: "存储", - clear_repos: "清除仓库", - patch_settings: "修补设置", - patch_settings_subtitle: "添加丢失的设置键", - sticky_search_bar: "禁用粘性搜索栏", -}; diff --git a/src/native/Build.ts b/src/native/Build.ts deleted file mode 100644 index 3ee67b94..00000000 --- a/src/native/Build.ts +++ /dev/null @@ -1,146 +0,0 @@ -class Build { - public static VERSION_CODES = class { - public static BASE = 1; - - public static BASE_1_1 = 2; - - public static CUPCAKE = 3; - - public static DONUT = 4; - - public static ECLAIR = 5; - - public static ECLAIR_0_1 = 6; - - public static ECLAIR_MR1 = 7; - - public static FROYO = 8; - - public static GINGERBREAD = 9; - - public static GINGERBREAD_MR1 = 10; - - public static HONEYCOMB = 11; - - public static HONEYCOMB_MR1 = 12; - - public static HONEYCOMB_MR2 = 13; - - public static ICE_CREAM_SANDWICH = 14; - - public static ICE_CREAM_SANDWICH_MR1 = 15; - - public static JELLY_BEAN = 16; - - public static JELLY_BEAN_MR1 = 17; - - public static JELLY_BEAN_MR2 = 18; - - public static KITKAT = 19; - - public static KITKAT_WATCH = 20; - - public static L = 21; - - public static LOLLIPOP = 21; - - public static LOLLIPOP_MR1 = 22; - - public static M = 23; - - public static N = 24; - - public static N_MR1 = 25; - - public static O = 26; - - public static O_MR1 = 27; - - public static P = 28; - - public static Q = 29; - - public static R = 30; - - public static S = 31; - - public static S_V2 = 32; - - public static TIRAMISU = 33; - }; - - public static parseVersion(version: number) { - switch (version) { - case this.VERSION_CODES.BASE: - return "Android 1.0"; - case this.VERSION_CODES.BASE_1_1: - return "Android 1.1 (Petit Four)"; - case this.VERSION_CODES.CUPCAKE: - return "Android 1.5 (Cupcake)"; - case this.VERSION_CODES.DONUT: - return "Android 1.6 (Donut)"; - case this.VERSION_CODES.ECLAIR: - return "Android 2.0 (Eclair)"; - case this.VERSION_CODES.ECLAIR_0_1: - return "Android 2.0.1 (Eclair)"; - case this.VERSION_CODES.ECLAIR_MR1: - return "Android 2.1 (Eclair)"; - case this.VERSION_CODES.FROYO: - return "Android 2.2 (Froyo)"; - case this.VERSION_CODES.GINGERBREAD: - return "Android 2.3.0 - 2.3.2 (Gingerbread)"; - case this.VERSION_CODES.GINGERBREAD_MR1: - return "Android 2.3.3 - 2.3.7 (Gingerbread)"; - case this.VERSION_CODES.HONEYCOMB: - return "Android 3.0 (Honeycomb)"; - case this.VERSION_CODES.HONEYCOMB_MR1: - return "Android 3.1 (Honeycomb)"; - case this.VERSION_CODES.HONEYCOMB_MR2: - return "Android 3.2 (Honeycomb)"; - case this.VERSION_CODES.ICE_CREAM_SANDWICH: - return "Android 4.0.1 - 4.0.2 (Ice Cream Sandwich)"; - case this.VERSION_CODES.ICE_CREAM_SANDWICH_MR1: - return "Android 4.0.3 - 4.0.4 (Ice Cream Sandwich)"; - case this.VERSION_CODES.JELLY_BEAN: - return "Android 4.1 (Jelly Bean)"; - case this.VERSION_CODES.JELLY_BEAN_MR1: - return "Android 4.2 (Jelly Bean)"; - case this.VERSION_CODES.JELLY_BEAN_MR2: - return "Android 4.3 (Jelly Bean)"; - case this.VERSION_CODES.KITKAT: - return "Android 4.4 (KikKat)"; - case this.VERSION_CODES.KITKAT_WATCH: - return "Android 4.4 (KitKat)"; - case this.VERSION_CODES.LOLLIPOP: - return "Android 5.0 (Lollipop)"; - case this.VERSION_CODES.LOLLIPOP_MR1: - return "Android 5.1 (Lollipop)"; - case this.VERSION_CODES.M: - return "Android 6.0 (Marshmallow)"; - case this.VERSION_CODES.N: - return "Android 7.0 (Nougat)"; - case this.VERSION_CODES.N_MR1: - return "Android 7.1 (Nougat)"; - case this.VERSION_CODES.O: - return "Android 8.0 (Oreo)"; - case this.VERSION_CODES.O_MR1: - return "Android 8.1 (Oreo)"; - case this.VERSION_CODES.P: - return "Android 9.0 (Pie)"; - case this.VERSION_CODES.Q: - return "Android 10 (Quince Tart)"; - case this.VERSION_CODES.R: - return "Android 11 (Red Velvet Cake)"; - case this.VERSION_CODES.S: - return "Android 12 (Snow Cone)"; - case this.VERSION_CODES.S_V2: - return "Android 12L (Snow Cone)"; - case this.VERSION_CODES.TIRAMISU: - return "Android 12 (Tiramisu)"; - default: - return "Sdk: " + version; - } - } -} - -export { Build }; diff --git a/src/native/BuildConfig.ts b/src/native/BuildConfig.ts deleted file mode 100644 index 141884ad..00000000 --- a/src/native/BuildConfig.ts +++ /dev/null @@ -1,61 +0,0 @@ -import pkg from "@Package"; -import { Native } from "./Native"; - -/** - * BuildConfigs for Android - */ -class BuildConfigClass extends Native { - public constructor() { - super(window.__buildconfig__); - } - - public get BUILD_DATE(): number { - if (this.isAndroid) { - return this.interface.BUILD_DATE(); - } else { - return WEB_BUILD_DATE; - } - } - - public get VERSION_NAME(): VersionType { - if (this.isAndroid) { - return this.interface.VERSION_NAME(); - } else { - return pkg.config.version_name as VersionType; - } - } - - public get VERSION_CODE(): number { - if (this.isAndroid) { - return this.interface.VERSION_CODE(); - } else { - return pkg.config.version_code; - } - } - - public get APPLICATION_ID(): string { - if (this.isAndroid) { - return this.interface.APPLICATION_ID(); - } else { - return pkg.name; - } - } - public get DEBUG(): boolean { - if (this.isAndroid) { - return this.interface.DEBUG(); - } else { - return __webpack__mode__ === "development"; - } - } - - public get BUILD_TYPE(): string { - if (this.isAndroid) { - return this.interface.BUILD_TYPE(); - } else { - return __webpack__mode__; - } - } -} - -const BuildConfig: BuildConfigClass = new BuildConfigClass(); -export { BuildConfig, BuildConfigClass }; diff --git a/src/native/Chooser.ts b/src/native/Chooser.ts deleted file mode 100644 index 7045c630..00000000 --- a/src/native/Chooser.ts +++ /dev/null @@ -1,93 +0,0 @@ -import { Native } from "./Native"; - -type SuccessCallback = (file: (File | string)[] | "RESULT_CANCELED") => void; -type ErrorCallback = ((code: number) => void) | null; - -interface ChooserNative { - getFile(type: string, allowMulti: boolean, successCallback: SuccessCallback, errorCallback: ErrorCallback): void; -} - -class Chooser extends Native { - public type: string; - private _onChose: SuccessCallback | undefined; - private _onError: ErrorCallback = null; - private _allowMultiChoose = false; - // @ts-ignore - private _inputElement: HTMLInputElement; - - public constructor(type: string) { - super(window.__chooser__); - - if (typeof type !== "string") throw new TypeError("Chooser plugin only accepts 'string' as type"); - - this.type = type; - - if (!this.isAndroid) { - this._inputElement = document.createElement("input"); - } - } - - public set allowMultiChoose(value: boolean) { - if (typeof value !== "boolean") return; - this._allowMultiChoose = value; - } - - public set onChose(func: SuccessCallback) { - this._onChose = func; - } - - public set onError(func: ErrorCallback) { - this._onError = func; - } - - public static isSuccess(arg: (File | string)[] | "RESULT_CANCELED") { - return arg !== "RESULT_CANCELED"; - } - - private async _blobToBase64(blob: Blob): Promise { - return new Promise((resolve, reject) => { - const reader = new FileReader(); - // @ts-ignore - reader.onloadend = () => resolve(reader.result?.split(",")[1]); - reader.onerror = reject; - reader.readAsDataURL(blob); - }); - } - - public async getFiles() { - if (this.isAndroid) { - if (typeof this._onChose !== "function") throw new TypeError("Chooser 'onChose' is not a function"); - - this.interface.getFile(this.type, this._allowMultiChoose, this._onChose, this._onError); - } else { - this._inputElement.type = "file"; - this._inputElement.accept = this.type; - this._inputElement.multiple = this._allowMultiChoose; - - // Handle file selection - this._inputElement.onchange = async (event) => { - const files = this._inputElement.files; - if (files && files.length > 0) { - const fileArray: (File | string)[] = []; - - for (let i = 0; i < files.length; i++) { - fileArray.push(files[i]); - } - - if (typeof this._onChose === "function") { - this._onChose(fileArray); - } - } else { - if (typeof this._onChose === "function") { - this._onChose("RESULT_CANCELED"); - } - } - }; - - // Trigger the file input dialog - this._inputElement.click(); - } - } -} - -export { Chooser }; diff --git a/src/native/Download.ts b/src/native/Download.ts deleted file mode 100644 index 14de8205..00000000 --- a/src/native/Download.ts +++ /dev/null @@ -1,53 +0,0 @@ -import { Native } from "./Native"; -import { os } from "./Os"; - -type DownloadState = { type: "downloading"; state: number } | { type: "finished"; state: null }; - -interface DownloadStart { - url: string; - dest: string; - onChange: ((s: DownloadState) => void) | undefined; - onError?: ((err: string) => void) | null; -} - -interface DownloadNative { - start(options: DownloadStart): void; -} - -class Download extends Native { - private _onError: ((err: string) => void) | null | undefined; - private _onChange: ((s: DownloadState) => void) | undefined; - private _dest: string; - private _url: string; - - public constructor(url: string, dest: string) { - super(window.__download__); - this._url = url; - this._dest = dest; - } - - public set onChange(func: DownloadStart["onChange"]) { - this._onChange = func; - } - - public set onError(func: DownloadStart["onError"]) { - this._onError = func; - } - - public start(): void { - if (this.isAndroid) { - if (typeof this._onChange !== "function") throw new TypeError("Download 'onChange' is not a function"); - - this.interface.start({ - url: this._url, - dest: this._dest, - onChange: this._onChange, - onError: this._onError, - }); - } else { - os.openURL(this._url, "_blank"); - } - } -} - -export { Download }; diff --git a/src/native/Environment.ts b/src/native/Environment.ts deleted file mode 100644 index 0622e970..00000000 --- a/src/native/Environment.ts +++ /dev/null @@ -1,61 +0,0 @@ -import { Native } from "./Native"; - -export interface NativeEnvironment { - getExternalStorageDir(): string; - getPackageDataDir(): string; - getPublicDir(type: string): string; - getDataDir(): string; -} - -/** - * @implements {NativeEnvironment} - */ -class EnvironmentClass extends Native { - public constructor() { - super(window.__environment__); - } - - public readonly DIRECTORY_MUSIC: string = "Music"; - public readonly DIRECTORY_PODCASTS: string = "Podcasts"; - public readonly DIRECTORY_RINGTONES: string = "Ringtones"; - public readonly DIRECTORY_ALARMS: string = "Alarms"; - public readonly DIRECTORY_NOTIFICATIONS: string = "Notifications"; - public readonly DIRECTORY_PICTURES: string = "Pictures"; - public readonly DIRECTORY_MOVIES: string = "Movies"; - public readonly DIRECTORY_DOWNLOADS: string = "Download"; - public readonly DIRECTORY_DCIM: string = "DCIM"; - public readonly DIRECTORY_DOCUMENTS: string = "Documents"; - public readonly DIRECTORY_SCREENSHOTS: string = "Screenshots"; - public readonly DIRECTORY_AUDIOBOOKS: string = "Audiobooks"; - public readonly DIRECTORY_RECORDINGS: string = "Recordings"; - - public getExternalStorageDir(): string { - if (this.isAndroid) { - return this.interface.getExternalStorageDir(); - } - return ""; - } - - public getPackageDataDir(): string { - if (this.isAndroid) { - return this.interface.getPackageDataDir(); - } - return ""; - } - - public getPublicDir(type: string): string { - if (this.isAndroid) { - return this.interface.getPublicDir(type); - } - return ""; - } - - public getDataDir(): string { - if (this.isAndroid) { - return this.interface.getDataDir(); - } - return ""; - } -} - -export const Environment: EnvironmentClass = new EnvironmentClass(); diff --git a/src/native/IsolatedEval/IsoAudio.ts b/src/native/IsolatedEval/IsoAudio.ts deleted file mode 100644 index 0c10297a..00000000 --- a/src/native/IsolatedEval/IsoAudio.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { SuFile } from "@Native/SuFile"; -import { IsolatedEvalError } from "./IsolatedEvalError"; - -class IsoAudio extends Audio { - public type: string; - public autoplay: boolean = false; - public controls: boolean = false; - - public constructor(src?: string, type: string = "audio/wav") { - if (typeof src !== "string") throw new IsolatedEvalError("Source is not a string in Audio class"); - super(src); - this.type = type; - - const file = new SuFile(src); - - if (file.exist()) { - this.src = `data:${this.type};base64,${file.readAsBase64()}`; - } - } - - public isPlaying() { - return !this.paused; - } -} - -export { IsoAudio }; diff --git a/src/native/IsolatedEval/IsoDOMParser.ts b/src/native/IsolatedEval/IsoDOMParser.ts deleted file mode 100644 index 897ede32..00000000 --- a/src/native/IsolatedEval/IsoDOMParser.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { SuFile } from "@Native/SuFile"; -import { IsolatedEvalError } from "./IsolatedEvalError"; - -class IsoDOMParser extends DOMParser { - public constructor() { - super(); - } - - public parseFromFile(fileName: string, type: DOMParserSupportedType = "application/xml"): Document { - if (typeof fileName !== "string") throw new IsolatedEvalError("'fileName' isn't a string"); - const file = new SuFile(fileName); - - if (file.exist()) { - return this.parseFromString(file.read(), type); - } else { - throw new IsolatedEvalError(`Unable to find '${fileName}'`); - } - } -} - -export { IsoDOMParser }; diff --git a/src/native/IsolatedEval/IsoDocument.ts b/src/native/IsolatedEval/IsoDocument.ts deleted file mode 100644 index ca9f12dc..00000000 --- a/src/native/IsolatedEval/IsoDocument.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { IsolatedFunctionBlockError } from "./IsolatedFunctionBlockError"; - -class IsoDocument extends Document { - public constructor() { - super(); - } - - public write(...text: string[]): void { - throw new IsolatedFunctionBlockError("document.write()"); - } - - public writeln(...text: string[]): void { - throw new IsolatedFunctionBlockError("document.writeln()"); - } -} - -export { IsoDocument }; diff --git a/src/native/IsolatedEval/IsoXMLSerializer.ts b/src/native/IsolatedEval/IsoXMLSerializer.ts deleted file mode 100644 index b61247d9..00000000 --- a/src/native/IsolatedEval/IsoXMLSerializer.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { SuFile } from "@Native/SuFile"; -import { IsolatedEvalError } from "./IsolatedEvalError"; - -class IsoXMLSerializer extends XMLSerializer { - public constructor() { - super(); - } - - public serializeToFile(fileName: string, root: Node): void { - if (typeof fileName !== "string") throw new IsolatedEvalError("'fileName' isn't a string"); - - const file = new SuFile(fileName); - - const xml = this.serializeToString(root); - - if (typeof xml === "string") { - file.write(xml); - } - } -} - -export { IsoXMLSerializer }; diff --git a/src/native/IsolatedEval/IsolatedEvalError.ts b/src/native/IsolatedEval/IsolatedEvalError.ts deleted file mode 100644 index 62630eb9..00000000 --- a/src/native/IsolatedEval/IsolatedEvalError.ts +++ /dev/null @@ -1,8 +0,0 @@ -class IsolatedEvalError extends Error { - constructor(message?: string) { - super(message); - this.name = "IsolatedEvalError"; - } -} - -export { IsolatedEvalError }; diff --git a/src/native/IsolatedEval/IsolatedFunctionBlockError.ts b/src/native/IsolatedEval/IsolatedFunctionBlockError.ts deleted file mode 100644 index 3e912a86..00000000 --- a/src/native/IsolatedEval/IsolatedFunctionBlockError.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { IsolatedEvalError } from "./IsolatedEvalError"; - -class IsolatedFunctionBlockError extends IsolatedEvalError { - constructor(message?: string) { - message = `${message} has been blacklisted`; - super(message); - this.name = "IsolatedFunctionBlockError"; - } -} - -export { IsolatedFunctionBlockError }; diff --git a/src/native/IsolatedEval/index.ts b/src/native/IsolatedEval/index.ts deleted file mode 100644 index 3f8ae48c..00000000 --- a/src/native/IsolatedEval/index.ts +++ /dev/null @@ -1,352 +0,0 @@ -import { Chooser } from "@Native/Chooser"; -import { SuZip } from "@Native/SuZip"; -import { Terminal } from "@Native/Terminal"; -import { Path } from "@Util/path.js"; -import { transform } from "@babel/standalone"; -import Sandbox from "@nyariv/sandboxjs"; -import { IScope } from "@nyariv/sandboxjs/dist/node/executor"; -import ini from "ini"; -import React from "react"; -import yaml from "yaml"; -import { Build } from "../Build"; -import { BuildConfig, BuildConfigClass } from "../BuildConfig"; -import { Native } from "../Native"; -import { OsClass, os } from "../Os"; -import { Shell } from "../Shell"; -import { SuFile } from "../SuFile"; -import { View, view } from "../View"; -import { IsoAudio } from "./IsoAudio"; -import { IsoDOMParser } from "./IsoDOMParser"; -import { IsoDocument } from "./IsoDocument"; -import { IsoXMLSerializer } from "./IsoXMLSerializer"; -import { IsolatedEvalError } from "./IsolatedEvalError"; -import { IsolatedFunctionBlockError } from "./IsolatedFunctionBlockError"; -import { InternalReact } from "@Activitys/ModConfActivity/components/ModConfView/libs"; -import ModFS from "modfs"; - -import YAML from "yaml"; -import INI from "ini"; - -type IsoModule = { - exports: { - default?: any; - __esModule?: boolean; - [x: string]: any; - }; -}; - -interface IsolatedEvalOptions { - libraries: Record; - indexFile: string; - cwd: string; - scope: IScope; - standaloneFile?: string; -} - -class IsolatedEval { - private readonly _sandbox: Sandbox = new Sandbox(); - private readonly _globals = { - ...Sandbox.SAFE_GLOBALS, - JSON: JSON, - YAML: YAML, - INI: INI, - console: console, - document: new IsoDocument(), - Toast: Toast, - Object: Object, - Document: IsoDocument, - Response: Response, - Element: Element, - Audio: IsoAudio, - HTMLMediaElement: HTMLMediaElement, - HTMLButtonElement: HTMLButtonElement, - HTMLTextAreaElement: HTMLTextAreaElement, - HTMLInputElement: HTMLInputElement, - DragEvent: DragEvent, - InputEvent: InputEvent, - KeyboardEvent: KeyboardEvent, - MouseEvent: MouseEvent, - PointerEvent: PointerEvent, - TouchEvent: TouchEvent, - TransitionEvent: TransitionEvent, - UIEvent: UIEvent, - WheelEvent: WheelEvent, - FileReader: FileReader, - Blob: Blob, - Event: Event, - ToggleEvent: ToggleEvent, - EventTarget: EventTarget, - NamedNodeMap: NamedNodeMap, - DOMParser: IsoDOMParser, - XMLSerializer: IsoXMLSerializer, - SuFile: SuFile, - SuZip: SuZip, - Terminal: Terminal, - Chooser: Chooser, - Shell: Shell, - view: view, - os: os, - BuildConfig: BuildConfig, - Build: Build, - ModFS: ModFS, - Native: Native, - React: InternalReact, - setInterval: setInterval, - clearInterval: clearInterval, - clearTimeout: clearTimeout, - setTimeout: setTimeout, - FormData: FormData, - import: async (whatever: string) => { - if (!os.isAndroid) { - return await import(/* webpackIgnore: true */ whatever); - } - return undefined; - }, - Promise: Promise, - PromiseRejectionEvent: PromiseRejectionEvent, - DataTransfer: DataTransfer, - DataTransferItem: DataTransferItem, - DataTransferItemList: DataTransferItemList, - eval() { - throw new IsolatedFunctionBlockError("eval()"); - }, - atob() { - throw new IsolatedFunctionBlockError("atob()"); - }, - btoa() { - throw new IsolatedFunctionBlockError("btoa()"); - }, - encodeURI() { - throw new IsolatedFunctionBlockError("encodeURI()"); - }, - encodeURIComponent() { - throw new IsolatedFunctionBlockError("encodeURIComponent()"); - }, - decodeURI() { - throw new IsolatedFunctionBlockError("decodeURI()"); - }, - decodeURIComponent() { - throw new IsolatedFunctionBlockError("decodeURIComponent()"); - }, - }; - private readonly _prototypeWhitelist = Sandbox.SAFE_PROTOTYPES; - public moduleCache: {}; - public module: IsoModule; - public path: Path; - public libraries: Record; - public indexFile: string; - public scope: IScope; - public standaloneFile: string | undefined; - - public constructor(options: IsolatedEvalOptions) { - this._prototypeWhitelist.set(Node, new Set()); - this._prototypeWhitelist.set(Object, new Set()); - this._prototypeWhitelist.set(Document, new Set()); - this._prototypeWhitelist.set(IsoDocument, new Set()); - this._prototypeWhitelist.set(Response, new Set()); - this._prototypeWhitelist.set(Element, new Set()); - this._prototypeWhitelist.set(HTMLInputElement, new Set()); - this._prototypeWhitelist.set(HTMLTextAreaElement, new Set()); - this._prototypeWhitelist.set(HTMLButtonElement, new Set()); - this._prototypeWhitelist.set(HTMLMediaElement, new Set()); - this._prototypeWhitelist.set(IsoAudio, new Set()); - this._prototypeWhitelist.set(FileReader, new Set()); - this._prototypeWhitelist.set(Blob, new Set()); - this._prototypeWhitelist.set(Event, new Set()); - this._prototypeWhitelist.set(DragEvent, new Set()); - this._prototypeWhitelist.set(InputEvent, new Set()); - this._prototypeWhitelist.set(KeyboardEvent, new Set()); - this._prototypeWhitelist.set(MouseEvent, new Set()); - this._prototypeWhitelist.set(PointerEvent, new Set()); - this._prototypeWhitelist.set(TouchEvent, new Set()); - this._prototypeWhitelist.set(TransitionEvent, new Set()); - this._prototypeWhitelist.set(UIEvent, new Set()); - this._prototypeWhitelist.set(WheelEvent, new Set()); - this._prototypeWhitelist.set(Promise, new Set()); - this._prototypeWhitelist.set(PromiseRejectionEvent, new Set()); - this._prototypeWhitelist.set(EventTarget, new Set()); - this._prototypeWhitelist.set(NamedNodeMap, new Set()); - this._prototypeWhitelist.set(IsoDOMParser, new Set()); - this._prototypeWhitelist.set(DOMParser, new Set()); - this._prototypeWhitelist.set(IsoXMLSerializer, new Set()); - this._prototypeWhitelist.set(XMLSerializer, new Set()); - this._prototypeWhitelist.set(SuFile, new Set()); - this._prototypeWhitelist.set(SuZip, new Set()); - this._prototypeWhitelist.set(View, new Set()); - this._prototypeWhitelist.set(OsClass, new Set()); - this._prototypeWhitelist.set(BuildConfigClass, new Set()); - this._prototypeWhitelist.set(Build, new Set()); - this._prototypeWhitelist.set(Native, new Set()); - this._prototypeWhitelist.set(Shell, new Set()); - this._prototypeWhitelist.set(Terminal, new Set()); - this._prototypeWhitelist.set(Chooser, new Set()); - this._prototypeWhitelist.set(Path, new Set()); - this._prototypeWhitelist.set(React, new Set()); - this._prototypeWhitelist.set(FormData, new Set()); - this._prototypeWhitelist.set(ModFS, new Set()); - this._prototypeWhitelist.set(DataTransfer, new Set()); - this._prototypeWhitelist.set(DataTransferItem, new Set()); - this._prototypeWhitelist.set(DataTransferItemList, new Set()); - - this.require = this.require.bind(this); - this._resolveModule = this._resolveModule.bind(this); - - this._sandbox = new Sandbox({ globals: this._globals, prototypeWhitelist: this._prototypeWhitelist }); - - this.module = { - exports: { - __esModule: true, - }, - }; - this.moduleCache = {}; - - this.path = new Path(options.cwd); - - this.libraries = options.libraries; - this.standaloneFile = options.standaloneFile; - this.indexFile = options.indexFile; - this.scope = { - ...options.scope, - module: this.module, - exports: this.module.exports, - path: this.path, - require: this.require, - }; - } - - public require(modulePath: string) { - // Check if the module is a core module - if (this.libraries[modulePath]) { - return this.libraries[modulePath]; - } - - // Resolve the module path - const resolvedPath = this._resolveModule(modulePath); - if (!resolvedPath) { - throw new IsolatedEvalError(`Cannot find module '${modulePath}'`); - } - - // Check if module is already cached - if (this.moduleCache[resolvedPath]) { - return this.moduleCache[resolvedPath].exports; - } - - // Create a new module and cache it - const module: IsoModule = { exports: {} }; - this.moduleCache[resolvedPath] = module; - - // Read and execute module content based on file extension - const extension = this.path.extname(resolvedPath); - - const readResolvedPath = new SuFile(resolvedPath); - - switch (extension) { - case ".json": - const jsonContent = readResolvedPath.read(); - module.exports = JSON.parse(jsonContent); - break; - - case ".yml": - case ".yaml": - module.exports = yaml.parse(readResolvedPath.read()); - break; - case ".properties": - case ".prop": - case ".ini": - module.exports = ini.parse(readResolvedPath.read()); - break; - case ".js": - case ".jsx": - const moduleContent = readResolvedPath.read(); - const transformed = this.transform(moduleContent); - if (transformed) { - const moduleWrapper = new Function("exports", "require", "module", "__filename", "__dirname", transformed); - const newPath = new Path(resolvedPath); - this.compile(`return ${moduleWrapper}`)( - module.exports, - this.require, - module, - resolvedPath, - newPath.dirname(resolvedPath), - { - path: newPath, - } - ); - } else { - throw new IsolatedEvalError("An error occurred, either there is a syntax mistake or something"); - } - break; - default: - module.exports.default = readResolvedPath.read(); - break; - } - - return module.exports.default || module.exports; - } - - private _resolveModule(modulePath: string): string | null { - const extensions = [".js", ".jsx", ".json", "yml", ".yaml", ".properties", ".prop", ".ini"]; - const resolvedPath = new SuFile(this.path.resolve(modulePath)); - - // Check if the exact file exists - if (resolvedPath.exist() && resolvedPath.isFile()) { - return resolvedPath.getPath(); - } - - // Check if file with extensions exists - for (let ext of extensions) { - const pth = new SuFile(resolvedPath.getPath() + ext); - - if (pth.exist() && pth.isFile()) { - return pth.getPath(); - } - } - - // Check if it's a directory and has an index file - if (resolvedPath.exist() && resolvedPath.isDirectory()) { - for (let ext of extensions) { - const ifp = new SuFile(this.path.join(resolvedPath.getPath(), "index" + ext)); - - if (ifp.exist() && ifp.isFile()) { - return ifp.getPath(); - } - } - } - - return null; - } - - public compileTransform(code: string) { - const parseCode = this.transform(code); - - if (typeof parseCode != "undefined") { - this._sandbox.compile(parseCode, false)(this.scope).run(); - } - - return this.module; - } - - public compile(code: string, ...scopes: IScope[]) { - return this._sandbox.compile(code, false)(this.scope, scopes).run(); - } - - public transform(data: string, filename?: string): string | undefined { - return transform(data, { - filename: this.indexFile, - presets: ["typescript", "react"], - plugins: [ - "transform-computed-properties", - "syntax-import-attributes", - ["transform-destructuring", { loose: true }], - ["transform-modules-commonjs", { loose: true, importInterop: "node" }], - "transform-object-rest-spread", - "syntax-class-properties", - ["transform-classes", { loose: true }], - ["transform-class-properties", { loose: true }], - "syntax-object-rest-spread", - ], - }).code as string | undefined; - } -} - -export { IsolatedEval, IsolatedEvalOptions }; diff --git a/src/native/Log.ts b/src/native/Log.ts deleted file mode 100644 index cb7f6040..00000000 --- a/src/native/Log.ts +++ /dev/null @@ -1,66 +0,0 @@ -import { Native } from "./Native"; - -type AllowedLogTypes = Pick; - -class Log extends Native { - private _tag: string; - - public static VERBOSE: number = 2; - public static DEBUG: number = 3; - public static INFO: number = 4; - public static WARN: number = 5; - public static ERROR: number = 6; - - public constructor(tag: string) { - super(window.__log__); - this._tag = tag; - } - - public v(message: string) { - this._native_log(Log.INFO, "info", message); - } - - public d(message: string) { - this._native_log(Log.DEBUG, "debug", message); - } - public i(message: string) { - this._native_log(Log.INFO, "info", message); - } - - public w(message: string) { - this._native_log(Log.WARN, "warn", message); - } - - public e(message: string) { - this._native_log(Log.ERROR, "error", message); - } - - private _native_log(prio: number, bPrio: keyof AllowedLogTypes, message: string) { - if (this.isAndroid) { - this.interface.native_log(prio, String(this._tag), String(message)); - } else { - console[bPrio](`[${this._tag}] -> ${message}`); - } - } - - public static v(tag: string, message: string) { - new Log(tag).v(message); - } - public static d(tag: string, message: string) { - new Log(tag).d(message); - } - - public static i(tag: string, message: string) { - new Log(tag).i(message); - } - - public static w(tag: string, message: string) { - new Log(tag).w(message); - } - - public static e(tag: string, message: string) { - new Log(tag).e(message); - } -} - -export { Log }; diff --git a/src/native/Magisk.ts b/src/native/Magisk.ts deleted file mode 100644 index dc5f0de0..00000000 --- a/src/native/Magisk.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { Native } from "./Native"; -import { Shell } from "./Shell"; - -class MagiskClass extends Native { - public constructor() { - super({}); - } - - /** - * Get current installed Magisk version code - */ - public get VERSION_CODE(): number { - if (this.isAndroid) { - return parseInt(Shell.cmd("magisk -V").result()); - } else { - return 0; - } - } - - public get VERSION_NAME(): string { - if (this.isAndroid) { - return Shell.cmd("magisk -v").result(); - } else { - return "0:MAGISKSU"; - } - } - - /** - * `XX.Y` is parsed as `XXY00`, so you can just put the Magisk version name. - * @param version - * @returns - */ - public PARSE_VERSION(version: str): number { - const i = version.indexOf("."); - if (i == -1) { - return parseInt(version); - } else { - return parseInt(version.substring(0, i)) * 1000 + parseInt(version.substring(i + 1)) * 100; - } - } -} - -/** - * @deprecated Use `Shell` instead - */ -export const Magisk: MagiskClass = new MagiskClass(); diff --git a/src/native/Native.ts b/src/native/Native.ts deleted file mode 100644 index a91023e8..00000000 --- a/src/native/Native.ts +++ /dev/null @@ -1,67 +0,0 @@ -import ModFS from "modfs"; - -export interface INative { - get interface(): T; -} - -export type NativeArgumentTypes = F extends (...args: infer A) => any ? A : never; - -/** - * Core functions for native functions/interfaces - */ -export class Native implements INative { - private _internal_interface: I; - public static static: Native; - - /** - * This field is required, otherwise the comunacation between Android will not work - * @required true - */ - public constructor(i: I) { - this._internal_interface = i; - Native.static = this; - } - - private static get userAgentRegex(): RegExp { - return /MMRL\/(.+)\s\(Linux;\sAndroid\s(.+);\s(.+)\sBuild\/(.+)\)/gs; - } - - public static get userAgent(): string { - return window.navigator.userAgent; - } - - /** - * Determine if MMRL runs on a Android device - */ - public static get isAndroid(): boolean { - return this.userAgentRegex.test(this.userAgent) || window.hasOwnProperty("cordova") ? true : false; - } - - /** - * Determine if MMRL runs on a Android device - */ - public get isAndroid(): boolean { - return Native.isAndroid; - } - - public get interface(): I { - return this._internal_interface; - } - - public static get interface() { - return Native.prototype.interface; - } - - public static Unsupported(mTarget: "Android" | "Browser", notes: string = "") { - return (target: any, key: string, descriptor: PropertyDescriptor) => { - const originalDef = descriptor.value; - - descriptor.value = function (...args: any[]) { - const obj = { M: `${key}()`, T: mTarget }; - console.log(ModFS.format(" is not supported on the variant.", obj), ModFS.format(notes, obj)); - return originalDef.apply(this, args); - }; - return descriptor; - }; - } -} diff --git a/src/native/Os.ts b/src/native/Os.ts deleted file mode 100644 index 0a608939..00000000 --- a/src/native/Os.ts +++ /dev/null @@ -1,226 +0,0 @@ -import ons from "onsenui"; -import React, { useCallback } from "react"; -import { useEventListener } from "usehooks-ts"; -import { Native } from "./Native"; -import { Build } from "./Build"; - -export type OpenOptions = { - target?: string | undefined; - features?: - | { - window?: string | undefined; - /** - * Only for Android - */ - color?: string | undefined; - } - | undefined; -}; - -class OsClass extends Native { - public constructor() { - super(window.__os__); - } - - readonly WindowMMRLOwn = "_mmrlOwn"; - readonly WindowBlank = "_blank"; - readonly WindowSelf = "_self"; - readonly WindowParent = "_parent"; - readonly WindowTop = "_top"; - readonly WindowUnfancedTop = "_unfencedTop"; - - /** - * @deprecated Use `os.openURL()` instead - */ - public open(url?: string | URL | undefined, options?: OpenOptions): Window | null { - if (this.isAndroid) { - return this.interface.open(url, options?.features?.color || "#101010"); - } else { - return window.open(url, options?.target, options?.features?.window); - } - } - - private _windowObjectReference: Window | null = null; - private _previousURL: string | URL | null | undefined = null; - - /** - * Handle opening link on Android and browsers. Android supports additional `color=#101010` feature - * @param url - * @param target - * @param features - * @returns - */ - public openURL(url?: string | URL, target?: string, features?: string): void { - const openRequestedSingleTab = (url?: string | URL, features?: string) => { - if (this._windowObjectReference === null || this._windowObjectReference.closed) { - this._windowObjectReference = open(url, this.WindowMMRLOwn, features); - } else if (this._previousURL !== url) { - this._windowObjectReference = open(url, this.WindowMMRLOwn, features); - /* if the resource to load is different, - then we load it in the already opened secondary window and then - we bring such window back on top/in front of its parent window. */ - this._windowObjectReference && this._windowObjectReference.focus(); - } else { - this._windowObjectReference.focus(); - } - this._previousURL = url; - /* explanation: we store the current url in order to compare url - in the event of another call of this function. */ - }; - - function parseWindowFeatures(features?: string) { - if (!features) return {}; - const featurePairs = features.split(","); - const featureObject = {}; - - featurePairs.forEach((pair) => { - const [key, value] = pair.split("="); - featureObject[key.trim()] = parseInt(value.trim()); // Parse value as integer - }); - - return featureObject; - } - - if (this.isAndroid) { - const parseFetures: Record = parseWindowFeatures(features); - this.interface.open(url, parseFetures.color || "#101010"); - } else { - if (target === this.WindowMMRLOwn) { - openRequestedSingleTab(url, features); - } else { - window.open(url, target, features); - } - } - } - - public hasStoragePermission(): boolean { - if (this.isAndroid) { - return this.interface.hasStoragePermission(); - } else { - return true; - } - } - - public requestStoargePermission(): void { - if (this.isAndroid) { - this.interface.requestStoargePermission(); - } else { - name; - } - } - - /** - * Closes the window. On Android closes the App - */ - public close(): void { - this.isAndroid ? this.interface.close() : window.close(); - } - - public shareText(title: string, body: string): void { - if (this.isAndroid) { - this.interface.shareText(title, body); - } - } - - /** - * Makes an toast, even on Android - * @param text - * @param duration - */ - public toast(text: string, duration: "long" | "short"): void { - const _duration = duration === "short" ? (this.isAndroid ? 0 : 2000) : this.isAndroid ? 1 : 5000; - if (this.isAndroid) { - this.interface.makeToast(text, _duration); - } else { - ons.notification.toast(text, { timeout: _duration, animation: "ascend" }); - } - } - - /** - * The SDK version of the software currently running on this hardware device. - * @returns {number} - */ - public get sdk(): number { - if (this.isAndroid) { - return this.interface.sdk(); - } else { - return 40; - } - } - - public getMonetColor(id: string): string { - if (this.isAndroid && this.sdk >= Build.VERSION_CODES.S) { - return this.interface.getMonetColor(id); - } else { - return "#ffffff"; - } - } - - /** - * Get the current status bar height from the current device. Has an automatic fallback for browsers - * @returns - */ - public getStatusBarHeight(): number { - if (this.isAndroid) { - return this.interface.getStatusBarHeight() / 2; - } else { - return 0; - } - } - - // public getSafeAreaInsets(typr: "top" | "bottom"): number { - // if (this.isAndroid) { - // return this.interface.getSafeAreaInsets() / 2; - // } else { - // return 0; - // } - // } - - /** - * Changes the status bar color - * @deprecated - * @param color Your color - * @param white `true` makes the status bar white - */ - public setStatusBarColor(color: string, white: boolean = false): void { - this.isAndroid ? this.interface.setStatusBarColor(color, white) : null; - } - - /** - * @deprecated - * @param color - */ - public setNavigationBarColor(color: string): void { - this.isAndroid ? this.interface.setNavigationBarColor(color) : null; - } - - public addNativeEventListener( - type: K, - callback: () => void, - options?: boolean | AddEventListenerOptions - ): void { - (window as any)[type] = new Event(type.toLowerCase()); - window.addEventListener(type.toLowerCase(), callback, options); - } - - public removeNativeEventListener( - type: K, - callback: () => void, - options?: boolean | AddEventListenerOptions - ): void { - (window as any)[type] = new Event(type.toLowerCase()); - window.removeEventListener(type.toLowerCase(), callback, options); - } - - public getSchemeParam(param: string): string { - if (this.isAndroid) { - return this.interface.getSchemeParam(param); - } else { - return new URLSearchParams(window.location.search).get(param) || ""; - } - } -} - -const os: OsClass = new OsClass(); - -export { os, OsClass }; diff --git a/src/native/Properties.ts b/src/native/Properties.ts deleted file mode 100644 index be233383..00000000 --- a/src/native/Properties.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { Native } from "./Native"; -import { Shell } from "./Shell"; - -interface IProperties { - get(key: string, def: string): string; - set(key: string, val: string): void; -} - -class PropertiesClass extends Native { - public constructor() { - super(null as any); - } - - public get(key: string, def: string): string { - if (this.isAndroid) { - return Shell.cmd(`getprop "${key}" "${def}"`).result(); - } else { - return window.localStorage.getItem(key) || def; - } - } - - public set(key: string, value: string): void { - if (this.isAndroid) { - Shell.cmd(`setprop "${key}" "${value}"`).exec(); - } else { - return window.localStorage.setItem(key, value); - } - } -} - -export const Properties = new PropertiesClass(); diff --git a/src/native/Shell.ts b/src/native/Shell.ts deleted file mode 100644 index 87899104..00000000 --- a/src/native/Shell.ts +++ /dev/null @@ -1,270 +0,0 @@ -import { Native } from "./Native"; -import { SuFile } from "./SuFile"; - -interface NativeShell { - /** - * Executes an command without result - */ - exec(command: string): void; - /** - * Executes an command with result - */ - result(command: string): string; - isSuccess(command: string): boolean; - getCode(command: string): number; - /** - * Checks if the app has been granted root privileges - * @deprecated Use `Shell.isSuAvailable()` instead - */ - isAppGrantedRoot(): boolean; - /** - * Checks if the app has been granted root privileges - */ - isSuAvailable(): boolean; - isKernelSU(): boolean; - isMagiskSU(): boolean; - isAPatchSU(): boolean; - getenv(key: string): string; - setenv(key: string, value: string, override: number): void; - pw_uid(): number; - pw_gid(): number; - pw_name(): string; -} - -interface NativeShellV2 extends NativeShell { - v2(command: string): { - exec(): void; - result(): string; - isSuccess(): boolean; - getCode(): number; - }; -} - -export type RootManager = "Magisk" | "KernelSU" | "APatchSU" | "Unknown"; -export type RootManagerV2 = "Magisk" | "KernelSU" | "APatch" | "Unknown"; - -/** - * Run Shell commands native on Android - */ -class Shell extends Native { - /** - * Successful module install exit code - */ - public static readonly M_INS_SUCCESS: number = 0; - /** - * Failed module install exit code - */ - public static readonly M_INS_FAILURE: number = 1; - /** - * Failed file download exit code - */ - public static readonly M_DWL_FAILURE: number = 2; - /** - * File creation exit code - */ - public static readonly FILE_CRA_ERRO: number = 3; - /** - * Internal terminal error exit code - */ - public static readonly TERM_INTR_ERR: number = 500; - - private _command: Array; - // @ts-ignore - Won't get even called - private _shell: ReturnType; - - public constructor(command: string | Array) { - super(window.__shell__); - - if (!Array.isArray(command)) { - this._command = [command]; - } else { - this._command = command; - } - - if (this.isAndroid) { - this._shell = this.interface.v2.bind(this.interface)(JSON.stringify(this._command)); - } - } - - public exec(): void { - if (this.isAndroid) { - this._shell.exec(); - } - } - - public result(): string { - if (this.isAndroid) { - return this._shell.result(); - } else { - return ""; - } - } - - public isSuccess(): boolean { - if (this.isAndroid) { - return this._shell.isSuccess(); - } else { - return false; - } - } - - public getCode(): number { - if (this.isAndroid) { - return this._shell.getCode(); - } else { - return 1; - } - } - - /** - * Compatibility method to ensure support without beaking changes - * @param command - * @returns - */ - public static cmd(command: string | Array): Shell { - return new Shell(command); - } - - /** - * Checks if the app has been granted root privileges - */ - public static isSuAvailable(): boolean { - if (this.isAndroid) { - return window.__shell__.isSuAvailable(); - } - - return false; - } - - /** - * Get current installed Superuser version code - */ - public static VERSION_CODE(): number { - if (this.isAndroid) { - return parseInt(this.cmd("su -V").result()); - } else { - return 0; - } - } - - public static VERSION_NAME(): string { - if (this.isAndroid) { - return this.cmd("su -v").result(); - } else { - return "0:SU"; - } - } - - /** - * @deprecated - * @returns - */ - public static getRootManager(): RootManager { - const rootManagers: [boolean, RootManager][] = [ - [this.isMagiskSU(), "Magisk"], - [this.isKernelSU(), "KernelSU"], - [this.isAPatchSU(), "APatchSU"], - ]; - - for (const [check, name] of rootManagers) { - if (check) { - return name; - } - } - - return "Unknown"; - } - - public static getRootManagerV2(): RootManagerV2 { - const rootManagers: [boolean, RootManagerV2][] = [ - [this.isMagiskSU(), "Magisk"], - [this.isKernelSU(), "KernelSU"], - [this.isAPatchSU(), "APatch"], - ]; - - for (const [check, name] of rootManagers) { - if (check) { - return name; - } - } - - return "Unknown"; - } - - /** - * Use regex for better detection - * @param searcher - * @returns - */ - private static _mountDetect(searcher: { [Symbol.search](string: string): number }): boolean { - const proc = new SuFile("/proc/self/mounts"); - - if (proc.exist()) { - return proc.read().search(searcher) !== -1; - } else { - return false; - } - } - - /** - * Determine if MMRL runs with KernelSU - */ - public static isKernelSU(): boolean { - // `proc.exist()` is always `false` on browsers - return this._mountDetect(/\b(KSU|KernelSU)\b/i); - } - - /** - * Determine if MMRL runs with Magisk - */ - public static isMagiskSU(): boolean { - // `proc.exist()` is always `false` on browsers - return this._mountDetect(/\b(magisk|core\/mirror|core\/img)\b/i); - } - - /** - * Determine if MMRL runs with APatch - */ - public static isAPatchSU(): boolean { - // `proc.exist()` is always `false` on browsers - return this._mountDetect(/\b(APD|APatch)\b/i); - } - - /** - * Returns the current user id - * @returns {strign} User ID - */ - public static pw_uid(): string { - if (this.isAndroid) { - return this.cmd("id -u").result(); - } else { - return "Unknown"; - } - } - - /** - * Returns the current group id - * @returns {string} Group ID - */ - public static pw_gid(): string { - if (this.isAndroid) { - return this.cmd("id -g").result(); - } else { - return "Unknown"; - } - } - - /** - * Returns the current user name - * @returns {string} User name - */ - public static pw_name(): string { - if (this.isAndroid) { - return this.cmd("id -un").result(); - } else { - return "Unknown"; - } - } -} - -export { Shell }; diff --git a/src/native/SuFile.ts b/src/native/SuFile.ts deleted file mode 100644 index a3a48eb0..00000000 --- a/src/native/SuFile.ts +++ /dev/null @@ -1,356 +0,0 @@ -import { path } from "@Util/path"; -import { Native } from "./Native"; -import { fs } from "@zenfs/core"; - -interface NativeSuFile extends NativeSuFileV2 { - readFile(path: string): string; - listFiles(path: string): string; - createFile(path: string): boolean; - deleteFile(path: string): boolean; - deleteRecursive(path: string): boolean; - existFile(path: string): boolean; - getSharedFile(): string | null; -} - -interface NativeSuFileV2 { - v2(path: string): { - write(data: string): void; - read(def: string): string; - readAsBase64(): string; - list(delimiter: string | null): string; - lastModified(): number; - create(type: number): boolean; - delete(): boolean; - deleteRecursive(): void; - exists(): boolean; - _is_TypeMethod(type: number): boolean; - _can_TypeMethod(type: number): boolean; - setExecuteWriteReadable(type: number, state: boolean, ownerOnly: boolean): boolean; - }; -} - -export interface SuFileoptions { - /** - * This should be always a string - */ - readDefaultValue: string; -} - -export type SuFileConstuctor = new (path: string) => SuFile; - -/** - * Class to read files on a native Android device - * @implements {NativeSuFile} - */ -class SuFile extends Native { - // @ts-ignore - Won't get even called - private _file: ReturnType; - // @ts-ignore - Won't get even called - private _bfile: typeof fs; - private _path: string; - private _imgblob: string | ArrayBuffer | null = null; - private _readDefaultValue: string; - - /** - * @returns `0` as number to create a new file - */ - public static readonly NEW_FILE: number = 0; - /** - * @returns `1` as number to create a new folder with parent folders - */ - public static readonly NEW_FOLDERS: number = 1; - /** - * @returns `2` as number to create a new folder - */ - public static readonly NEW_FOLDER: number = 2; - - public static TYPE = class { - public static readonly ISFILE = 0; - public static readonly ISSYMLINK = 1; - public static readonly ISDIRECTORY = 2; - public static readonly ISBLOCK = 3; - public static readonly ISCHARACTER = 4; - public static readonly ISNAMEDPIPE = 5; - public static readonly ISSOCKET = 6; - public static readonly ISHIDDEN = 7; - - public static readonly CANREAD = 0; - public static readonly CANWRITE = 1; - public static readonly CANEXECUTE = 2; - }; - - private _restrictedPaths = [/(\/data\/data\/(.+)\/?|(\/storage\/emulated\/0|\/sdcard)\/Android\/(data|media|obb)(.+)?)\/?/i]; - - public constructor(path?: string, opt?: SuFileoptions) { - super(window.__sufile__); - this._readDefaultValue = opt?.readDefaultValue || ""; - this._path = path ? String(path) : ""; - - if (this._isRestrictedPath(this._path)) { - throw new Error(`SuFile tried to access "${path}" which has been blocked due security.`); - } - - if (this.isAndroid) { - this._file = this.interface.v2.bind(this.interface)(this._path); - } else { - this._bfile = fs; - } - } - - private _isRestrictedPath(path: string): boolean { - return this._restrictedPaths.some((restrictedPath) => restrictedPath.test(path)); - } - - public retrictedPaths(newPaths: RegExp[]) { - // run typo checks - for (const path of newPaths) { - if (!(path instanceof RegExp)) { - throw new TypeError(String(path) + " is not a regular expression"); - } - } - - this._restrictedPaths = [...this._restrictedPaths, ...newPaths]; - } - - public getPath(): string { - return this._path; - } - - public read(): string { - if (this.isAndroid) { - return this._file.read(this._readDefaultValue); - } else { - return this._bfile.readFileSync(this._path, "utf-8") || this._readDefaultValue; - } - } - - public readAsBase64() { - if (this.isAndroid) { - return this._file.readAsBase64(); - } else { - return ""; - } - } - - public write(content: string): void { - if (this.isAndroid) { - this._file.write(content); - } else { - this._bfile.writeFileSync(this._path, content); - } - } - - public list(delimiter: string = ","): Array { - if (this.isAndroid) { - return this._file.list(delimiter).split(delimiter); - } else { - return this._bfile.readdirSync(this._path); - } - } - - public lastModified(): number { - if (this.isAndroid) { - return this._file.lastModified(); - } else { - return 0; - } - } - - public exist(): boolean { - if (this.isAndroid) { - return this._file.exists(); - } else { - return this._bfile.existsSync(this._path); - } - } - - public delete(): boolean { - if (this.isAndroid) { - return this._file.delete(); - } else { - return false; - } - } - - public deleteRecursive(): void { - if (this.isAndroid) { - this._file.deleteRecursive(); - } else { - this._bfile.rmSync(this._path, { recursive: true }); - } - } - /** - * Creates a new file or folder - * ``` - * SuFile.NEW_FILE - * SuFile.NEW_FOLDER - * SuFile.NEW_FOLDERS - * ``` - * @param type - * @default SuFile.NEW_FILE - * @returns {boolean} - */ - public create(type: number = SuFile.NEW_FILE): boolean { - if (this.isAndroid) { - return this._file.create(type); - } else { - switch (type) { - case SuFile.NEW_FILE: - this._bfile.writeFileSync(this._path, ""); - break; - case SuFile.NEW_FOLDER: - this._bfile.mkdirSync(this._path); - break; - case SuFile.NEW_FOLDERS: - this._bfile.mkdirSync(this._path, { recursive: true }); - break; - default: - return true; - } - - return true; - } - } - - private _isTypeMethod(type: number, defR: boolean = false) { - if (typeof type !== "number") throw new TypeError("'SuFile' => '_isTypeMethod' only accepts numbers as type"); - - if (this.isAndroid) { - return this._file._is_TypeMethod(type); - } - - return defR; - } - - private _canTypeMethod(type: number, defR: boolean = false): boolean { - if (typeof type !== "number") throw new TypeError("'SuFile' => '_canTypeMethod' only accepts numbers as type"); - - if (this.isAndroid) { - return this._file._can_TypeMethod(type); - } - - return defR; - } - - private _setExecuteWriteReadable(type: number, state: boolean, ownerOnly: boolean, defR: boolean = false): boolean { - if (typeof type !== "number") throw new TypeError("'SuFile' => '_canTypeMethod' only accepts numbers as type"); - - if (this.isAndroid) { - return this._file.setExecuteWriteReadable(type, state, ownerOnly); - } - - return defR; - } - - public canRead(): boolean { - return this._canTypeMethod(SuFile.TYPE.CANREAD); - } - - public canWrite(): boolean { - return this._canTypeMethod(SuFile.TYPE.CANWRITE); - } - - public canExecute(): boolean { - return this._canTypeMethod(SuFile.TYPE.CANEXECUTE); - } - - public setExecuteable(executable: boolean, ownerOnly: boolean = true) { - return this._setExecuteWriteReadable(SuFile.TYPE.CANEXECUTE, executable, ownerOnly); - } - - public setWriteable(writeable: boolean, ownerOnly: boolean = true) { - return this._setExecuteWriteReadable(SuFile.TYPE.CANWRITE, writeable, ownerOnly); - } - - public setReadable(readable: boolean, ownerOnly: boolean = true) { - return this._setExecuteWriteReadable(SuFile.TYPE.CANREAD, readable, ownerOnly); - } - - public isFile(): boolean { - return this._isTypeMethod(SuFile.TYPE.ISFILE, this._path in localStorage); - } - - public isSymlink(): boolean { - return this._isTypeMethod(SuFile.TYPE.ISSYMLINK); - } - - public isDirectory(): boolean { - return this._isTypeMethod(SuFile.TYPE.ISDIRECTORY); - } - - public isBlock(): boolean { - return this._isTypeMethod(SuFile.TYPE.ISBLOCK); - } - - public isCharacter(): boolean { - return this._isTypeMethod(SuFile.TYPE.ISCHARACTER); - } - - public isNamedPipe(): boolean { - return this._isTypeMethod(SuFile.TYPE.ISNAMEDPIPE); - } - - public isSocket(): boolean { - return this._isTypeMethod(SuFile.TYPE.ISSOCKET); - } - - public isHidden(): boolean { - return this._isTypeMethod(SuFile.TYPE.ISHIDDEN); - } - - public static read(path: string): string { - return new SuFile(path).read(); - } - - public static write(path: string, content: string): void { - new SuFile(path).write(content); - } - - public static list(path: string): Array { - return new SuFile(path).list(); - } - - public static exist(path: string): boolean { - return new SuFile(path).exist(); - } - - public static delete(path: string): boolean { - return new SuFile(path).delete(); - } - - public static deleteRecursive(path: string): void { - new SuFile(path).deleteRecursive(); - } - - public static create(path: string, type: number = SuFile.NEW_FILE): boolean { - return new SuFile(path).create(type); - } - - public static getSharedFile(): string | undefined { - if (this.isAndroid) { - return this.static.interface.getSharedFile(); - } - return undefined; - } - - public static createFileTree(fileTree: object, basePath: string) { - Object.keys(fileTree).forEach((key) => { - const value = fileTree[key]; - const fullPath = path.join(basePath, key.replace(/^\//, "")); // Remove leading '/' if present - - if (typeof value === "object") { - // If value is an object, create a directory - if (!SuFile.exist(fullPath)) { - SuFile.create(fullPath, SuFile.NEW_FOLDERS); - } - // Recursively create subdirectories and files - this.createFileTree(value, fullPath); - } else if (typeof value === "string") { - // If value is a string, treat it as file content - SuFile.write(fullPath, value); - } - }); - } -} - -export { SuFile }; diff --git a/src/native/SuZip.ts b/src/native/SuZip.ts deleted file mode 100644 index 435a52e7..00000000 --- a/src/native/SuZip.ts +++ /dev/null @@ -1,98 +0,0 @@ -import { Native } from "./Native"; - -export type SuZipConstuctor = new (zipFile: string, path: string) => SuZip; - -export interface NativeSuZip { - newFS(zipFile: string): { - list(): string; - read(path: string): string; - exists(path: string): boolean; - isFile(path: string): boolean; - isDirectory(path: string): boolean; - }; -} - -/** - * Class to read files on a native Android device - * @implements {NativeSuZip} - */ -class SuZip extends Native { - // @ts-ignore - Won't get even called - private _zipFile: ReturnType; - private _zipFilePath: string; - private _path: string; - - public constructor(zipFile: string, path: string) { - super(window.__suzip__); - this._zipFilePath = zipFile; - this._path = path; - if (this.isAndroid) { - this._zipFile = this.interface.newFS.bind(this.interface)(zipFile); - } - } - - public getZipPath(): string { - return this._zipFilePath; - } - - public getPath(): string { - return this._path; - } - - public read(): string { - if (this.isAndroid) { - return this._zipFile.read(this._path); - } - return ""; - } - - public list(): Array { - if (this.isAndroid) { - return this._zipFile.list().split(","); - } - return [""]; - } - - public exist(): boolean { - if (this.isAndroid) { - return this._zipFile.exists(this._path); - } - return false; - } - - public isFile(): boolean { - if (this.isAndroid) { - return this._zipFile.isFile(this._path); - } - return false; - } - - public isDirectory(): boolean { - if (this.isAndroid) { - return this._zipFile.isDirectory(this._path); - } - return false; - } - - public static read(zipPath: string, path: string) { - return new SuZip(zipPath, path).read(); - } - - public static list(zipPath: string) { - return new SuZip(zipPath, "").list(); - } - - public static exist(zipPath: string, path: string) { - return new SuZip(zipPath, path).exist(); - } - - public static isFile(zipPath: string, path: string) { - return new SuZip(zipPath, path).isFile(); - } - - public static isDirectory(zipPath: string, path: string) { - return new SuZip(zipPath, path).isDirectory(); - } -} - -export { SuZip }; diff --git a/src/native/Terminal.ts b/src/native/Terminal.ts deleted file mode 100644 index 0df6ecfc..00000000 --- a/src/native/Terminal.ts +++ /dev/null @@ -1,92 +0,0 @@ -import { Log } from "./Log"; -import { Native } from "./Native"; - -type TerminalStream = { - stdout: string | null; - stderr: string | null; -}; - -interface TerminalExec { - command: string; - /** - * Environment variables that should be used in your command execution - */ - env?: Record; - /** - * Working directory - */ - cwd?: string; - /** - * Prints the error to the console or the `onLine` argument - * @default true - */ - printError?: boolean; - onLine: (stream: TerminalStream) => void; - onExit?: ((code: number) => void) | null; -} - -type TerminalOptions = Omit; - -interface TerminalNative { - exec(options: TerminalExec): void; -} - -export type TerminalEnv = Record; - -class Terminal extends Native { - private _env: TerminalEnv = {}; - public cwd: string | undefined; - printErrors: boolean | undefined; - private _onLine: ((stdout: string) => void) | undefined; - private _onExit: ((code: number) => void) | undefined | null; - private _onError: ((stderr: string) => void) | undefined | null; - - public constructor(options?: TerminalOptions) { - super(window.__terminal__); - - this.cwd = options?.cwd; - this.printErrors = options?.printError; - } - - public set onLine(func: (stdout: string) => void) { - this._onLine = func; - } - - public set onError(func: (stderr: string) => void) { - this._onError = func; - } - - public set onExit(func: TerminalExec["onExit"]) { - this._onExit = func; - } - - public get env(): TerminalEnv { - return this._env; - } - - public set env(envs: TerminalEnv) { - this._env = envs; - } - - public exec(command: string): void { - if (this.isAndroid) { - if (typeof this._onLine !== "function") throw new TypeError("Terminal 'onLine' is not a function"); - - this.interface.exec({ - command: command, - cwd: this.cwd, - env: this.env, - onLine: (stream: TerminalStream) => { - if (stream.stdout && this._onLine) { - this._onLine(stream.stdout); - } else if (stream.stdout && this._onError) { - this._onError(stream.stdout); - } - }, - onExit: this._onExit, - }); - } - } -} - -export { Terminal }; diff --git a/src/native/View.ts b/src/native/View.ts deleted file mode 100644 index 99c76e67..00000000 --- a/src/native/View.ts +++ /dev/null @@ -1,183 +0,0 @@ -import { Native } from "./Native"; - -interface NativeView { - getWindowTopInsets(): number; - getWindowRightInsets(): number; - getWindowBottomInsets(): number; - getWindowLeftInsets(): number; - setStatusBarColor(color: string, white: boolean): void; - setNavigationBarColor(color: string): void; - isAppearanceLightNavigationBars(): boolean; - setAppearanceLightNavigationBars(isLight: boolean): void; - isAppearanceLightStatusBars(): boolean; - setAppearanceLightStatusBars(isLight: boolean): void; - showSystemBars(type: number): void; - hideSystemBars(type: number): void; - addFlag(flag: int): void; - clearFlag(flag: int): void; -} - -class ViewInsetsCompat { - public static systemBars(): number { - return this.Type.STATUS_BARS | this.Type.NAVIGATION_BARS | this.Type.CAPTION_BAR; - } - - public static Type = class { - public static readonly FIRST = 1; - public static readonly STATUS_BARS = this.FIRST; - public static readonly NAVIGATION_BARS = 1 << 1; - public static readonly CAPTION_BAR = 1 << 2; - }; -} - -class WindowManager { - public static LayoutParams = class { - public static readonly FLAG_KEEP_SCREEN_ON = 128; - }; -} - -class View extends Native { - public constructor() { - super(window.__view__); - } - - public getWindowTopInsets(): number { - if (this.isAndroid) { - return this.interface.getWindowTopInsets(); - } else { - return 0; - } - } - - public getWindowRightInsets(): number { - if (this.isAndroid) { - return this.interface.getWindowRightInsets(); - } else { - return 0; - } - } - - public getWindowBottomInsets(): number { - if (this.isAndroid) { - return this.interface.getWindowBottomInsets(); - } else { - return 0; - } - } - - public getWindowLeftInsets(): number { - if (this.isAndroid) { - return this.interface.getWindowLeftInsets(); - } else { - return 0; - } - } - - public fullscreen(enable: boolean) { - if (this.isAndroid) { - if (enable) { - this.interface.hideSystemBars(ViewInsetsCompat.systemBars()); - } else { - this.interface.showSystemBars(ViewInsetsCompat.systemBars()); - } - } - } - /** - * Checks if the foreground of the navigation bar is set to light. - * ``` - * ``` - * This method always returns false on API < 26. - * - * @return true if the foreground is light - */ - public isAppearanceLightNavigationBars(): boolean { - if (this.isAndroid) { - return this.interface.isAppearanceLightNavigationBars(); - } else { - return false; - } - } - - /** - * If true, changes the foreground color of the navigation bars to light so that the items on - * the bar can be read clearly. If false, reverts to the default appearance. - * ``` - * ``` - * This method has no effect on API < 26. - */ - public setAppearanceLightNavigationBars(isLight: boolean = false): void { - if (this.isAndroid) { - this.interface.setAppearanceLightNavigationBars(isLight); - } - } - - /** - * Checks if the foreground of the status bar is set to light. - * ``` - * ``` - * This method always returns false on API < 23. - * - * @return true if the foreground is light - */ - public isAppearanceLightStatusBars(): boolean { - if (this.isAndroid) { - return this.interface.isAppearanceLightStatusBars(); - } else { - return false; - } - } - - /** - * If true, changes the foreground color of the status bars to light so that the items on the - * bar can be read clearly. If false, reverts to the default appearance. - * ``` - * ``` - * This method has no effect on API < 23. - */ - - public setAppearanceLightStatusBars(isLight: boolean = true): void { - if (this.isAndroid) { - this.interface.setAppearanceLightStatusBars(isLight); - } - } - - /** - * Changes the status bar color - * @param color Your color - * @param white `true` makes the status bar white - */ - public setStatusBarColor(color: string, white: boolean = false): void { - if (this.isAndroid) { - this.interface.setStatusBarColor(color, white); - } - } - - /** - * - * @param color - */ - public setNavigationBarColor(color: string): void { - if (this.isAndroid) { - this.interface.setNavigationBarColor(color); - } - } - - public addFlags(flags: list) { - if (this.isAndroid) { - for (const flag of flags) { - this.interface.addFlag(flag); - } - } - } - - public clearFlags(flags: list) { - if (this.isAndroid) { - for (const flag of flags) { - this.interface.clearFlag(flag); - } - } - } -} - -const view: View = new View(); -export { view, View, ViewInsetsCompat, WindowManager }; diff --git a/src/styles/default.scss b/src/styles/default.scss deleted file mode 100644 index 6da8c581..00000000 --- a/src/styles/default.scss +++ /dev/null @@ -1,40 +0,0 @@ -*, -*::before, -*::after { - box-sizing: border-box; - ::-webkit-scrollbar { - display: none; - } - touch-action: manipulation; -} - -@media (prefers-reduced-motion: no-preference) { - :root { - scroll-behavior: smooth; - } -} - -::-webkit-scrollbar { - display: none; -} - -::-moz-selection { - color: #000; - background: #fff; -} - -::selection { - color: #000; - background: #fff; -} - -.lazy-container { - opacity: 0; - transform: translateY(20px); - transition: opacity 0.5s ease-out, transform 0.5s ease-out; -} - -.lazy-container.fade-in { - opacity: 1; - transform: translateY(0); -} diff --git a/src/styles/light_theme.tsx b/src/styles/light_theme.tsx deleted file mode 100644 index c8e878bd..00000000 --- a/src/styles/light_theme.tsx +++ /dev/null @@ -1,4799 +0,0 @@ -import { os } from "@Native/Os"; -import { view } from "@Native/View"; -import { GlobalStyles, Theme } from "@mui/material"; - -// export const LightTheme = (theme: Theme): any => ({ -// "@global": { -// ":root": {}, -// }, -// }); - -export const LightTheme = () => { - return ( - ({ - // eruda tools - ".eruda-dev-tools": { - paddingBottom: `${view.getWindowBottomInsets()}px !important`, - }, - - html: { - height: "100%", - width: "100%", - }, - body: { - position: "absolute", - overflow: "hidden", - top: "0", - right: "0", - left: "0", - bottom: "0", - padding: "0", - margin: "0", - webkitTextSizeAdjust: "100%", - touchAction: "manipulation", - }, - "html, body, div, span, applet, object, iframe, h1, h2, h3, h4, h5, h6, p, blockquote, pre, a, abbr, acronym, address, big, cite, code, del, dfn, em, img, ins, kbd, q, s, samp, small, strike, strong, sub, sup, tt, var, b, u, i, center, dl, dt, dd, ol, ul, li, fieldset, form, label, legend, table, caption, tbody, tfoot, thead, tr, th, td, article, aside, canvas, details, embed, figure, figcaption, footer, header, hgroup, menu, nav, output, ruby, section, summary, time, mark, audio, video": - { - webkitUserSelect: "none", - mozUserSelect: "none", - msUserSelect: "none", - userSelect: "none", - webkitTapHighlightColor: "transparent", - webkitTouchCallout: "none", - }, - "input, textarea, select, pre, code": { - webkitUserSelect: "auto", - msUserSelect: "text", - userSelect: "auto", - mozUserSelect: "text", - webkitTouchCallout: "none", - }, - "a, button, input, textarea, select": { - touchAction: "manipulation", - }, - "input:active, input:focus, textarea:active, textarea:focus, select:active, select:focus": { - outline: "none", - }, - h1: { - fontSize: "36px", - }, - h2: { - fontSize: "30px", - }, - h3: { - fontSize: "24px", - }, - "h4, h5, h6": { - fontSize: "18px", - }, - "pre, code": { - userSelect: "text", - span: { - userSelect: "text", - }, - }, - ".page": { - fontFamily: '-apple-system, "Helvetica Neue", "Helvetica", "Arial", "Lucida Grande", sans-serif', - webkitFontSmoothing: "antialiased", - mozOsxFontSmoothing: "grayscale", - fontWeight: "400", - backgroundColor: theme.palette.background.default, - position: "absolute", - top: "0", - left: "0", - right: "0", - bottom: "0", - overflowX: "visible", - overflowY: "hidden", - // color: "#1f1f21", - msOverflowStyle: "none", - }, - ".page::-webkit-scrollbar": { - display: "none", - }, - ".page__content": { - containerType: "inline-size", - containerName: "ons-page-content", - backgroundColor: theme.palette.background.default, - position: "absolute", - top: "0", - left: "0", - right: "0", - bottom: "0", - boxSizing: "border-box", - paddingTop: "0", - paddingBottom: view.getWindowBottomInsets(), - }, - ".page__background": { - containerType: "inline-size", - containerName: "ons-page-background", - backgroundColor: theme.palette.background.default, - position: "absolute", - top: "0", - left: "0", - right: "0", - bottom: "0", - boxSizing: "border-box", - }, - ".page--material": { - containerType: "inline-size", - containerName: "ons-page", - fontFamily: '"Roboto", "Noto", sans-serif', - webkitFontSmoothing: "antialiased", - fontWeight: "400", - backgroundColor: theme.palette.background.default, - }, - // Android - ".page--wrapper": { - // marginTop: window.__os__.getSafeArea("top") / 2 + "px", - // marginTop: os.getStatusBarHeight() + "px", - }, - ".page--material__content": { - fontFamily: '"Roboto", "Noto", sans-serif', - webkitFontSmoothing: "antialiased", - fontWeight: "400", - }, - // ".page__content h1, .page__content h2, .page__content h3, .page__content h4, .page__content h5": { - // fontFamily: '"Roboto", "Noto", sans-serif', - // webkitFontSmoothing: "antialiased", - // fontWeight: "500", - // fallbacks: [ - // { - // fontWeight: "400", - // }, - // ], - // margin: "0.6em 0", - // padding: "0", - // }, - // ".page__content h1": { - // fontSize: "28px", - // }, - // ".page__content h2": { - // fontSize: "24px", - // }, - // ".page__content h3": { - // fontSize: "20px", - // }, - // ".page--material__content h1, .page--material__content h2, .page--material__content h3, .page--material__content h4, .page--material__content h5": - // { - // fontFamily: '"Roboto", "Noto", sans-serif', - // webkitFontSmoothing: "antialiased", - // fontWeight: "500", - // fallbacks: [ - // { - // fontWeight: "400", - // }, - // ], - // margin: "0.6em 0", - // padding: "0", - // }, - // ".page--material__content h1": { - // fontSize: "28px", - // }, - // ".page--material__content h2": { - // fontSize: "24px", - // }, - // ".page--material__content h3": { - // fontSize: "20px", - // }, - ".page--material__background": { - backgroundColor: theme.palette.background.default, - }, - ".switch": { - display: "inline-block", - verticalAlign: "top", - boxSizing: "border-box", - backgroundClip: "padding-box", - position: "relative", - minWidth: "51px", - fontSize: "17px", - padding: "0 20px", - border: "none", - overflow: "visible", - width: "51px", - height: "32px", - zIndex: "0", - textAlign: "left", - }, - ".switch__input": { - position: "absolute", - right: "0", - top: "0", - left: "0", - bottom: "0", - padding: "0", - border: "0", - backgroundColor: "transparent", - zIndex: "0", - verticalAlign: "top", - outline: "none", - width: "100%", - height: "100%", - margin: "0", - webkitAppearance: "none", - mozAppearance: "none", - appearance: "none", - }, - ".switch__toggle": { - backgroundColor: "white", - position: "absolute", - top: "0", - left: "0", - right: "0", - bottom: "0", - borderRadius: "30px", - transitionProperty: "all", - transitionDuration: "0.35s", - transitionTimingFunction: "ease-out", - boxShadow: "inset 0 0 0 2px #e5e5e5", - }, - ".switch__handle": { - boxSizing: "border-box", - backgroundClip: "padding-box", - position: "absolute", - content: '""', - borderRadius: "28px", - height: "28px", - width: "28px", - backgroundColor: "white", - left: "1px", - top: "2px", - transitionProperty: "all", - transitionDuration: "0.35s", - transitionTimingFunction: "cubic-bezier(0.59, 0.01, 0.5, 0.99)", - boxShadow: "0 0 1px 0 rgba(0, 0, 0, 0.25), 0 3px 2px rgba(0, 0, 0, 0.25)", - }, - ".switch--active__handle": { - transition: "none", - }, - ":checked + .switch__toggle": { - boxShadow: "inset 0 0 0 2px #44db5e", - backgroundColor: "#44db5e", - }, - ":checked + .switch__toggle > .switch__handle": { - left: "21px", - boxShadow: "0 3px 2px rgba(0, 0, 0, 0.25)", - }, - ":disabled + .switch__toggle": { - opacity: "0.3", - pointerEvents: "none", - }, - ".switch__touch": { - position: "absolute", - top: "-5px", - bottom: "-5px", - left: "-10px", - right: "-10px", - }, - ".switch--material": { - width: "36px", - height: "24px", - padding: "0 10px", - minWidth: "36px", - }, - ".switch--material__toggle": { - backgroundColor: "#b0afaf", - marginTop: "5px", - height: "14px", - boxShadow: "none", - }, - ".switch--material__input": { - position: "absolute", - right: "0", - top: "0", - left: "0", - bottom: "0", - padding: "0", - border: "0", - backgroundColor: "transparent", - zIndex: "0", - verticalAlign: "top", - outline: "none", - width: "100%", - height: "100%", - margin: "0", - webkitAppearance: "none", - mozAppearance: "none", - appearance: "none", - }, - ".switch--material__handle": { - backgroundColor: "#f1f1f1", - left: "0", - marginTop: "-5px", - width: "20px", - height: "20px", - boxShadow: "0 4px 5px 0 rgba(0, 0, 0, 0.14), 0 1px 10px 0 rgba(0, 0, 0, 0.12),\r\n 0 2px 4px -1px rgba(0, 0, 0, 0.4)", - }, - ":checked + .switch--material__toggle": { - backgroundColor: "#7c43bd", - boxShadow: "none", - }, - ":checked + .switch--material__toggle > .switch--material__handle": { - left: "16px", - backgroundColor: theme.palette.primary.main, - boxShadow: "0 2px 2px 0 rgba(0, 0, 0, 0.14), 0 1px 5px 0 rgba(0, 0, 0, 0.12),\r\n 0 3px 1px -2px rgba(0, 0, 0, 0.2)", - }, - ":disabled + .switch--material__toggle": { - opacity: "0.3", - pointerEvents: "none", - }, - ".switch--material__handle:before": { - background: "transparent", - content: '""', - display: "block", - width: "100%", - height: "100%", - borderRadius: "50%", - zIndex: "0", - boxShadow: "0 0 0 0 rgba(0, 0, 0, 0.12)", - transition: "box-shadow 0.1s linear", - }, - ".switch--material__toggle > .switch--active__handle:before": { - boxShadow: "0 0 0 14px rgba(0, 0, 0, 0.12)", - }, - ":checked + .switch--material__toggle > .switch--active__handle:before": { - boxShadow: "0 0 0 14px color-mod(#4a148c alpha(20%))", - }, - ".switch--material__touch": { - position: "absolute", - top: "-10px", - bottom: "-10px", - left: "-15px", - right: "-15px", - }, - - ".range": { - display: "inline-block", - position: "relative", - width: "100px", - height: "30px", - margin: "0", - padding: "0", - backgroundImage: "linear-gradient(#a4aab3, #a4aab3)", - backgroundPosition: "left center", - backgroundSize: "100% 2px", - backgroundRepeat: "no-repeat", - backgroundColor: "transparent", - }, - ".range__input": { - boxSizing: "border-box", - backgroundClip: "padding-box", - padding: "0", - margin: "0", - font: "inherit", - color: "inherit", - background: "transparent", - border: "none", - verticalAlign: "top", - outline: "none", - lineHeight: "1", - webkitAppearance: "none", - mozAppearance: "none", - appearance: "none", - backgroundImage: "linear-gradient(#4a148c, #4a148c)", - backgroundPosition: "left center", - backgroundSize: "0% 2px", - backgroundRepeat: "no-repeat", - height: "30px", - position: "relative", - zIndex: "1", - width: "100%", - }, - ".range__input::-moz-range-track": { - position: "relative", - border: "none", - background: "none", - boxShadow: "none", - top: "0", - margin: "0", - padding: "0", - }, - ".range__input::-ms-track": { - position: "relative", - border: "none", - backgroundColor: "#a4aab3", - height: "0", - borderRadius: "50%", - }, - ".range__input::-webkit-slider-thumb": { - position: "relative", - height: "28px", - width: "28px", - backgroundColor: "#fff", - border: "none", - boxShadow: "0 0 1px 0 rgba(0, 0, 0, 0.25), 0 3px 2px rgba(0, 0, 0, 0.25)", - borderRadius: "50%", - margin: "0", - padding: "0", - boxSizing: "border-box", - webkitAppearance: "none", - appearance: "none", - top: "0", - zIndex: "1", - }, - ".range__input::-moz-range-thumb": { - position: "relative", - height: "28px", - width: "28px", - backgroundColor: "#fff", - border: "none", - boxShadow: "0 0 1px 0 rgba(0, 0, 0, 0.25), 0 3px 2px rgba(0, 0, 0, 0.25)", - borderRadius: "50%", - margin: "0", - padding: "0", - }, - ".range__input::-ms-thumb": { - position: "relative", - height: "28px", - width: "28px", - backgroundColor: "#fff", - border: "none", - boxShadow: "0 0 1px 0 rgba(0, 0, 0, 0.25), 0 3px 2px rgba(0, 0, 0, 0.25)", - borderRadius: "50%", - margin: "0", - padding: "0", - top: "0", - }, - ".range__input::-ms-fill-lower": { - height: "2px", - backgroundColor: theme.palette.primary.main, - }, - ".range__input::-ms-tooltip": { - display: "none", - }, - ".range__input:disabled": { - opacity: "1", - pointerEvents: "none", - }, - ".range__focus-ring": { - pointerEvents: "none", - top: "0", - left: "0", - display: "none", - boxSizing: "border-box", - backgroundClip: "padding-box", - padding: "0", - margin: "0", - font: "inherit", - color: "inherit", - background: "none", - border: "none", - verticalAlign: "top", - outline: "none", - lineHeight: "1", - webkitAppearance: "none", - mozAppearance: "none", - appearance: "none", - - height: "30px", - position: "absolute", - zIndex: "0", - width: "100%", - }, - ".range--disabled": { - opacity: "0.3", - pointerEvents: "none", - }, - ".range--material": { - position: "relative", - backgroundImage: "linear-gradient(#bdbdbd, #bdbdbd)", - }, - ".range--material__input": { - backgroundImage: "linear-gradient(#4a148c, #4a148c)", - backgroundPosition: "center left", - backgroundSize: "0% 2px", - }, - ".range--material__focus-ring": { - display: "block", - }, - ".range--material__focus-ring::-webkit-slider-thumb": { - webkitAppearance: "none", - appearance: "none", - width: "14px", - height: "14px", - border: "none", - boxShadow: "0 0 0 9px #4a148c", - backgroundColor: theme.palette.primary.main, - borderRadius: "50%", - opacity: "0", - transition: "opacity 0.25s ease-out, transform 0.25s ease-out, -webkit-transform 0.25s ease-out", - }, - ".range--material__input.range__input--active + .range--material__focus-ring::-webkit-slider-thumb": { - opacity: "0.2", - webkitTransform: "scale(1.5, 1.5, 1.5)", - transform: "scale(1.5, 1.5, 1.5)", - }, - ".range--material__input::-webkit-slider-thumb": { - position: "relative", - boxSizing: "border-box", - border: "none", - backgroundColor: "transparent", - width: "14px", - height: "32px", - borderRadius: "0", - boxShadow: "none", - backgroundImage: - "radial-gradient(\r\n circle farthest-corner,\r\n #4a148c 0%,\r\n #4a148c 6.6px,\r\n transparent 7px\r\n )", - transition: "transform 0.1s linear, -webkit-transform 0.1s linear", - - overflow: "visible", - }, - ".range--material__input[_zero]::-webkit-slider-thumb": { - backgroundImage: - "radial-gradient(\r\n circle farthest-corner,\r\n #f2f2f2 0%,\r\n #f2f2f2 4px,\r\n #bdbdbd 4px,\r\n #bdbdbd 6.4px,\r\n transparent 7px\r\n )", - }, - ".range--material__input[_zero] + .range--material__focus-ring::-webkit-slider-thumb": { - boxShadow: "0 0 0 9px #bdbdbd", - }, - ".range--material__input::-moz-range-track": { - background: "none", - }, - ".range--material__input::-moz-range-thumb, .range--material__input:focus::-moz-range-thumb": { - boxSizing: "border-box", - border: "none", - width: "14px", - height: "32px", - borderRadius: "0", - backgroundColor: "transparent", - backgroundImage: - "-moz-radial-gradient(\r\n circle farthest-corner,\r\n #4a148c 0%,\r\n #4a148c 6.6px,\r\n transparent 7px\r\n )", - boxShadow: "none", - }, - ".range--material__input:active::-webkit-slider-thumb, .range--material__input.range__input--active::-webkit-slider-thumb": { - webkitTransform: "scale(1.5)", - transform: "scale(1.5)", - transition: "transform 0.1s linear, -webkit-transform 0.1s linear", - }, - ".range--disabled.range--material": { - opacity: "1", - }, - ".range--disabled > .range--material__input": { - backgroundImage: "none", - }, - ".range--material__input:disabled::-webkit-slider-thumb": { - backgroundImage: - "radial-gradient(\r\n circle farthest-corner,\r\n #b0b0b0 0%,\r\n #b0b0b0 4px,\r\n #eeeeee 4.4px,\r\n #eeeeee 7.6px,\r\n transparent 7.6px\r\n )", - transition: "none", - }, - ".range--material__input:disabled::-moz-range-thumb": { - backgroundImage: - "-moz-radial-gradient(\r\n circle farthest-corner,\r\n #b0b0b0 0%,\r\n #b0b0b0 4px,\r\n #eeeeee 4.4px,\r\n #eeeeee 7.6px,\r\n transparent 7.6px\r\n )", - transition: "none", - }, - ".notification": { - position: "relative", - display: "inline-block", - verticalAlign: "top", - font: "inherit", - border: "none", - boxSizing: "border-box", - backgroundClip: "padding-box", - padding: "0 4px", - margin: "0", - - color: "white", - background: "transparent", - lineHeight: "19px", - fontFamily: '-apple-system, "Helvetica Neue", "Helvetica", "Arial", "Lucida Grande", sans-serif', - webkitFontSmoothing: "antialiased", - mozOsxFontSmoothing: "grayscale", - fontWeight: "400", - webkitUserSelect: "none", - mozUserSelect: "none", - msUserSelect: "none", - userSelect: "none", - textOverflow: "ellipsis", - whiteSpace: "nowrap", - overflow: "hidden", - textDecoration: "none", - width: "auto", - height: "19px", - borderRadius: "19px", - backgroundColor: "#fe3824", - textAlign: "center", - fontSize: "16px", - minWidth: "19px", - }, - ".notification:empty": { - display: "none", - }, - ".notification--material": { - fontFamily: '"Roboto", "Noto", sans-serif', - webkitFontSmoothing: "antialiased", - fontWeight: "500", - backgroundColor: theme.palette.primary.main, - fontSize: "16px", - - color: "white", - }, - ".toolbar": { - fontFamily: '-apple-system, "Helvetica Neue", "Helvetica", "Arial", "Lucida Grande", sans-serif', - webkitFontSmoothing: "antialiased", - mozOsxFontSmoothing: "grayscale", - fontWeight: "400", - boxSizing: "border-box", - backgroundClip: "padding-box", - whiteSpace: "nowrap", - overflow: "hidden", - wordSpacing: "0", - padding: "0", - margin: "0", - font: "inherit", - color: "#1f1f21", - background: "#fafafa", - border: "none", - lineHeight: "normal", - webkitUserSelect: "none", - mozUserSelect: "none", - msUserSelect: "none", - userSelect: "none", - zIndex: "2", - display: "flex", - - webkitBoxAlign: "stretch", - webkitAlignItems: "stretch", - alignItems: "stretch", - webkitFlexWrap: "nowrap", - flexWrap: "nowrap", - height: "44px", - paddingLeft: "0", - paddingRight: "0", - boxShadow: "none", - width: "100%", - borderBottom: "none", - backgroundSize: "100% 1px", - backgroundRepeat: "no-repeat", - backgroundPosition: "bottom", - backgroundImage: "linear-gradient(0deg, #b2b2b2, #b2b2b2 100%)", - top: "0", - paddingTop: "0", - }, - "@media (-webkit-min-device-pixel-ratio: 2), (min-resolution: 192dpi), (min-resolution: 2dppx)": { - ".toolbar": { - backgroundImage: "linear-gradient(0deg, #b2b2b2, #b2b2b2 50%, transparent 50%)", - }, - ".bottom-bar": { - backgroundImage: "linear-gradient(180deg, #b2b2b2, #b2b2b2 50%, transparent 50%)", - }, - ".tabbar": { - borderTop: "none", - backgroundSize: "100% 1px", - backgroundRepeat: "no-repeat", - backgroundPosition: "top", - backgroundImage: "linear-gradient(180deg, #ccc, #ccc 50%, transparent 50%)", - }, - ".tabbar__button": { - borderTop: "none", - }, - ".tabbar--noshadow": { - boxShadow: "none", - backgroundImage: "none", - borderBottom: "none", - }, - ".tabbar--top": { - borderBottom: "none", - backgroundSize: "100% 1px", - backgroundRepeat: "no-repeat", - backgroundPosition: "bottom", - backgroundImage: "linear-gradient(0deg, #ccc, #ccc 50%, transparent 50%)", - }, - ".list": { - backgroundImage: - "linear-gradient(0deg, #ccc, #ccc 50%, transparent 50%),\r\n linear-gradient(180deg, #ccc, #ccc 50%, transparent 50%)", - }, - ".list-item--expandable": { - backgroundImage: "linear-gradient(0deg, #ccc, #ccc 50%, transparent 50%)", - }, - ".list-item__center": { - backgroundImage: "linear-gradient(0deg, #ccc, #ccc 50%, transparent 50%)", - }, - ".list-item__right": { - backgroundImage: "linear-gradient(0deg, #ccc, #ccc 50%, transparent 50%)", - }, - ".list-header": { - backgroundImage: "linear-gradient(180deg, #ccc, #ccc 50%, transparent 50%)", - }, - ".list-item--material__left:empty, .list-item--material__center": { - backgroundImage: theme.palette.background.default, - }, - ".list-item--material__right": { - backgroundImage: "linear-gradient(0deg, #eee, #eee 50%, transparent 50%)", - }, - ".list-item--material.list-item--expandable": { - backgroundImage: "linear-gradient(0deg, #eee, #eee 50%, transparent 50%)", - }, - ".list-item--material.list-item--longdivider, .list-item--material.list-item--expandable.list-item--longdivider": { - backgroundImage: "linear-gradient(0deg, #eee, #eee 50%, transparent 50%)", - }, - ".list-header--material:not(:first-of-type)": { - backgroundImage: "linear-gradient(180deg, #eee, #eee 50%, transparent 50%)", - }, - ".list-item--longdivider": { - backgroundImage: "linear-gradient(0deg, #ccc, #ccc 50%, transparent 50%)", - }, - ".alert-dialog-button": { - borderTop: "none", - backgroundSize: "100% 1px", - backgroundRepeat: "no-repeat", - backgroundPosition: "top", - backgroundImage: "linear-gradient(180deg, #ddd, #ddd 50%, transparent 50%)", - }, - ".alert-dialog-button--rowfooter": { - borderTop: "none", - borderLeft: "none", - backgroundSize: "100% 1px, 1px 100%", - backgroundRepeat: "no-repeat", - backgroundPosition: "top, left", - backgroundImage: - "linear-gradient(0deg, transparent, transparent 50%, #ddd 50%),\r\n linear-gradient(90deg, transparent, transparent 50%, #ddd 50%)", - }, - ".alert-dialog-button--rowfooter:first-of-type": { - borderTop: "none", - backgroundSize: "100% 1px", - backgroundRepeat: "no-repeat", - backgroundPosition: "top, left", - backgroundImage: "linear-gradient(0deg, transparent, transparent 50%, #ddd 50%)", - }, - ".alert-dialog-button--material": { - background: "none", - }, - ".alert-dialog-button--rowfooter--material, .alert-dialog-button--rowfooter--material:first-of-type": { - background: "none", - }, - ".alert-dialog-button--primal--material": { - background: "none", - }, - ".action-sheet-button": { - backgroundImage: - "linear-gradient(\r\n 0deg,\r\n rgba(0, 0, 0, 0.1),\r\n rgba(0, 0, 0, 0.1) 50%,\r\n transparent 50%\r\n )", - }, - ".action-sheet-title": { - backgroundImage: - "linear-gradient(\r\n 0deg,\r\n rgba(0, 0, 0, 0.1),\r\n rgba(0, 0, 0, 0.1) 50%,\r\n transparent 50%\r\n )", - }, - }, - ".toolbar__bg": { - background: "#fafafa", - }, - ".toolbar__item": { - boxSizing: "border-box", - backgroundClip: "padding-box", - padding: "0", - margin: "0", - font: "inherit", - color: "inherit", - background: "transparent", - border: "none", - lineHeight: "normal", - height: "44px", - overflow: "visible", - display: "block", - verticalAlign: "middle", - }, - ".toolbar__left": { - boxSizing: "border-box", - backgroundClip: "padding-box", - padding: "0", - margin: "0", - font: "inherit", - color: "inherit", - background: "transparent", - border: "none", - lineHeight: "44px", - maxWidth: "50%", - width: "27%", - textAlign: "left", - }, - ".toolbar__right": { - boxSizing: "border-box", - backgroundClip: "padding-box", - padding: "0", - margin: "0", - font: "inherit", - color: "inherit", - background: "transparent", - border: "none", - lineHeight: "44px", - maxWidth: "50%", - width: "27%", - textAlign: "right", - }, - ".toolbar__center": { - boxSizing: "border-box", - backgroundClip: "padding-box", - padding: "0", - margin: "0", - font: "inherit", - color: "#1f1f21", - background: "transparent", - border: "none", - lineHeight: "44px", - width: "46%", - textAlign: "center", - - fontSize: "17px", - fontWeight: "500", - }, - ".toolbar__title": { - lineHeight: "44px", - fontSize: "17px", - fontWeight: "500", - color: "#1f1f21", - margin: "0", - padding: "0", - overflow: "visible", - }, - ".toolbar__center:first-of-type:last-child": { - width: "100%", - }, - ".bottom-bar": { - fontFamily: '-apple-system, "Helvetica Neue", "Helvetica", "Arial", "Lucida Grande", sans-serif', - webkitFontSmoothing: "antialiased", - mozOsxFontSmoothing: "grayscale", - fontWeight: "400", - boxSizing: "border-box", - backgroundClip: "padding-box", - whiteSpace: "nowrap", - overflow: "hidden", - wordSpacing: "0", - padding: "0", - margin: "0", - font: "inherit", - color: "#1f1f21", - background: "#fafafa", - border: "none", - lineHeight: "normal", - webkitUserSelect: "none", - mozUserSelect: "none", - msUserSelect: "none", - userSelect: "none", - zIndex: "2", - display: "block", - paddingBottom: view.getWindowBottomInsets(), - height: `calc(44px + ${view.getWindowBottomInsets()}px)`, - paddingLeft: "0", - paddingRight: "0", - - boxShadow: "none", - borderBottom: "none", - borderTop: "none", - position: "absolute", - bottom: "0", - right: "0", - left: "0", - backgroundSize: "100% 1px", - backgroundRepeat: "no-repeat", - backgroundPosition: "top", - backgroundImage: "linear-gradient(180deg, #b2b2b2, #b2b2b2 100%)", - }, - ".bottom-bar__line-height": { - lineHeight: "44px", - paddingBottom: "0", - paddingTop: "0", - }, - ".bottom-bar--aligned": { - display: "flex", - - webkitFlexWrap: "nowrap", - flexWrap: "nowrap", - lineHeight: "44px", - }, - ".bottom-bar--transparent": { - backgroundColor: "transparent", - backgroundImage: "none", - border: "none", - }, - ".toolbar--material": { - display: "flex", - webkitFlexWrap: "nowrap", - flexWrap: "nowrap", - webkitBoxPack: "justify", - webkitJustifyContent: "space-between", - justifyContent: "space-between", - paddingTop: view.getWindowTopInsets(), - height: `calc(56px + ${view.getWindowTopInsets()}px)`, - boxShadow: "0 1px 5px rgba(0, 0, 0, 0.3)", - backgroundColor: theme.palette.background.default, - backgroundSize: "0", - }, - ".toolbar--noshadow": { - boxShadow: "none", - backgroundImage: "none", - borderBottom: "none", - }, - ".toolbar--material__left, .toolbar--material__right": { - fontFamily: '"Roboto", "Noto", sans-serif', - webkitFontSmoothing: "antialiased", - fontWeight: "500", - fontSize: "20px", - - color: "rgba(255, 255, 255, 1)", - height: "56px", - minWidth: "72px", - width: "auto", - lineHeight: "56px", - }, - ".toolbar--material__center": { - fontFamily: '"Roboto", "Noto", sans-serif', - webkitFontSmoothing: "antialiased", - fontWeight: "500", - fontSize: "20px", - - color: "rgba(255, 255, 255, 1)", - height: "56px", - width: "auto", - webkitBoxFlex: "1", - webkitFlexGrow: "1", - flexGrow: "1", - overflow: "hidden", - textOverflow: "ellipsis", - textAlign: "left", - lineHeight: "56px", - }, - ".toolbar--material__center:first-of-type": { - marginLeft: "16px", - }, - ".toolbar--material__center:last-child": { - marginRight: "16px", - }, - ".toolbar--material__left:empty, .toolbar--material__right:empty": { - minWidth: "16px", - }, - ".toolbar--transparent": { - backgroundColor: "transparent", - boxShadow: "none", - backgroundImage: "none", - borderBottom: "none", - }, - ".button": { - position: "relative", - display: "inline-block", - boxSizing: "border-box", - backgroundClip: "padding-box", - padding: "4px 10px", - margin: "0", - font: "inherit", - color: "white", - background: "transparent", - border: "0 solid currentColor", - lineHeight: "32px", - fontFamily: '-apple-system, "Helvetica Neue", "Helvetica", "Arial", "Lucida Grande", sans-serif', - webkitFontSmoothing: "antialiased", - mozOsxFontSmoothing: "grayscale", - fontWeight: "400", - webkitUserSelect: "none", - mozUserSelect: "none", - msUserSelect: "none", - userSelect: "none", - textOverflow: "ellipsis", - whiteSpace: "nowrap", - overflow: "hidden", - height: "auto", - textDecoration: "none", - - fontSize: "17px", - letterSpacing: "0", - verticalAlign: "middle", - backgroundColor: theme.palette.primary.main, - borderRadius: "3px", - transition: "none", - }, - ".button::-moz-focus-inner": { - outline: "0", - }, - ".button:hover": { - transition: "none", - }, - ".button:active": { - backgroundColor: theme.palette.primary.main, - transition: "none", - opacity: "0.2", - }, - ".button:focus": { - outline: "0", - }, - ".button:disabled, .button[disabled]": { - opacity: "0.3", - pointerEvents: "none", - }, - ".button--outline": { - backgroundColor: "transparent", - border: `1px solid ${theme.palette.primary.main}`, - color: theme.palette.primary.main, - }, - ".button--outline:active": { - backgroundColor: "color-mod(#4a148c tint(70%))", - border: `1px solid ${theme.palette.primary.main}`, - color: theme.palette.primary.main, - opacity: "1", - }, - ".button--outline:hover": { - border: `1px solid ${theme.palette.primary.main}`, - transition: "0", - }, - ".button--light": { - backgroundColor: "transparent", - color: "color-mod(black a(40%))", - border: "1px solid color-mod(black a(20%))", - }, - ".button--light:active": { - backgroundColor: "color-mod(black a(5%))", - color: "color-mod(black a(40%))", - border: "1px solid color-mod(black a(20%))", - opacity: "1", - }, - ".button--quiet": { - position: "relative", - display: "inline-block", - boxSizing: "border-box", - backgroundClip: "padding-box", - padding: "4px 10px", - margin: "0", - font: "inherit", - color: theme.palette.primary.main, - background: "transparent", - border: "none", - lineHeight: "32px", - fontFamily: '-apple-system, "Helvetica Neue", "Helvetica", "Arial", "Lucida Grande", sans-serif', - webkitFontSmoothing: "antialiased", - mozOsxFontSmoothing: "grayscale", - fontWeight: "400", - webkitUserSelect: "none", - mozUserSelect: "none", - msUserSelect: "none", - userSelect: "none", - textOverflow: "ellipsis", - whiteSpace: "nowrap", - overflow: "hidden", - height: "auto", - textDecoration: "none", - - fontSize: "17px", - letterSpacing: "0", - verticalAlign: "middle", - backgroundColor: theme.palette.primary.main, - borderRadius: "3px", - transition: "none", - boxShadow: "none", - }, - ".button--quiet:disabled, .button--quiet[disabled]": { - opacity: "0.3", - pointerEvents: "none", - border: "none", - }, - ".button--quiet:hover": { - transition: "none", - }, - ".button--quiet:focus": { - outline: "0", - }, - ".button--quiet:active": { - backgroundColor: "transparent", - border: "none", - transition: "none", - opacity: "0.2", - color: theme.palette.primary.main, - }, - ".button--cta": { - position: "relative", - display: "inline-block", - boxSizing: "border-box", - backgroundClip: "padding-box", - padding: "4px 10px", - margin: "0", - font: "inherit", - color: "white", - background: "transparent", - border: "none", - lineHeight: "32px", - fontFamily: '-apple-system, "Helvetica Neue", "Helvetica", "Arial", "Lucida Grande", sans-serif', - webkitFontSmoothing: "antialiased", - mozOsxFontSmoothing: "grayscale", - fontWeight: "400", - webkitUserSelect: "none", - mozUserSelect: "none", - msUserSelect: "none", - userSelect: "none", - textOverflow: "ellipsis", - whiteSpace: "nowrap", - overflow: "hidden", - height: "auto", - textDecoration: "none", - - fontSize: "17px", - letterSpacing: "0", - verticalAlign: "middle", - backgroundColor: "#7c43bd", - borderRadius: "3px", - transition: "none", - }, - ".button--cta:hover": { - transition: "none", - }, - ".button--cta:focus": { - outline: "0", - }, - ".button--cta:active": { - color: "white", - backgroundColor: "#7c43bd", - transition: "none", - opacity: "0.2", - }, - ".button--cta:disabled, .button--cta[disabled]": { - opacity: "0.3", - pointerEvents: "none", - }, - ".button--large": { - fontSize: "17px", - fontWeight: "500", - lineHeight: "36px", - padding: "4px 12px", - display: "block", - width: "100%", - textAlign: "center", - }, - ".button--large:active": { - backgroundColor: theme.palette.primary.main, - transition: "none", - opacity: "0.2", - }, - ".button--large:disabled, .button--large[disabled]": { - opacity: "0.3", - pointerEvents: "none", - }, - ".button--large:hover": { - transition: "none", - }, - ".button--large:focus": { - outline: "0", - }, - ".button--large--quiet": { - position: "relative", - display: "block", - boxSizing: "border-box", - backgroundClip: "padding-box", - padding: "4px 12px", - margin: "0", - font: "inherit", - color: "#4a148c", - background: "transparent", - border: "1px solid transparent", - lineHeight: "36px", - fontFamily: '-apple-system, "Helvetica Neue", "Helvetica", "Arial", "Lucida Grande", sans-serif', - webkitFontSmoothing: "antialiased", - mozOsxFontSmoothing: "grayscale", - fontWeight: "500", - webkitUserSelect: "none", - mozUserSelect: "none", - msUserSelect: "none", - userSelect: "none", - textOverflow: "ellipsis", - whiteSpace: "nowrap", - overflow: "hidden", - height: "auto", - textDecoration: "none", - - fontSize: "17px", - letterSpacing: "0", - verticalAlign: "middle", - backgroundColor: theme.palette.primary.main, - borderRadius: "3px", - transition: "none", - width: "100%", - boxShadow: "none", - textAlign: "center", - }, - ".button--large--quiet:active": { - transition: "none", - opacity: "0.2", - color: "#4a148c", - background: "transparent", - border: "1px solid transparent", - boxShadow: "none", - }, - ".button--large--quiet:disabled, .button--large--quiet[disabled]": { - opacity: "0.3", - pointerEvents: "none", - }, - ".button--large--quiet:hover": { - transition: "none", - }, - ".button--large--quiet:focus": { - outline: "0", - }, - ".button--large--cta": { - position: "relative", - display: "block", - boxSizing: "border-box", - backgroundClip: "padding-box", - padding: "4px 12px", - margin: "0", - font: "inherit", - color: "white", - background: "transparent", - border: "none", - lineHeight: "36px", - fontFamily: '-apple-system, "Helvetica Neue", "Helvetica", "Arial", "Lucida Grande", sans-serif', - webkitFontSmoothing: "antialiased", - mozOsxFontSmoothing: "grayscale", - fontWeight: "500", - webkitUserSelect: "none", - mozUserSelect: "none", - msUserSelect: "none", - userSelect: "none", - textOverflow: "ellipsis", - whiteSpace: "nowrap", - overflow: "hidden", - height: "auto", - textDecoration: "none", - - fontSize: "17px", - letterSpacing: "0", - verticalAlign: "middle", - backgroundColor: "#7c43bd", - borderRadius: "3px", - transition: "none", - width: "100%", - textAlign: "center", - }, - ".button--large--cta:hover": { - transition: "none", - }, - ".button--large--cta:focus": { - outline: "0", - }, - ".button--large--cta:active": { - color: "white", - backgroundColor: "#7c43bd", - transition: "none", - opacity: "0.2", - }, - ".button--large--cta:disabled, .button--large--cta[disabled]": { - opacity: "0.3", - pointerEvents: "none", - }, - ".button--material": { - position: "relative", - display: "inline-block", - boxSizing: "border-box", - backgroundClip: "padding-box", - padding: "0 16px", - margin: "0", - font: "inherit", - color: "#ffffff", - background: "transparent", - border: "0 solid currentColor", - lineHeight: "36px", - fontFamily: '"Roboto", "Noto", sans-serif', - webkitFontSmoothing: "antialiased", - mozOsxFontSmoothing: "grayscale", - fontWeight: "500", - webkitUserSelect: "none", - mozUserSelect: "none", - msUserSelect: "none", - userSelect: "none", - textOverflow: "ellipsis", - whiteSpace: "nowrap", - overflow: "hidden", - height: "auto", - textDecoration: "none", - - fontSize: "14px", - letterSpacing: "0", - verticalAlign: "middle", - backgroundColor: theme.palette.primary.main, - borderRadius: theme.shape.borderRadius, - transition: "all 0.25s linear", - // boxShadow: - // "0 2px 2px 0 rgba(0, 0, 0, 0.14), 0 1px 5px 0 rgba(0, 0, 0, 0.12),\r\n 0 3px 1px -2px rgba(0, 0, 0, 0.2)", - minHeight: "36px", - textAlign: "center", - webkitTransform: "translate3d(0, 0, 0)", - transform: "translate3d(0, 0, 0)", - textTransform: "uppercase", - opacity: "1", - }, - ".button--material:hover": { - transition: "all 0.25s linear", - }, - ".button--material:active": { - // boxShadow: - // "0 6px 10px 0 rgba(0, 0, 0, 0.14), 0 1px 18px 0 rgba(0, 0, 0, 0.12),\r\n 0 3px 5px -1px rgba(0, 0, 0, 0.4)", - backgroundColor: theme.palette.primary.main, - opacity: "0.9", - transition: "all 0.25s linear", - }, - ".button--material:focus": { - outline: "0", - }, - ".button--material:disabled, .button--material[disabled]": { - transition: "none", - boxShadow: "none", - backgroundColor: theme.palette.primary.main, - filter: "brightness(85%)", - color: "color-mod(black a(26%))", - opacity: "1", - }, - ".button--material--flat": { - position: "relative", - display: "inline-block", - boxSizing: "border-box", - backgroundClip: "padding-box", - padding: "0 16px", - margin: "0", - font: "inherit", - color: "#4a148c", - background: "transparent", - border: "0 solid currentColor", - lineHeight: "36px", - fontFamily: '"Roboto", "Noto", sans-serif', - webkitFontSmoothing: "antialiased", - mozOsxFontSmoothing: "grayscale", - fontWeight: "500", - webkitUserSelect: "none", - mozUserSelect: "none", - msUserSelect: "none", - userSelect: "none", - textOverflow: "ellipsis", - whiteSpace: "nowrap", - overflow: "hidden", - height: "auto", - textDecoration: "none", - - fontSize: "14px", - letterSpacing: "0", - verticalAlign: "middle", - backgroundColor: "transparent", - borderRadius: "3px", - transition: "all 0.25s linear", - boxShadow: "none", - minHeight: "36px", - textAlign: "center", - webkitTransform: "translate3d(0, 0, 0)", - transform: "translate3d(0, 0, 0)", - textTransform: "uppercase", - }, - ".button--material--flat:hover": { - transition: "all 0.25s linear", - }, - ".button--material--flat:focus": { - boxShadow: "none", - backgroundColor: "transparent", - color: "#4a148c", - outline: "0", - opacity: "1", - border: "none", - }, - ".button--material--flat:active": { - boxShadow: "none", - outline: "0", - opacity: "1", - border: "none", - backgroundColor: "color-mod(#999 a(20%))", - color: "#4a148c", - transition: "all 0.25s linear", - }, - ".button--material--flat:disabled, .button--material--flat[disabled]": { - transition: "none", - opacity: "1", - boxShadow: "none", - backgroundColor: "transparent", - color: "color-mod(black a(26%))", - }, - ".button-bar": { - fontFamily: '-apple-system, "Helvetica Neue", "Helvetica", "Arial", "Lucida Grande", sans-serif', - webkitFontSmoothing: "antialiased", - mozOsxFontSmoothing: "grayscale", - fontWeight: "400", - display: "inline-flex", - - webkitBoxAlign: "stretch", - webkitAlignItems: "stretch", - alignItems: "stretch", - webkitAlignContent: "stretch", - alignContent: "stretch", - webkitFlexWrap: "nowrap", - flexWrap: "nowrap", - margin: "0", - padding: "0", - border: "none", - }, - ".button-bar__item": { - fontFamily: '-apple-system, "Helvetica Neue", "Helvetica", "Arial", "Lucida Grande", sans-serif', - webkitFontSmoothing: "antialiased", - mozOsxFontSmoothing: "grayscale", - fontWeight: "400", - borderRadius: "0", - width: "100%", - padding: "0", - margin: "0", - position: "relative", - overflow: "hidden", - boxSizing: "border-box", - }, - ".button-bar__button": { - fontFamily: '-apple-system, "Helvetica Neue", "Helvetica", "Arial", "Lucida Grande", sans-serif', - webkitFontSmoothing: "antialiased", - mozOsxFontSmoothing: "grayscale", - fontWeight: "400", - borderRadius: "0", - backgroundColor: "transparent", - color: "#4a148c", - border: "1px solid #4a148c", - borderTopWidth: "1px", - borderBottomWidth: "1px", - borderRightWidth: "1px", - borderLeftWidth: "0", - - padding: "0", - fontSize: "13px", - height: "27px", - lineHeight: "27px", - width: "100%", - transition: "background-color 0.2s linear, color 0.2s linear", - boxSizing: "border-box", - }, - ".button-bar__button:disabled": { - opacity: "0.3", - pointerEvents: "none", - }, - ".button-bar__button:hover": { - transition: "none", - }, - ".button-bar__button:focus": { - outline: "0", - }, - ":checked + .button-bar__button": { - backgroundColor: theme.palette.primary.main, - color: "#fff", - transition: "none", - }, - ".button-bar__button:active, :active + .button-bar__button": { - backgroundColor: "color-mod(#4a148c tint(70%))", - border: "0 solid #4a148c", - borderTop: "1px solid #4a148c", - borderBottom: "1px solid #4a148c", - borderRight: "1px solid #4a148c", - fontSize: "13px", - width: "100%", - transition: "none", - }, - ".button-bar__item:first-of-type > .button-bar__button": { - borderLeftWidth: "1px", - borderRadius: "4px 0 0 4px", - }, - ".button-bar__item:last-child > .button-bar__button": { - borderRightWidth: "1px", - borderRadius: "0 4px 4px 0", - }, - ".segment": { - fontFamily: '-apple-system, "Helvetica Neue", "Helvetica", "Arial", "Lucida Grande", sans-serif', - webkitFontSmoothing: "antialiased", - mozOsxFontSmoothing: "grayscale", - fontWeight: "400", - display: "inline-flex", - - webkitBoxAlign: "stretch", - webkitAlignItems: "stretch", - alignItems: "stretch", - webkitAlignContent: "stretch", - alignContent: "stretch", - webkitFlexWrap: "nowrap", - flexWrap: "nowrap", - margin: "0", - padding: "0", - border: "none", - }, - ".segment__item": { - fontFamily: '-apple-system, "Helvetica Neue", "Helvetica", "Arial", "Lucida Grande", sans-serif', - webkitFontSmoothing: "antialiased", - mozOsxFontSmoothing: "grayscale", - fontWeight: "400", - borderRadius: "0", - width: "100%", - padding: "0", - margin: "0", - position: "relative", - overflow: "hidden", - boxSizing: "border-box", - display: "block", - backgroundColor: "transparent", - border: "none", - }, - ".segment__input": { - position: "absolute", - right: "0", - top: "0", - left: "0", - bottom: "0", - padding: "0", - border: "0", - backgroundColor: "transparent", - zIndex: "1", - verticalAlign: "top", - outline: "none", - width: "100%", - height: "100%", - margin: "0", - webkitAppearance: "none", - mozAppearance: "none", - appearance: "none", - }, - ".segment__button": { - fontFamily: '-apple-system, "Helvetica Neue", "Helvetica", "Arial", "Lucida Grande", sans-serif', - webkitFontSmoothing: "antialiased", - mozOsxFontSmoothing: "grayscale", - fontWeight: "400", - borderRadius: "0", - backgroundColor: "transparent", - color: "#4a148c", - border: "1px solid #4a148c", - borderTopWidth: "1px", - borderBottomWidth: "1px", - borderRightWidth: "1px", - borderLeftWidth: "0", - padding: "0", - fontSize: "13px", - height: "29px", - lineHeight: "29px", - width: "100%", - transition: "background-color 0.2s linear, color 0.2s linear", - boxSizing: "border-box", - textAlign: "center", - }, - ".segment__item:disabled": { - opacity: "0.3", - pointerEvents: "none", - }, - ".segment__button:hover": { - transition: "none", - }, - ".segment__button:focus": { - outline: "0", - }, - ":active + .segment__button": { - backgroundColor: "color-mod(#4a148c tint(70%))", - border: "0 solid #4a148c", - borderTop: "1px solid #4a148c", - borderBottom: "1px solid #4a148c", - borderRight: "1px solid #4a148c", - fontSize: "13px", - width: "100%", - transition: "none", - }, - ":checked + .segment__button": { - backgroundColor: theme.palette.primary.main, - color: "#fff", - transition: "none", - }, - ".segment__item:first-of-type > .segment__button": { - borderLeftWidth: "1px", - borderRadius: "4px 0 0 4px", - }, - ".segment__item:last-child > .segment__button": { - borderRightWidth: "1px", - borderRadius: "0 4px 4px 0", - }, - ".segment--material": { - borderRadius: "2px", - overflow: "hidden", - boxShadow: "0 0 2px 0 rgba(0, 0, 0, 0.12), 0 2px 2px 0 rgba(0, 0, 0, 0.24)", - }, - ".segment--material__button": { - fontFamily: '"Roboto", "Noto", sans-serif', - webkitFontSmoothing: "antialiased", - fontWeight: "400", - fontSize: "14px", - height: "32px", - lineHeight: "32px", - borderWidth: "0", - color: "color-mod(black a(38%))", - borderRadius: "0", - backgroundColor: "#fafafa", - }, - ":active + .segment--material__button": { - backgroundColor: "#fafafa", - borderRadius: "0", - borderWidth: "0", - fontSize: "14px", - transition: "none", - color: "color-mod(black a(38%))", - }, - ":checked + .segment--material__button": { - backgroundColor: "#c8c8c8", - color: "#353535", - borderRadius: "0", - borderWidth: "0", - }, - ".segment--material__item:first-of-type > .segment--material__button, .segment--material__item:last-child > .segment--material__button": - { - borderRadius: "0", - borderWidth: "0", - }, - ".tabbar": { - fontFamily: '-apple-system, "Helvetica Neue", "Helvetica", "Arial", "Lucida Grande", sans-serif', - webkitFontSmoothing: "antialiased", - mozOsxFontSmoothing: "grayscale", - fontWeight: "400", - display: "flex", - - position: "absolute", - bottom: "0", - left: "0", - right: "0", - whiteSpace: "nowrap", - margin: "0", - padding: "0", - height: "49px", - backgroundColor: "#fafafa", - borderTop: "none", - width: "100%", - }, - ".tabbar__item": { - fontFamily: '-apple-system, "Helvetica Neue", "Helvetica", "Arial", "Lucida Grande", sans-serif', - webkitFontSmoothing: "antialiased", - mozOsxFontSmoothing: "grayscale", - fontWeight: "400", - position: "relative", - webkitBoxFlex: "1", - webkitFlexGrow: "1", - flexGrow: "1", - webkitFlexBasis: "0", - flexBasis: "0", - width: "auto", - borderRadius: "0", - }, - ".tabbar__item > input": { - position: "absolute", - right: "0", - top: "0", - left: "0", - bottom: "0", - padding: "0", - border: "0", - backgroundColor: "transparent", - zIndex: "1", - verticalAlign: "top", - outline: "none", - width: "100%", - height: "100%", - margin: "0", - webkitAppearance: "none", - mozAppearance: "none", - appearance: "none", - }, - ".tabbar__button": { - fontFamily: '-apple-system, "Helvetica Neue", "Helvetica", "Arial", "Lucida Grande", sans-serif', - webkitFontSmoothing: "antialiased", - mozOsxFontSmoothing: "grayscale", - fontWeight: "400", - boxSizing: "border-box", - backgroundClip: "padding-box", - padding: "0", - margin: "0", - font: "inherit", - color: "#999", - background: "transparent", - border: "none", - lineHeight: "49px", - webkitUserSelect: "none", - mozUserSelect: "none", - msUserSelect: "none", - userSelect: "none", - textOverflow: "ellipsis", - whiteSpace: "nowrap", - overflow: "hidden", - position: "relative", - display: "inline-block", - textDecoration: "none", - - height: "49px", - letterSpacing: "0", - verticalAlign: "top", - backgroundColor: "transparent", - borderTop: "none", - width: "100%", - }, - ".tabbar__icon": { - fontSize: "24px", - padding: "0", - margin: "0", - lineHeight: "26px", - display: "block !important", - height: "28px", - }, - ".tabbar__label": { - fontFamily: '-apple-system, "Helvetica Neue", "Helvetica", "Arial", "Lucida Grande", sans-serif', - webkitFontSmoothing: "antialiased", - mozOsxFontSmoothing: "grayscale", - fontWeight: "400", - display: "inline-block", - }, - ".tabbar__badge.notification": { - verticalAlign: "text-bottom", - top: "-1px", - marginLeft: "5px", - zIndex: "10", - fontSize: "12px", - height: "16px", - minWidth: "16px", - lineHeight: "16px", - borderRadius: "8px", - }, - ".tabbar__icon ~ .tabbar__badge.notification": { - position: "absolute", - top: "5px", - marginLeft: "0", - }, - ".tabbar__icon + .tabbar__label": { - display: "block", - fontSize: "10px", - lineHeight: "1", - margin: "0", - fontWeight: "400", - }, - ".tabbar__label:first-of-type": { - fontSize: "16px", - lineHeight: "49px", - margin: "0", - padding: "0", - }, - ":checked + .tabbar__button": { - color: "#4a148c", - backgroundColor: "transparent", - boxShadow: "none", - borderTop: "none", - }, - ".tabbar__button:disabled": { - opacity: "0.3", - pointerEvents: "none", - }, - ".tabbar__button:focus": { - zIndex: "1", - borderTop: "none", - boxShadow: "none", - outline: "0", - }, - ".tabbar__content": { - position: "absolute", - top: "0", - left: "0", - right: "0", - bottom: "49px", - zIndex: "0", - }, - ".tabbar--autogrow .tabbar__item": { - webkitFlexBasis: "auto", - flexBasis: "auto", - }, - ".tabbar--top": { - position: "relative", - top: "0", - left: "0", - right: "0", - bottom: "auto", - borderTop: "none", - borderBottom: "1px solid #ccc", - paddingTop: "0", - }, - ".tabbar--top__content": { - top: "49px", - left: "0", - right: "0", - bottom: "0", - zIndex: "0", - }, - ".tabbar--top-border__button": { - backgroundColor: "transparent", - borderBottom: "4px solid transparent", - }, - ":checked + .tabbar--top-border__button": { - backgroundColor: "transparent", - borderBottom: "4px solid #4a148c", - }, - ".tabbar__border": { - position: "absolute", - bottom: "0", - left: "0", - width: "0", - height: "4px", - backgroundColor: theme.palette.primary.main, - }, - ".tabbar--material": { - background: "none", - backgroundColor: theme.palette.background.default, - borderBottomWidth: "0", - // boxShadow: "0 4px 2px -2px rgba(0, 0, 0, 0.14), 0 3px 5px -2px rgba(0, 0, 0, 0.12),\r\n 0 5px 1px -4px rgba(0, 0, 0, 0.2)", - }, - ".tabbar--material__button": { - backgroundColor: "transparent", - color: "rgba(255, 255, 255, 1)", - // color - borderBottom: "1px solid #f3f5f726", - textTransform: "uppercase", - fontSize: "14px", - fontWeight: "400", - fontFamily: '"Roboto", "Noto", sans-serif', - webkitFontSmoothing: "antialiased", - }, - ".tabbar--material__button:after": { - content: '""', - display: "block", - width: "0", - height: "2px", - bottom: "0", - position: "absolute", - marginTop: "-2px", - backgroundColor: "rgba(255, 255, 255, 1)", - }, - ":checked + .tabbar--material__button:after": { - width: "100%", - transition: "width 0.2s ease-in-out", - }, - ":checked + .tabbar--material__button": { - backgroundColor: "transparent", - color: "rgba(255, 255, 255, 1)", - }, - ".tabbar--material__item:not([ripple]):active": { - backgroundColor: "rgba(49, 49, 58, 0.1)", - }, - ".tabbar--material__border": { - height: "1px", - backgroundColor: "rgba(255, 255, 255, 1)", - }, - ".tabbar--material__icon": { - fontSize: "22px !important", - lineHeight: "36px", - }, - ".tabbar--material__label": { - fontFamily: '"Roboto", "Noto", sans-serif', - webkitFontSmoothing: "antialiased", - fontWeight: "400", - }, - ".tabbar--material__label:first-of-type": { - fontFamily: '"Roboto", "Noto", sans-serif', - webkitFontSmoothing: "antialiased", - fontWeight: "500", - letterSpacing: "0.015em", - - fontSize: "14px", - }, - ".tabbar--material__icon + .tabbar--material__label": { - fontSize: "10px", - }, - ".toolbar-button": { - fontFamily: '-apple-system, "Helvetica Neue", "Helvetica", "Arial", "Lucida Grande", sans-serif', - webkitFontSmoothing: "antialiased", - mozOsxFontSmoothing: "grayscale", - fontWeight: "400", - padding: "4px 10px", - letterSpacing: "0", - color: "#4a148c", - backgroundColor: "rgba(0, 0, 0, 0)", - - borderRadius: "2px", - border: "1px solid transparent", - fontSize: "17px", - transition: "none", - }, - ".toolbar-button:active": { - backgroundColor: "rgba(0, 0, 0, 0)", - - transition: "none", - opacity: "0.2", - }, - ".toolbar-button:disabled, .toolbar-button[disabled]": { - opacity: "0.3", - pointerEvents: "none", - }, - ".toolbar-button:focus": { - outline: "0", - transition: "none", - }, - ".toolbar-button:hover": { - transition: "none", - }, - ".toolbar-button--outline": { - border: "1px solid #4a148c", - margin: "auto 8px", - paddingLeft: "6px", - paddingRight: "6px", - }, - ".toolbar-button--material": { - fontSize: "22px", - color: "rgba(255, 255, 255, 1)", - - display: "inline-block", - height: "100%", - margin: "0", - border: "none", - borderRadius: "0", - verticalAlign: "initial", - transition: "background-color 0.25s linear", - }, - ".toolbar-button--material:first-of-type": { - marginLeft: "4px", - }, - ".toolbar-button--material:last-of-type": { - marginRight: "4px", - }, - ".toolbar-button--material:active": { - opacity: "1", - transition: "background-color 0.25s linear", - }, - ".back-button": { - height: "44px", - lineHeight: "44px", - paddingLeft: "8px", - color: "#4a148c", - backgroundColor: "rgba(0, 0, 0, 0)", - - display: "inline-block", - }, - ".back-button:active": { - opacity: "0.2", - }, - ".back-button__label": { - display: "inline-block", - height: "100%", - verticalAlign: "top", - lineHeight: "44px", - fontSize: "17px", - fontWeight: "500", - }, - ".back-button__icon": { - marginRight: "6px", - display: "inline-flex", - - fill: "#4a148c", - webkitBoxAlign: "center", - webkitAlignItems: "center", - alignItems: "center", - height: "100%", - }, - ".back-button--material": { - fontSize: "22px", - color: "rgba(255, 255, 255, 1)", - - display: "inline-block", - padding: "0 12px", - height: "100%", - margin: "0 0 0 4px", - border: "none", - borderRadius: "0", - verticalAlign: "initial", - lineHeight: "56px", - }, - ".back-button--material__label": { - display: "none", - fontSize: "20px", - }, - ".back-button--material__icon": { - display: "inline-flex", - - fill: "rgba(255, 255, 255, 1)", - webkitBoxAlign: "center", - webkitAlignItems: "center", - alignItems: "center", - height: "100%", - }, - ".back-button--material:active": { - opacity: "1", - }, - ".checkbox": { - position: "relative", - display: "inline-block", - verticalAlign: "top", - webkitUserSelect: "none", - mozUserSelect: "none", - msUserSelect: "none", - userSelect: "none", - fontFamily: '-apple-system, "Helvetica Neue", "Helvetica", "Arial", "Lucida Grande", sans-serif', - webkitFontSmoothing: "antialiased", - mozOsxFontSmoothing: "grayscale", - fontWeight: "400", - lineHeight: "22px", - }, - ".checkbox__checkmark": { - boxSizing: "border-box", - backgroundClip: "padding-box", - position: "relative", - display: "inline-block", - verticalAlign: "top", - webkitUserSelect: "none", - mozUserSelect: "none", - msUserSelect: "none", - userSelect: "none", - fontFamily: '-apple-system, "Helvetica Neue", "Helvetica", "Arial", "Lucida Grande", sans-serif', - webkitFontSmoothing: "antialiased", - mozOsxFontSmoothing: "grayscale", - fontWeight: "400", - - height: "22px", - width: "22px", - pointerEvents: "none", - }, - ".checkbox__input, .checkbox__input:checked": { - position: "absolute", - right: "0", - top: "0", - left: "0", - bottom: "0", - padding: "0", - border: "0", - backgroundColor: "transparent", - zIndex: "1", - verticalAlign: "top", - outline: "none", - width: "100%", - height: "100%", - margin: "0", - webkitAppearance: "none", - mozAppearance: "none", - appearance: "none", - }, - ".checkbox__checkmark:before": { - content: '""', - position: "absolute", - boxSizing: "border-box", - backgroundClip: "padding-box", - width: "22px", - height: "22px", - background: "transparent", - border: "1px solid #c7c7cd", - borderRadius: "22px", - left: "0", - }, - ".checkbox__checkmark:after": { - content: '""', - position: "absolute", - top: "7px", - left: "5px", - width: "11px", - height: "5px", - background: "transparent", - border: "2px solid #fff", - borderWidth: "1px", - borderTop: "none", - borderRight: "none", - borderRadius: "0", - webkitTransform: "rotate(-45deg)", - transform: "rotate(-45deg)", - opacity: "0", - }, - ":checked + .checkbox__checkmark:before": { - background: "#4a148c", - border: "none", - }, - ":checked + .checkbox__checkmark:after": { - opacity: "1", - }, - ":disabled + .checkbox__checkmark": { - opacity: "0.3", - pointerEvents: "none", - }, - ":disabled:active + .checkbox__checkmark:before": { - background: "transparent", - }, - ".checkbox--noborder": { - position: "relative", - display: "inline-block", - verticalAlign: "top", - webkitUserSelect: "none", - mozUserSelect: "none", - msUserSelect: "none", - userSelect: "none", - fontFamily: '-apple-system, "Helvetica Neue", "Helvetica", "Arial", "Lucida Grande", sans-serif', - webkitFontSmoothing: "antialiased", - mozOsxFontSmoothing: "grayscale", - fontWeight: "400", - lineHeight: "22px", - }, - ".checkbox--noborder__input": { - position: "absolute", - right: "0", - top: "0", - left: "0", - bottom: "0", - padding: "0", - border: "0", - backgroundColor: "transparent", - zIndex: "1", - verticalAlign: "top", - outline: "none", - width: "100%", - height: "100%", - margin: "0", - webkitAppearance: "none", - mozAppearance: "none", - appearance: "none", - }, - ".checkbox--noborder__checkmark": { - position: "relative", - display: "inline-block", - verticalAlign: "top", - webkitUserSelect: "none", - mozUserSelect: "none", - msUserSelect: "none", - userSelect: "none", - boxSizing: "border-box", - backgroundClip: "padding-box", - width: "22px", - height: "22px", - background: "transparent", - border: "none", - }, - ".checkbox--noborder__checkmark:before": { - content: '""', - position: "absolute", - width: "22px", - height: "22px", - background: "transparent", - border: "none", - borderRadius: "22px", - left: "0", - }, - ".checkbox--noborder__checkmark:after": { - content: '""', - position: "absolute", - top: "7px", - left: "4px", - opacity: "0", - width: "11px", - height: "4px", - background: "transparent", - border: "2px solid #4a148c", - borderTop: "none", - borderRight: "none", - borderRadius: "0", - webkitTransform: "rotate(-45deg)", - transform: "rotate(-45deg)", - }, - ":checked + .checkbox--noborder__checkmark:before": { - background: "transparent", - border: "none", - }, - ":checked + .checkbox--noborder__checkmark:after": { - opacity: "1", - }, - ":focus + .checkbox--noborder__checkmark:before": { - border: "none", - }, - ":disabled + .checkbox--noborder__checkmark": { - opacity: "0.3", - pointerEvents: "none", - }, - ":disabled:active + .checkbox--noborder__checkmark:before": { - background: "transparent", - border: "none", - }, - ".checkbox--material": { - lineHeight: "18px", - fontFamily: '"Roboto", "Noto", sans-serif', - webkitFontSmoothing: "antialiased", - fontWeight: "400", - overflow: "visible", - }, - ".checkbox--material__checkmark": { - width: "18px", - height: "18px", - }, - ".checkbox--material__checkmark:before": { - borderRadius: "2px", - height: "18px", - width: "18px", - border: "2px solid #717171", - transition: "background-color 0.1s linear 0.2s, border-color 0.1s linear 0.2s", - backgroundColor: "transparent", - }, - ":checked + .checkbox--material__checkmark:before": { - border: "2px solid #4a148c", - backgroundColor: theme.palette.primary.main, - transition: "background-color 0.1s linear, border-color 0.1s linear", - }, - ".checkbox--material__checkmark:after": { - borderColor: "#ffffff", - transition: "transform 0.2s ease 0, -webkit-transform 0.2s ease 0", - - width: "10px", - height: "5px", - top: "4px", - left: "3px", - webkitTransform: "scale(0) rotate(-45deg)", - transform: "scale(0) rotate(-45deg)", - borderWidth: "2px", - }, - ":checked + .checkbox--material__checkmark:after": { - transition: "transform 0.2s ease 0.2s, -webkit-transform 0.2s ease 0.2s", - - width: "10px", - height: "5px", - top: "4px", - left: "3px", - webkitTransform: "scale(1) rotate(-45deg)", - transform: "scale(1) rotate(-45deg)", - borderWidth: "2px", - }, - ".checkbox--material__input:before": { - content: '""', - opacity: "0", - position: "absolute", - top: "0", - left: "0", - width: "18px", - height: "18px", - boxShadow: "0 0 0 11px #717171", - boxSizing: "border-box", - borderRadius: "50%", - backgroundColor: "#717171", - pointerEvents: "none", - display: "block", - webkitTransform: "scale3d(0.2, 0.2, 0.2)", - transform: "scale3d(0.2, 0.2, 0.2)", - transition: "opacity 0.25s ease-out, transform 0.1s ease-out, -webkit-transform 0.1s ease-out", - }, - ".checkbox--material__input:checked:before": { - boxShadow: "0 0 0 11px #4a148c", - backgroundColor: theme.palette.primary.main, - }, - ".checkbox--material__input:active:before": { - opacity: "0.15", - webkitTransform: "scale3d(1, 1, 1)", - transform: "scale3d(1, 1, 1)", - }, - ":disabled + .checkbox--material__checkmark": { - opacity: "1", - }, - ":disabled + .checkbox--material__checkmark:before": { - borderColor: "#afafaf", - }, - ":disabled:checked + .checkbox--material__checkmark:before": { - backgroundColor: "#afafaf", - }, - ":disabled:checked + .checkbox--material__checkmark:after": { - borderColor: "#ffffff", - }, - ".radio-button__input": { - position: "absolute", - right: "0", - top: "0", - left: "0", - bottom: "0", - padding: "0", - border: "0", - backgroundColor: "transparent", - zIndex: "1", - verticalAlign: "top", - outline: "none", - width: "100%", - height: "100%", - margin: "0", - webkitAppearance: "none", - mozAppearance: "none", - appearance: "none", - }, - ".radio-button__input:active, .radio-button__input:focus": { - outline: "0", - webkitTapHighlightColor: "rgba(0, 0, 0, 0)", - }, - ".radio-button": { - position: "relative", - display: "inline-block", - verticalAlign: "top", - webkitUserSelect: "none", - mozUserSelect: "none", - msUserSelect: "none", - userSelect: "none", - - lineHeight: "24px", - textAlign: "left", - }, - ".radio-button__checkmark:before": { - content: '""', - position: "absolute", - borderRadius: "22px", - boxSizing: "border-box", - backgroundClip: "padding-box", - width: "22px", - height: "22px", - background: "transparent", - border: "none", - - left: "0", - }, - ".radio-button__checkmark": { - boxSizing: "border-box", - backgroundClip: "padding-box", - position: "relative", - display: "inline-block", - verticalAlign: "top", - webkitUserSelect: "none", - mozUserSelect: "none", - msUserSelect: "none", - userSelect: "none", - fontFamily: '-apple-system, "Helvetica Neue", "Helvetica", "Arial", "Lucida Grande", sans-serif', - webkitFontSmoothing: "antialiased", - mozOsxFontSmoothing: "grayscale", - fontWeight: "400", - - width: "24px", - height: "24px", - background: "transparent", - pointerEvents: "none", - }, - ".radio-button__checkmark:after": { - content: '""', - position: "absolute", - top: "7px", - left: "4px", - opacity: "0", - width: "11px", - height: "4px", - background: "transparent", - border: "2px solid #4a148c", - borderTop: "none", - borderRight: "none", - borderRadius: "0", - webkitTransform: "rotate(-45deg)", - transform: "rotate(-45deg)", - }, - ":checked + .radio-button__checkmark": { - background: "rgba(0, 0, 0, 0)", - }, - ":checked + .radio-button__checkmark:after": { - opacity: "1", - }, - ":checked + .radio-button__checkmark:before": { - background: "transparent", - border: "none", - }, - ":disabled + .radio-button__checkmark": { - opacity: "0.3", - pointerEvents: "none", - }, - ".radio-button--material": { - lineHeight: "22px", - fontFamily: '"Roboto", "Noto", sans-serif', - webkitFontSmoothing: "antialiased", - fontWeight: "400", - }, - ".radio-button--material__input:before": { - content: '""', - position: "absolute", - top: "0", - left: "0", - opacity: "0", - width: "20px", - height: "20px", - boxShadow: "0 0 0 14px #717171", - border: "none", - boxSizing: "border-box", - borderRadius: "50%", - backgroundColor: "#717171", - pointerEvents: "none", - display: "block", - webkitTransform: "scale3d(0.2, 0.2, 0.2)", - transform: "scale3d(0.2, 0.2, 0.2)", - transition: "opacity 0.25s ease-out, transform 0.1s ease-out, -webkit-transform 0.1s ease-out", - }, - ".radio-button--material__input:checked:before": { - boxShadow: "0 0 0 14px #4a148c", - backgroundColor: theme.palette.primary.main, - }, - ".radio-button--material__input:active:before": { - opacity: "0.15", - webkitTransform: "scale3d(1, 1, 1)", - transform: "scale3d(1, 1, 1)", - }, - ".radio-button--material__checkmark": { - width: "20px", - height: "20px", - overflow: "visible", - }, - ".radio-button--material__checkmark:before": { - background: "transparent", - border: "2px solid #717171", - boxSizing: "border-box", - borderRadius: "50%", - width: "20px", - height: "20px", - transition: "border 0.2s ease", - }, - ".radio-button--material__checkmark:after": { - transition: "background 0.2s ease, transform 0.2s ease, -webkit-transform 0.2s ease", - - top: "5px", - left: "5px", - width: "10px", - height: "10px", - border: "none", - borderRadius: "50%", - webkitTransform: "scale(0)", - transform: "scale(0)", - }, - ":checked + .radio-button--material__checkmark:before": { - background: "transparent", - border: "2px solid #4a148c", - }, - ".radio-button--material__input + .radio-button__checkmark:after": { - background: "#717171", - opacity: "1", - webkitTransform: "scale(0)", - transform: "scale(0)", - }, - ":checked + .radio-button--material__checkmark:after": { - opacity: "1", - background: "#4a148c", - webkitTransform: "scale(1)", - transform: "scale(1)", - }, - ":disabled + .radio-button--material__checkmark": { - opacity: "1", - }, - ":disabled + .radio-button--material__checkmark:after": { - backgroundColor: "#afafaf", - borderColor: "#afafaf", - }, - ":disabled + .radio-button--material__checkmark:before": { - borderColor: "#afafaf", - }, - ".list": { - padding: "0", - margin: "0", - font: "inherit", - color: "inherit", - background: "transparent", - border: "none", - lineHeight: "normal", - webkitUserSelect: "none", - mozUserSelect: "none", - msUserSelect: "none", - userSelect: "none", - fontFamily: '-apple-system, "Helvetica Neue", "Helvetica", "Arial", "Lucida Grande", sans-serif', - webkitFontSmoothing: "antialiased", - mozOsxFontSmoothing: "grayscale", - fontWeight: "400", - listStyleType: "none", - textAlign: "left", - display: "block", - webkitOverflowScrolling: "touch", - overflow: "hidden", - backgroundImage: "linear-gradient(#ccc, #ccc), linear-gradient(#ccc, #ccc)", - backgroundSize: "100% 1px, 100% 1px", - backgroundRepeat: "no-repeat", - backgroundPosition: "bottom, top", - - backgroundColor: "#fff", - }, - ".list-item": { - position: "relative", - width: "100%", - listStyle: "none", - boxSizing: "border-box", - display: "flex", - webkitBoxOrient: "horizontal", - webkitBoxDirection: "normal", - webkitFlexDirection: "row", - flexDirection: "row", - webkitBoxPack: "start", - webkitJustifyContent: "flex-start", - justifyContent: "flex-start", - webkitBoxAlign: "center", - webkitAlignItems: "center", - alignItems: "center", - padding: "0 0 0 14px", - margin: "0 0 -1px 0", - // color: "#1f1f21", - color: theme.palette.text.primary, - transition: "background-color 0.2s linear", - }, - ".list-item__top": { - display: "flex", - - webkitBoxOrient: "horizontal", - webkitBoxDirection: "normal", - webkitFlexDirection: "row", - flexDirection: "row", - webkitBoxPack: "start", - webkitJustifyContent: "flex-start", - justifyContent: "flex-start", - webkitBoxAlign: "center", - webkitAlignItems: "center", - alignItems: "center", - webkitBoxOrdinalGroup: "1", - webkitOrder: "0", - order: "0", - width: "100%", - }, - ".list-item--expandable": { - display: "flex", - - webkitBoxOrient: "vertical", - webkitBoxDirection: "normal", - webkitFlexDirection: "column", - flexDirection: "column", - borderBottom: "none", - backgroundSize: "100% 1px", - backgroundRepeat: "no-repeat", - backgroundPosition: "bottom", - backgroundImage: "linear-gradient(0deg, #ccc, #ccc 100%)", - backgroundPositionX: "14px", - }, - ".list-item__expandable-content": { - display: "none", - width: "100%", - padding: "12px 14px 12px 0", - boxSizing: "border-box", - webkitBoxOrdinalGroup: "2", - webkitOrder: "1", - order: "1", - overflow: "hidden", - }, - ".list-item.expanded > .list-item__expandable-content": { - display: "block", - height: "auto", - }, - ".list-item__left": { - boxSizing: "border-box", - display: "flex", - - padding: "12px 14px 12px 0", - webkitBoxOrdinalGroup: "1", - webkitOrder: "0", - order: "0", - webkitBoxAlign: "center", - webkitAlignItems: "center", - alignItems: "center", - webkitAlignSelf: "stretch", - alignSelf: "stretch", - lineHeight: "1.2em", - minHeight: "44px", - }, - ".list-item__left:empty": { - width: "0", - minWidth: "0", - padding: "0", - margin: "0", - }, - ".list-item__center": { - boxSizing: "border-box", - display: "flex", - - webkitBoxFlex: "1", - webkitFlexGrow: "1", - flexGrow: "1", - webkitFlexWrap: "wrap", - flexWrap: "wrap", - webkitBoxOrient: "horizontal", - webkitBoxDirection: "normal", - webkitFlexDirection: "row", - flexDirection: "row", - webkitBoxOrdinalGroup: "2", - webkitOrder: "1", - order: "1", - marginRight: "auto", - webkitBoxAlign: "center", - webkitAlignItems: "center", - alignItems: "center", - webkitAlignSelf: "stretch", - alignSelf: "stretch", - marginLeft: "0", - borderBottom: "none", - backgroundSize: "100% 1px", - backgroundRepeat: "no-repeat", - backgroundPosition: "bottom", - backgroundImage: "linear-gradient(0deg, #ccc, #ccc 100%)", - padding: "12px 6px 12px 0", - lineHeight: "1.2em", - minHeight: "44px", - }, - ".list-item__right": { - boxSizing: "border-box", - display: "flex", - - marginLeft: "auto", - padding: "12px 12px 12px 0", - webkitBoxOrdinalGroup: "3", - webkitOrder: "2", - order: "2", - webkitBoxAlign: "center", - webkitAlignItems: "center", - alignItems: "center", - webkitAlignSelf: "stretch", - alignSelf: "stretch", - borderBottom: "none", - backgroundSize: "100% 1px", - backgroundRepeat: "no-repeat", - backgroundPosition: "bottom", - backgroundImage: "linear-gradient(0deg, #ccc, #ccc 100%)", - lineHeight: "1.2em", - minHeight: "44px", - }, - ".list-header": { - margin: "0", - listStyle: "none", - textAlign: "left", - display: "block", - boxSizing: "border-box", - padding: "0 0 0 15px", - fontSize: "12px", - fontWeight: "500", - color: "#1f1f21", - minHeight: "24px", - lineHeight: "25px", - textTransform: "uppercase", - position: "relative", - backgroundColor: "#eee", - backgroundSize: "100% 1px", - backgroundRepeat: "no-repeat", - backgroundPosition: "top", - backgroundImage: "linear-gradient(0deg, #ccc, #ccc 100%)", - }, - ".list--noborder": { - borderTop: "none", - borderBottom: "none", - backgroundImage: "none", - }, - ".list-item--tappable:active": { - transition: "none", - backgroundColor: "#d9d9d9", - }, - ".list--inset": { - margin: "0 8px", - border: "1px solid #ccc", - borderRadius: "4px", - backgroundImage: "none", - }, - ".list-item__label": { - fontSize: "14px", - padding: "0 4px", - opacity: "0.6", - }, - ".list-item__title": { - webkitFlexBasis: "100%", - flexBasis: "100%", - webkitAlignSelf: "flex-end", - alignSelf: "flex-end", - webkitBoxOrdinalGroup: "1", - webkitOrder: "0", - order: "0", - }, - ".list-item__subtitle": { - opacity: "0.75", - fontSize: "14px", - webkitBoxOrdinalGroup: "2", - webkitOrder: "1", - order: "1", - webkitFlexBasis: "100%", - flexBasis: "100%", - webkitAlignSelf: "flex-start", - alignSelf: "flex-start", - }, - ".list-item__thumbnail": { - width: "40px", - height: "40px", - borderRadius: "6px", - display: "block", - margin: "0", - }, - ".list-item__icon": { - fontSize: "22px", - padding: "0 6px", - }, - ".list--material": { - fontFamily: '"Roboto", "Noto", sans-serif', - webkitFontSmoothing: "antialiased", - fontWeight: "400", - backgroundImage: "none", - backgroundColor: theme.palette.background.default, - }, - ".list-item--material": { - border: "0", - padding: "0 0 0 16px", - lineHeight: "normal", - }, - ".list-item--material__subtitle": { - marginTop: "4px", - }, - ".list-item--material:first-of-type": { - boxShadow: "none", - }, - ".list-item--material__left": { - padding: "14px 0", - minWidth: "56px", - lineHeight: "1", - minHeight: "48px", - }, - ".list-item--material__left:empty, .list-item--material__center": { - padding: "14px 6px 14px 0", - borderColor: "#eee", - borderBottom: "none", - backgroundSize: "100% 1px", - backgroundRepeat: "no-repeat", - backgroundPosition: "bottom", - backgroundImage: `linear-gradient(0deg, ${theme.palette.divider}, ${theme.palette.divider} 100%)`, - minHeight: "48px", - }, - ".list-item--material__right": { - padding: "14px 16px 14px 0", - lineHeight: "1", - borderColor: "#eee", - borderBottom: "none", - backgroundSize: "100% 1px", - backgroundRepeat: "no-repeat", - backgroundPosition: "bottom", - backgroundImage: `linear-gradient(0deg, ${theme.palette.divider}, ${theme.palette.divider} 100%)`, - minHeight: "48px", - }, - ".list-item--material.list-item--expandable": { - borderBottom: "none", - - backgroundSize: "100% 1px", - backgroundRepeat: "no-repeat", - backgroundPosition: "bottom", - backgroundImage: `linear-gradient(0deg, ${theme.palette.divider}, ${theme.palette.divider} 100%)`, - backgroundPositionX: "16px", - }, - ".list-item--material.list-item--longdivider, .list-item--material.list-item--expandable.list-item--longdivider": { - borderBottom: "none", - - backgroundSize: "100% 1px", - backgroundRepeat: "no-repeat", - backgroundPosition: "bottom", - backgroundImage: `linear-gradient(0deg, ${theme.palette.divider}, ${theme.palette.divider} 100%)`, - }, - ".list-header--material": { - background: theme.palette.background.default, - border: "none", - fontSize: "14px", - textTransform: "none", - margin: "-1px 0 0 0", - color: "#757575", - fontWeight: "500", - padding: "8px 16px", - }, - ".list-header--material:not(:first-of-type)": { - borderTop: "none", - backgroundSize: "100% 1px", - backgroundRepeat: "no-repeat", - backgroundPosition: "top", - backgroundImage: `linear-gradient(0deg, ${theme.palette.divider}, ${theme.palette.divider} 100%)`, - paddingTop: "16px", - }, - ".list-item--material__thumbnail": { - width: "40px", - height: "40px", - borderRadius: "100%", - }, - ".list-item--material__icon": { - fontSize: "20px", - padding: "0 4px", - }, - ".list-item--chevron:before, .list-item__expand-chevron": { - borderRight: "2px solid #c7c7cc", - borderBottom: "2px solid #c7c7cc", - width: "7px", - height: "7px", - backgroundColor: "transparent", - zIndex: "5", - }, - ".list-item--chevron:before": { - position: "absolute", - content: '""', - right: "16px", - top: "50%", - webkitTransform: "translateY(-50%) rotate(-45deg)", - transform: "translateY(-50%) rotate(-45deg)", - }, - ".list-item__expand-chevron": { - webkitTransform: "rotate(45deg)", - transform: "rotate(45deg)", - margin: "1px", - }, - ".list-item--expandable.expanded .list-item__expand-chevron": { - webkitTransform: "rotate(225deg)", - transform: "rotate(225deg)", - }, - ".list-item--chevron__right": { - paddingRight: "30px", - }, - ".list-item--nodivider__center, .list-item--nodivider__right, .list-item--nodivider.list-item--expandable, .list-item--expandable .list-item__center, .list-item--expandable .list-item__right": - { - border: "none", - backgroundImage: "none", - }, - ".list-item--longdivider": { - borderBottom: "none", - - backgroundSize: "100% 1px", - backgroundRepeat: "no-repeat", - backgroundPosition: "bottom", - backgroundImage: "linear-gradient(0deg, #ccc, #ccc 100%)", - }, - ".list-item--longdivider:last-of-type": { - border: "none", - backgroundImage: "none", - }, - ".list-item--longdivider__center": { - border: "none", - backgroundImage: "none", - }, - ".list-item--longdivider__right": { - border: "none", - backgroundImage: "none", - }, - ".list-title": { - padding: "0 0 0 16px", - margin: "0", - font: "inherit", - color: "#6d6d72", - background: "transparent", - border: "none", - lineHeight: "24px", - webkitUserSelect: "none", - mozUserSelect: "none", - msUserSelect: "none", - userSelect: "none", - fontFamily: '-apple-system, "Helvetica Neue", "Helvetica", "Arial", "Lucida Grande", sans-serif', - webkitFontSmoothing: "antialiased", - mozOsxFontSmoothing: "grayscale", - fontWeight: "500", - display: "block", - - textAlign: "left", - boxSizing: "border-box", - fontSize: "13px", - textTransform: "uppercase", - letterSpacing: "0.04em", - }, - ".list-title--material": { - fontFamily: '"Roboto", "Noto", sans-serif', - webkitFontSmoothing: "antialiased", - fontWeight: "500", - color: "#757575", - fontSize: "14px", - margin: "0", - padding: "12px 0 12px 16px", - - lineHeight: "24px", - }, - ".search-input": { - boxSizing: "border-box", - backgroundClip: "padding-box", - padding: "0 8px 0 28px", - margin: "0", - "border-radius": "8px", - font: "inherit", - color: "#1f1f21", - background: "transparent", - border: "none", - verticalAlign: "top", - outline: "none", - lineHeight: "1.3", - fontFamily: '-apple-system, "Helvetica Neue", "Helvetica", "Arial", "Lucida Grande", sans-serif', - webkitFontSmoothing: "antialiased", - mozOsxFontSmoothing: "grayscale", - fontWeight: "400", - webkitAppearance: "textfield", - mozAppearance: "textfield", - appearance: "textfield", - - height: "28px", - fontSize: "14px", - backgroundColor: "rgba(3, 3, 3, 0.09)", - boxShadow: "none", - borderRadius: "5.5px", - backgroundImage: - 'url("data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz4KPHN2ZyB3aWR0aD0iMTNweCIgaGVpZ2h0PSIxNHB4IiB2aWV3Qm94PSIwIDAgMTMgMTQiIHZlcnNpb249IjEuMSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayI+CiAgICA8IS0tIEdlbmVyYXRvcjogU2tldGNoIDQyICgzNjc4MSkgLSBodHRwOi8vd3d3LmJvaGVtaWFuY29kaW5nLmNvbS9za2V0Y2ggLS0+CiAgICA8dGl0bGU+aW9zLXNlYXJjaC1pbnB1dC1pY29uPC90aXRsZT4KICAgIDxkZXNjPkNyZWF0ZWQgd2l0aCBTa2V0Y2guPC9kZXNjPgogICAgPGRlZnM+PC9kZWZzPgogICAgPGcgaWQ9ImNvbXBvbmVudHMiIHN0cm9rZT0ibm9uZSIgc3Ryb2tlLXdpZHRoPSIxIiBmaWxsPSJub25lIiBmaWxsLXJ1bGU9ImV2ZW5vZGQiPgogICAgICAgIDxnIGlkPSJpb3Mtc2VhcmNoLWlucHV0IiB0cmFuc2Zvcm09InRyYW5zbGF0ZSgtNDguMDAwMDAwLCAtNDMuMDAwMDAwKSIgZmlsbD0iIzdBNzk3QiI+CiAgICAgICAgICAgIDxnIGlkPSJHcm91cCIgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoNDAuMDAwMDAwLCAzNi4wMDAwMDApIj4KICAgICAgICAgICAgICAgIDxwYXRoIGQ9Ik0xNi45OTcyNDgyLDE1LjUwNDE0NjYgQzE3LjA3NzM2NTcsMTUuNTQwNTkzOCAxNy4xNTIyNzMxLDE1LjU5MTYxMjkgMTcuMjE3NzUxNiwxNS42NTcwOTE0IEwyMC42NDk5OTEsMTkuMDg5MzMwOCBDMjAuOTQ0ODQ0OSwxOS4zODQxODQ3IDIwLjk0ODQ3NjQsMTkuODU4NjA2IDIwLjY1MzU0MTIsMjAuMTUzNTQxMiBDMjAuMzYwNjQ4LDIwLjQ0NjQzNDQgMTkuODgxMjcxNiwyMC40NDE5MzE3IDE5LjU4OTMzMDgsMjAuMTQ5OTkxIEwxNi4xNTcwOTE0LDE2LjcxNzc1MTYgQzE2LjA5MTM3LDE2LjY1MjAzMDEgMTYuMDQwMTE3MSwxNi41NzczODc0IDE2LjAwMzQxNDEsMTYuNDk3Nzk5NSBDMTUuMTY3MTY5NCwxNy4xMjcwNDExIDE0LjEyNzEzOTMsMTcuNSAxMywxNy41IEMxMC4yMzg1NzYzLDE3LjUgOCwxNS4yNjE0MjM3IDgsMTIuNSBDOCw5LjczODU3NjI1IDEwLjIzODU3NjMsNy41IDEzLDcuNSBDMTUuNzYxNDIzNyw3LjUgMTgsOS43Mzg1NzYyNSAxOCwxMi41IEMxOCwxMy42Mjc0Njg1IDE3LjYyNjgyMzIsMTQuNjY3Nzc2OCAxNi45OTcyNDgyLDE1LjUwNDE0NjYgWiBNMTMsMTYuNSBDMTUuMjA5MTM5LDE2LjUgMTcsMTQuNzA5MTM5IDE3LDEyLjUgQzE3LDEwLjI5MDg2MSAxNS4yMDkxMzksOC41IDEzLDguNSBDMTAuNzkwODYxLDguNSA5LDEwLjI5MDg2MSA5LDEyLjUgQzksMTQuNzA5MTM5IDEwLjc5MDg2MSwxNi41IDEzLDE2LjUgWiIgaWQ9Imlvcy1zZWFyY2gtaW5wdXQtaWNvbiI+PC9wYXRoPgogICAgICAgICAgICA8L2c+CiAgICAgICAgPC9nPgogICAgPC9nPgo8L3N2Zz4=")', - backgroundPosition: "8px center", - backgroundRepeat: "no-repeat", - backgroundSize: "13px", - display: "inline-block", - textIndent: "0", - }, - ".search-input::-webkit-search-cancel-button": { - webkitAppearance: "textfield", - appearance: "textfield", - display: "none", - }, - ".search-input::-webkit-search-decoration": { - display: "none", - }, - ".search-input:focus": { - outline: "none", - }, - ".search-input::-webkit-input-placeholder": { - color: "#7a797b", - fontSize: "14px", - textIndent: "0", - }, - ".search-input:-ms-input-placeholder": { - color: "#7a797b", - fontSize: "14px", - textIndent: "0", - }, - ".search-input::-ms-input-placeholder": { - color: "#7a797b", - fontSize: "14px", - textIndent: "0", - }, - ".search-input::placeholder": { - color: "#7a797b", - fontSize: "14px", - textIndent: "0", - }, - ".search-input:placeholder-shown": {}, - ".search-input:disabled": { - opacity: "0.3", - pointerEvents: "none", - }, - ".search-input--material": { - fontFamily: '"Roboto", "Noto", sans-serif', - webkitFontSmoothing: "antialiased", - fontWeight: "400", - // borderRadius: "8px", - height: "48px", - backgroundColor: "#fafafa", - backgroundImage: - 'url("data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz4KPHN2ZyB3aWR0aD0iMThweCIgaGVpZ2h0PSIxOHB4IiB2aWV3Qm94PSIwIDAgMTggMTgiIHZlcnNpb249IjEuMSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayI+CiAgICA8IS0tIEdlbmVyYXRvcjogU2tldGNoIDQzLjIgKDM5MDY5KSAtIGh0dHA6Ly93d3cuYm9oZW1pYW5jb2RpbmcuY29tL3NrZXRjaCAtLT4KICAgIDx0aXRsZT5TaGFwZTwvdGl0bGU+CiAgICA8ZGVzYz5DcmVhdGVkIHdpdGggU2tldGNoLjwvZGVzYz4KICAgIDxkZWZzPjwvZGVmcz4KICAgIDxnIGlkPSJQYWdlLTEiIHN0cm9rZT0ibm9uZSIgc3Ryb2tlLXdpZHRoPSIxIiBmaWxsPSJub25lIiBmaWxsLXJ1bGU9ImV2ZW5vZGQiPgogICAgICAgIDxnIGlkPSJhbmRyb2lkLXNlYXJjaC1pbnB1dC1pY29uIiBmaWxsLXJ1bGU9Im5vbnplcm8iIGZpbGw9IiM4OTg5ODkiPgogICAgICAgICAgICA8ZyBpZD0iY29tcG9uZW50cyI+CiAgICAgICAgICAgICAgICA8ZyBpZD0ibWF0ZXJpYWwtc2VhcmNoIj4KICAgICAgICAgICAgICAgICAgICA8ZyBpZD0ic2VhcmNoIj4KICAgICAgICAgICAgICAgICAgICAgICAgPGcgaWQ9Ik1hdGVyaWFsL0ljb25zLWJsYWNrL3NlYXJjaCI+CiAgICAgICAgICAgICAgICAgICAgICAgICAgICA8cGF0aCBkPSJNMTIuNTAyLDYuNDkxIEwxMS43MDgsNi40OTEgTDExLjQzMiw2Ljc2NSBDMTIuNDA3LDcuOTAyIDEzLDkuMzc2IDEzLDEwLjk5MSBDMTMsMTQuNTgxIDEwLjA5LDE3LjQ5MSA2LjUsMTcuNDkxIEMyLjkxLDE3LjQ5MSAwLDE0LjU4MSAwLDEwLjk5MSBDMCw3LjQwMSAyLjkxLDQuNDkxIDYuNSw0LjQ5MSBDOC4xMTUsNC40OTEgOS41ODgsNS4wODMgMTAuNzI1LDYuMDU3IEwxMS4wMDEsNS43ODMgTDExLjAwMSw0Ljk5MSBMMTUuOTk5LDAgTDE3LjQ5LDEuNDkxIEwxMi41MDIsNi40OTEgTDEyLjUwMiw2LjQ5MSBaIE02LjUsNi40OTEgQzQuMDE0LDYuNDkxIDIsOC41MDUgMiwxMC45OTEgQzIsMTMuNDc2IDQuMDE0LDE1LjQ5MSA2LjUsMTUuNDkxIEM4Ljk4NSwxNS40OTEgMTEsMTMuNDc2IDExLDEwLjk5MSBDMTEsOC41MDUgOC45ODUsNi40OTEgNi41LDYuNDkxIEw2LjUsNi40OTEgWiIgaWQ9IlNoYXBlIiB0cmFuc2Zvcm09InRyYW5zbGF0ZSg4Ljc0NTAwMCwgOC43NDU1MDApIHNjYWxlKC0xLCAxKSByb3RhdGUoLTE4MC4wMDAwMDApIHRyYW5zbGF0ZSgtOC43NDUwMDAsIC04Ljc0NTUwMCkgIj48L3BhdGg+CiAgICAgICAgICAgICAgICAgICAgICAgIDwvZz4KICAgICAgICAgICAgICAgICAgICA8L2c+CiAgICAgICAgICAgICAgICA8L2c+CiAgICAgICAgICAgIDwvZz4KICAgICAgICA8L2c+CiAgICA8L2c+Cjwvc3ZnPg==")', - backgroundSize: "18px", - backgroundPosition: "18px center", - fontSize: "14px", - padding: "0 24px 0 64px", - // boxShadow: - // "0 0 2px 0 rgba(0, 0, 0, 0.12), 0 2px 2px 0 rgba(0, 0, 0, 0.24),\r\n 0 1px 0 0 rgba(255, 255, 255, 0.06) inset", - borderRadius: 4, - border: "1px solid rgba(0, 0, 0, 0.12)", - }, - ".text-input": { - boxSizing: "border-box", - backgroundClip: "padding-box", - padding: "0", - margin: "0", - font: "inherit", - color: "#1f1f21", - background: "transparent", - border: "none", - verticalAlign: "top", - outline: "none", - lineHeight: "1", - fontFamily: '-apple-system, "Helvetica Neue", "Helvetica", "Arial", "Lucida Grande", sans-serif', - webkitFontSmoothing: "antialiased", - mozOsxFontSmoothing: "grayscale", - fontWeight: "400", - - backgroundColor: "transparent", - letterSpacing: "0", - boxShadow: "none", - width: "auto", - fontSize: "16px", - height: "31px", - }, - ".text-input::-ms-clear": { - display: "none", - }, - ".text-input:disabled": { - opacity: "0.3", - pointerEvents: "none", - }, - ".text-input::-webkit-input-placeholder": { - color: "#999", - }, - ".text-input:-ms-input-placeholder": { - color: "#999", - }, - ".text-input::-ms-input-placeholder": { - color: "#999", - }, - ".text-input::placeholder": { - color: "#999", - }, - ".text-input:disabled::-webkit-input-placeholder": { - border: "none", - backgroundColor: "transparent", - color: "#999", - }, - ".text-input:disabled:-ms-input-placeholder": { - border: "none", - backgroundColor: "transparent", - color: "#999", - }, - ".text-input:disabled::-ms-input-placeholder": { - border: "none", - backgroundColor: "transparent", - color: "#999", - }, - ".text-input:disabled::placeholder": { - border: "none", - backgroundColor: "transparent", - color: "#999", - }, - ".text-input:invalid": { - border: "none", - backgroundColor: "transparent", - color: "#1f1f21", - }, - ".text-input--underbar": { - boxSizing: "border-box", - backgroundClip: "padding-box", - padding: "0", - margin: "0", - font: "inherit", - color: "#1f1f21", - background: "transparent", - border: "none", - verticalAlign: "top", - outline: "none", - lineHeight: "1", - fontFamily: '-apple-system, "Helvetica Neue", "Helvetica", "Arial", "Lucida Grande", sans-serif', - webkitFontSmoothing: "antialiased", - mozOsxFontSmoothing: "grayscale", - fontWeight: "400", - - backgroundColor: "transparent", - letterSpacing: "0", - boxShadow: "none", - width: "auto", - fontSize: "16px", - height: "31px", - borderBottom: "1px solid #ccc", - borderRadius: "0", - }, - ".text-input--underbar:disabled": { - opacity: "0.3", - pointerEvents: "none", - border: "none", - backgroundColor: "transparent", - borderBottom: "1px solid #ccc", - }, - ".text-input--underbar:disabled::-webkit-input-placeholder": { - border: "none", - backgroundColor: "transparent", - color: "#999", - }, - ".text-input--underbar:disabled:-ms-input-placeholder": { - border: "none", - backgroundColor: "transparent", - color: "#999", - }, - ".text-input--underbar:disabled::-ms-input-placeholder": { - border: "none", - backgroundColor: "transparent", - color: "#999", - }, - ".text-input--underbar:disabled::placeholder": { - border: "none", - backgroundColor: "transparent", - color: "#999", - }, - ".text-input--underbar:invalid": { - border: "none", - backgroundColor: "transparent", - color: "#1f1f21", - - borderBottom: "1px solid #ccc", - }, - ".text-input--material": { - boxSizing: "border-box", - backgroundClip: "padding-box", - padding: "0", - margin: "0", - font: "inherit", - color: "#212121", - background: "transparent", - border: "none", - verticalAlign: "middle", - outline: "none", - lineHeight: "1", - fontFamily: '"Roboto", "Noto", sans-serif', - webkitFontSmoothing: "antialiased", - mozOsxFontSmoothing: "grayscale", - fontWeight: "400", - - backgroundImage: "linear-gradient(to top, transparent 1px, #afafaf 1px)", - backgroundSize: "100% 2px", - backgroundRepeat: "no-repeat", - backgroundPosition: "center bottom", - backgroundColor: "transparent", - fontSize: "16px", - paddingBottom: "2px", - borderRadius: "0", - height: "24px", - webkitTransform: "translate3d(0, 0, 0)", - }, - ".text-input--material__label": { - fontFamily: '"Roboto", "Noto", sans-serif', - webkitFontSmoothing: "antialiased", - fontWeight: "400", - color: "#afafaf", - position: "absolute", - left: "0", - top: "2px", - fontSize: "16px", - - pointerEvents: "none", - }, - ".text-input--material__label--active": { - color: "#3d5afe", - webkitTransform: "translate(0, -75%) scale(0.75)", - transform: "translate(0, -75%) scale(0.75)", - webkitTransformOrigin: "left top", - transformOrigin: "left top", - transition: "transform 0.1s ease-in, color 0.1s ease-in, -webkit-transform 0.1s ease-in", - }, - ".text-input--material:focus": { - backgroundImage: "linear-gradient(#3d5afe, #3d5afe),\r\n linear-gradient(to top, transparent 1px, #afafaf 1px)", - webkitAnimation: "material-text-input-animate 0.3s forwards", - animation: "material-text-input-animate 0.3s forwards", - }, - ".text-input--material::-webkit-input-placeholder": { - color: "#afafaf", - lineHeight: "20px", - }, - ".text-input--material:-ms-input-placeholder": { - color: "#afafaf", - lineHeight: "20px", - }, - ".text-input--material::-ms-input-placeholder": { - color: "#afafaf", - lineHeight: "20px", - }, - ".text-input--material::placeholder": { - color: "#afafaf", - lineHeight: "20px", - }, - "@keyframes material-text-input-animate": { - "0%": { - backgroundSize: "0% 2px, 100% 2px", - }, - "100%": { - backgroundSize: "100% 2px, 100% 2px", - }, - }, - ".textarea": { - boxSizing: "border-box", - backgroundClip: "padding-box", - padding: "5px 5px 5px 5px", - margin: "0", - font: "inherit", - color: "#1f1f21", - background: "transparent", - border: "1px solid #ccc", - lineHeight: "normal", - fontFamily: '-apple-system, "Helvetica Neue", "Helvetica", "Arial", "Lucida Grande", sans-serif', - webkitFontSmoothing: "antialiased", - mozOsxFontSmoothing: "grayscale", - fontWeight: "400", - verticalAlign: "top", - resize: "none", - outline: "none", - - fontSize: "16px", - borderRadius: "4px", - backgroundColor: "rgba(255, 255, 255, 1)", - letterSpacing: "0", - boxShadow: "none", - webkitAppearance: "none", - mozAppearance: "none", - appearance: "none", - width: "auto", - }, - ".textarea:disabled": { - opacity: "0.3", - pointerEvents: "none", - }, - ".textarea::-webkit-input-placeholder": { - color: "#999", - }, - ".textarea:-ms-input-placeholder": { - color: "#999", - }, - ".textarea::-ms-input-placeholder": { - color: "#999", - }, - ".textarea::placeholder": { - color: "#999", - }, - ".textarea--transparent": { - boxSizing: "border-box", - backgroundClip: "padding-box", - padding: "5px 5px 5px 5px", - margin: "0", - font: "inherit", - color: "#1f1f21", - background: "transparent", - border: "none", - lineHeight: "normal", - fontFamily: '-apple-system, "Helvetica Neue", "Helvetica", "Arial", "Lucida Grande", sans-serif', - webkitFontSmoothing: "antialiased", - mozOsxFontSmoothing: "grayscale", - fontWeight: "400", - verticalAlign: "top", - resize: "none", - outline: "none", - - paddingLeft: "0", - paddingRight: "0", - fontSize: "16px", - borderRadius: "4px", - backgroundColor: "transparent", - letterSpacing: "0", - boxShadow: "none", - webkitAppearance: "none", - mozAppearance: "none", - appearance: "none", - width: "auto", - }, - ".textarea--transparent:disabled": { - opacity: "0.3", - pointerEvents: "none", - }, - ".textarea--transparent::-webkit-input-placeholder": { - color: "#999", - }, - ".textarea--transparent:-ms-input-placeholder": { - color: "#999", - }, - ".textarea--transparent::-ms-input-placeholder": { - color: "#999", - }, - ".textarea--transparent::placeholder": { - color: "#999", - }, - ".dialog": { - boxSizing: "border-box", - backgroundClip: "padding-box", - padding: "0", - margin: "auto auto", - font: "inherit", - color: "inherit", - background: "transparent", - border: "none", - lineHeight: "normal", - webkitUserSelect: "none", - mozUserSelect: "none", - msUserSelect: "none", - userSelect: "none", - fontFamily: '-apple-system, "Helvetica Neue", "Helvetica", "Arial", "Lucida Grande", sans-serif', - webkitFontSmoothing: "antialiased", - mozOsxFontSmoothing: "grayscale", - fontWeight: "400", - position: "absolute", - top: "50%", - left: "50%", - webkitTransform: "translate(-50%, -50%)", - transform: "translate(-50%, -50%)", - - overflow: "hidden", - minWidth: "270px", - minHeight: "100px", - textAlign: "left", - }, - ".dialog-container": { - height: "inherit", - minHeight: "inherit", - overflow: "hidden", - borderRadius: "4px", - backgroundColor: "#f4f4f4", - webkitMaskImage: - 'url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAIAAACQd1PeAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAA5JREFUeNpiYGBgAAgwAAAEAAGbA+oJAAAAAElFTkSuQmCC")', - color: "#1f1f21", - }, - ".dialog-mask": { - padding: "0", - margin: "0", - font: "inherit", - color: "inherit", - background: "transparent", - border: "none", - lineHeight: "normal", - webkitUserSelect: "none", - mozUserSelect: "none", - msUserSelect: "none", - userSelect: "none", - position: "absolute", - top: "0", - right: "0", - left: "0", - bottom: "0", - backgroundColor: "rgba(0, 0, 0, 0.2)", - }, - ".dialog--material": { - fontFamily: '"Roboto", "Noto", sans-serif', - webkitFontSmoothing: "antialiased", - fontWeight: "400", - textAlign: "left", - boxShadow: "0 16px 24px 2px rgba(0, 0, 0, 0.14), 0 6px 30px 5px rgba(0, 0, 0, 0.12),\r\n 0 8px 10px -5px rgba(0, 0, 0, 0.4)", - }, - ".dialog-container--material": { - borderRadius: "8px", - backgroundColor: "#ffffff", - color: "#1f1f21", - }, - ".dialog-mask--material": { - backgroundColor: "rgba(0, 0, 0, 0.3)", - }, - ".alert-dialog": { - boxSizing: "border-box", - backgroundClip: "padding-box", - padding: "0", - margin: "auto", - font: "inherit", - color: "#1f1f21", - background: "transparent", - border: "none", - lineHeight: "normal", - webkitUserSelect: "none", - mozUserSelect: "none", - msUserSelect: "none", - userSelect: "none", - fontFamily: '-apple-system, "Helvetica Neue", "Helvetica", "Arial", "Lucida Grande", sans-serif', - webkitFontSmoothing: "antialiased", - mozOsxFontSmoothing: "grayscale", - fontWeight: "400", - position: "absolute", - top: "50%", - left: "50%", - webkitTransform: "translate(-50%, -50%)", - transform: "translate(-50%, -50%)", - width: "270px", - - backgroundColor: "#f4f4f4", - borderRadius: "8px", - overflow: "visible", - maxWidth: "95%", - }, - ".alert-dialog-container": { - height: "inherit", - paddingTop: "16px", - overflow: "hidden", - }, - ".alert-dialog-title": { - fontFamily: '-apple-system, "Helvetica Neue", "Helvetica", "Arial", "Lucida Grande", sans-serif', - webkitFontSmoothing: "antialiased", - mozOsxFontSmoothing: "grayscale", - fontWeight: "500", - fontSize: "17px", - - padding: "0 8px", - textAlign: "center", - color: "#1f1f21", - }, - ".alert-dialog-content": { - boxSizing: "border-box", - backgroundClip: "padding-box", - padding: "4px 12px 8px", - fontSize: "14px", - minHeight: "36px", - textAlign: "center", - color: "#1f1f21", - }, - ".alert-dialog-footer": { - width: "100%", - }, - ".alert-dialog-button": { - boxSizing: "border-box", - backgroundClip: "padding-box", - padding: "0 8px", - margin: "0", - font: "inherit", - color: "#4a148c", - background: "transparent", - border: "none", - lineHeight: "44px", - fontFamily: '-apple-system, "Helvetica Neue", "Helvetica", "Arial", "Lucida Grande", sans-serif', - webkitFontSmoothing: "antialiased", - mozOsxFontSmoothing: "grayscale", - fontWeight: "400", - webkitUserSelect: "none", - mozUserSelect: "none", - msUserSelect: "none", - userSelect: "none", - textOverflow: "ellipsis", - whiteSpace: "nowrap", - overflow: "hidden", - textDecoration: "none", - letterSpacing: "0", - verticalAlign: "middle", - - borderTop: "1px solid #ddd", - fontSize: "16px", - display: "block", - width: "100%", - backgroundColor: "transparent", - textAlign: "center", - height: "44px", - outline: "none", - }, - ".alert-dialog-button:active": { - backgroundColor: "rgba(0, 0, 0, 0.05)", - }, - ".alert-dialog-button--primal": { - fontWeight: "500", - }, - ".alert-dialog-footer--rowfooter": { - whiteSpace: "nowrap", - display: "flex", - - webkitFlexWrap: "wrap", - flexWrap: "wrap", - }, - ".alert-dialog-button--rowfooter": { - webkitBoxFlex: "1", - webkitFlex: "1", - flex: "1", - display: "block", - width: "100%", - borderLeft: "1px solid #ddd", - }, - ".alert-dialog-button--rowfooter:first-of-type": { - borderLeft: "none", - }, - ".alert-dialog-mask": { - padding: "0", - margin: "0", - font: "inherit", - color: "inherit", - background: "transparent", - border: "none", - lineHeight: "normal", - webkitUserSelect: "none", - mozUserSelect: "none", - msUserSelect: "none", - userSelect: "none", - position: "absolute", - top: "0", - right: "0", - left: "0", - bottom: "0", - backgroundColor: "rgba(0, 0, 0, 0.2)", - }, - ".alert-dialog--material": { - borderRadius: "25px", - backgroundColor: "#ffffff", - }, - ".alert-dialog-container--material": { - borderRadius: "25px", - padding: "22px 0 0 0", - boxShadow: "0 16px 24px 2px rgba(0, 0, 0, 0.14), 0 6px 30px 5px rgba(0, 0, 0, 0.12),\r\n 0 8px 10px -5px rgba(0, 0, 0, 0.4)", - }, - ".alert-dialog-title--material": { - fontFamily: '"Roboto", "Noto", sans-serif', - webkitFontSmoothing: "antialiased", - fontWeight: "500", - textAlign: "left", - fontSize: "20px", - - padding: "0 24px", - color: "#31313a", - }, - ".alert-dialog-content--material": { - fontFamily: '"Roboto", "Noto", sans-serif', - webkitFontSmoothing: "antialiased", - fontWeight: "400", - textAlign: "left", - fontSize: "16px", - - lineHeight: "20px", - padding: "0 24px", - margin: "24px 0 10px 0", - minHeight: "0", - color: "rgba(49, 49, 58, 0.85)", - }, - ".alert-dialog-footer--material": { - display: "block", - padding: "0", - height: "52px", - boxSizing: "border-box", - margin: "0", - lineHeight: "1", - }, - ".alert-dialog-button--material": { - fontFamily: '"Roboto", "Noto", sans-serif', - webkitFontSmoothing: "antialiased", - fontWeight: "500", - textTransform: "uppercase", - display: "inline-block", - width: "auto", - float: "right", - background: "none", - border: "none", - borderRadius: "2px", - fontSize: "14px", - - outline: "none", - height: "36px", - lineHeight: "36px", - padding: "0 8px", - margin: "8px 8px 8px 0", - boxSizing: "border-box", - minWidth: "50px", - color: "#4a148c", - }, - ".alert-dialog-button--material:active": { - backgroundColor: "initial", - }, - ".alert-dialog-button--rowfooter--material, .alert-dialog-button--rowfooter--material:first-of-type": { - border: "0", - }, - ".alert-dialog-button--primal--material": { - fontWeight: "500", - }, - ".alert-dialog-mask--material": { - backgroundColor: "rgba(0, 0, 0, 0.3)", - }, - ".popover": { - position: "absolute", - zIndex: "20001", - }, - ".popover--bottom": { - bottom: "0", - }, - ".popover--top": { - top: "0", - }, - ".popover--left": { - left: "0", - }, - ".popover--right": { - right: "0", - }, - ".popover-mask": { - left: "0", - right: "0", - top: "0", - bottom: "0", - backgroundColor: "rgba(0, 0, 0, 0.2)", - - position: "absolute", - zIndex: "19999", - }, - ".popover__content": { - boxSizing: "border-box", - backgroundClip: "padding-box", - padding: "0", - margin: "0", - font: "inherit", - color: "#1f1f21", - background: "transparent", - border: "none", - lineHeight: "normal", - webkitUserSelect: "none", - mozUserSelect: "none", - msUserSelect: "none", - userSelect: "none", - fontFamily: '-apple-system, "Helvetica Neue", "Helvetica", "Arial", "Lucida Grande", sans-serif', - webkitFontSmoothing: "antialiased", - mozOsxFontSmoothing: "grayscale", - fontWeight: "400", - display: "block", - width: "220px", - overflow: "auto", - minHeight: "100px", - maxHeight: "100%", - backgroundColor: "white", - borderRadius: "8px", - - pointerEvents: "auto", - }, - ".popover--top__content": {}, - ".popover--bottom__content": {}, - ".popover--left__content": {}, - ".popover--right__content": {}, - ".popover__arrow": { - position: "absolute", - width: "18px", - height: "18px", - webkitTransformOrigin: "50% 50% 0", - transformOrigin: "50% 50% 0", - backgroundColor: "transparent", - backgroundImage: "linear-gradient(45deg, white, white 50%, transparent 50%)", - borderRadius: "0 0 0 4px", - margin: "0", - zIndex: "20001", - }, - ".popover--bottom__arrow": { - webkitTransform: "translateY(6px) translateX(-9px) rotate(-45deg)", - transform: "translateY(6px) translateX(-9px) rotate(-45deg)", - bottom: "0", - marginRight: "-18px", - }, - ".popover--top__arrow": { - webkitTransform: "translateY(-6px) translateX(-9px) rotate(135deg)", - transform: "translateY(-6px) translateX(-9px) rotate(135deg)", - top: "0", - marginRight: "-18px", - }, - ".popover--left__arrow": { - webkitTransform: "translateX(-6px) translateY(-9px) rotate(45deg)", - transform: "translateX(-6px) translateY(-9px) rotate(45deg)", - left: "0", - marginBottom: "-18px", - }, - ".popover--right__arrow": { - webkitTransform: "translateX(6px) translateY(-9px) rotate(225deg)", - transform: "translateX(6px) translateY(-9px) rotate(225deg)", - right: "0", - marginBottom: "-18px", - }, - ".popover--material": {}, - ".popover-mask--material": { - backgroundColor: "transparent", - }, - ".popover--material__content": { - backgroundColor: "#fafafa", - borderRadius: "2px", - color: "#1f1f21", - boxShadow: "0 2px 2px 0 rgba(0, 0, 0, 0.14), 0 1px 5px 0 rgba(0, 0, 0, 0.12),\r\n 0 3px 1px -2px rgba(0, 0, 0, 0.2)", - }, - ".popover--material__arrow": { - display: "none", - }, - ".progress-bar": { - position: "relative", - height: "2px", - display: "block", - width: "100%", - backgroundColor: "transparent", - backgroundClip: "padding-box", - margin: "0", - overflow: "hidden", - borderRadius: "4px", - }, - ".progress-bar__primary, .progress-bar__secondary": { - position: "absolute", - backgroundColor: theme.palette.primary.main, - top: "0", - bottom: "0", - transition: "width 0.3s linear", - zIndex: "100", - borderRadius: "4px", - }, - ".progress-bar__secondary": { - backgroundColor: "#65adff", - zIndex: "0", - }, - ".progress-bar--indeterminate:before": { - content: '""', - position: "absolute", - backgroundColor: theme.palette.primary.main, - top: "0", - left: "0", - bottom: "0", - willChange: "left, right", - webkitAnimation: "progress-bar__indeterminate 2.1s cubic-bezier(0.65, 0.815, 0.735, 0.395)\r\n infinite", - animation: "progress-bar__indeterminate 2.1s cubic-bezier(0.65, 0.815, 0.735, 0.395) infinite", - borderRadius: "4px", - }, - ".progress-bar--indeterminate:after": { - content: '""', - position: "absolute", - backgroundColor: theme.palette.primary.main, - top: "0", - left: "0", - bottom: "0", - willChange: "left, right", - webkitAnimation: "progress-bar__indeterminate-short 2.1s cubic-bezier(0.165, 0.84, 0.44, 1)\r\n infinite", - animation: "progress-bar__indeterminate-short 2.1s cubic-bezier(0.165, 0.84, 0.44, 1) infinite", - webkitAnimationDelay: "1.15s", - animationDelay: "1.15s", - borderRadius: "4px", - }, - "@keyframes progress-bar__indeterminate": { - "0%": { - left: "-35%", - right: "100%", - }, - "60%": { - left: "100%", - right: "-90%", - }, - "100%": { - left: "100%", - right: "-90%", - }, - }, - "@keyframes progress-bar__indeterminate-short": { - "0%": { - left: "-200%", - right: "100%", - }, - "60%": { - left: "107%", - right: "-8%", - }, - "100%": { - left: "107%", - right: "-8%", - }, - }, - ".progress-bar--material": { - height: "4px", - backgroundColor: "transparent", - borderRadius: "0", - }, - ".progress-bar--material__primary, .progress-bar--material__secondary": { - backgroundColor: theme.palette.primary.main, - borderRadius: "0", - }, - ".progress-bar--material__secondary": { - backgroundColor: "#12005e", - zIndex: "0", - }, - ".progress-bar--material.progress-bar--indeterminate:before": { - backgroundColor: theme.palette.primary.main, - borderRadius: "0", - }, - ".progress-bar--material.progress-bar--indeterminate:after": { - backgroundColor: theme.palette.primary.main, - borderRadius: "0", - }, - ".progress-circular": { - height: "40px", - position: "relative", - width: "40px", - webkitTransform: "rotate(270deg)", - transform: "rotate(270deg)", - webkitAnimation: "none", - animation: "none", - }, - ".progress-circular__background, .progress-circular__primary, .progress-circular__secondary": { - cx: "50%", - cy: "50%", - r: "40%", - webkitAnimation: "none", - animation: "none", - fill: "none", - strokeWidth: "5%", - strokeMiterlimit: "10", - }, - ".progress-circular__background": { - stroke: "transparent", - }, - ".progress-circular__primary": { - strokeDasharray: "1, 200", - strokeDashoffset: "0", - stroke: "#4a148c", - transition: "all 1s cubic-bezier(0.4, 0, 0.2, 1)", - }, - ".progress-circular__secondary": { - stroke: "#65adff", - }, - ".progress-circular--indeterminate": { - webkitAnimation: "progress__rotate 2s linear infinite", - animation: "progress__rotate 2s linear infinite", - webkitTransform: "none", - transform: "none", - }, - ".progress-circular--indeterminate__primary": { - webkitAnimation: "progress__dash 1.5s ease-in-out infinite", - animation: "progress__dash 1.5s ease-in-out infinite", - }, - ".progress-circular--indeterminate__secondary": { - display: "none", - }, - "@keyframes progress__rotate": { - "100%": { - webkitTransform: "rotate(360deg)", - transform: "rotate(360deg)", - }, - }, - "@keyframes progress__dash": { - "0%": { - strokeDasharray: "10%, 241.32%", - strokeDashoffset: "0", - }, - "50%": { - strokeDasharray: "201%, 50.322%", - strokeDashoffset: "-100%", - }, - "100%": { - strokeDasharray: "10%, 241.32%", - strokeDashoffset: "-251.32%", - }, - }, - ".progress-circular--material__background, .progress-circular--material__primary, .progress-circular--material__secondary": { - strokeWidth: "9%", - }, - ".progress-circular--material__background": { - stroke: "transparent", - }, - ".progress-circular--material__primary": { - stroke: theme.palette.primary.main, - }, - ".progress-circular--material__secondary": { - stroke: "#12005e", - }, - - "ons-fab.fab, ons-speed-dial-item.fab, button.fab": { - position: "relative", - display: "inline-block", - boxSizing: "border-box", - backgroundClip: "padding-box", - padding: "0", - margin: "0", - font: "inherit", - color: "#ffffff", - background: "transparent", - border: "0 solid currentColor", - lineHeight: "56px", - fontFamily: '-apple-system, "Helvetica Neue", "Helvetica", "Arial", "Lucida Grande", sans-serif', - webkitFontSmoothing: "antialiased", - mozOsxFontSmoothing: "grayscale", - fontWeight: "400", - webkitUserSelect: "none", - mozUserSelect: "none", - msUserSelect: "none", - userSelect: "none", - width: "56px", - height: "56px", - textDecoration: "none", - fontSize: "25px", - - letterSpacing: "0", - verticalAlign: "middle", - textAlign: "center", - backgroundColor: theme.palette.primary.main, - borderRadius: "30%", - overflow: "hidden", - boxShadow: "0 3px 6px rgba(0, 0, 0, 0.12)", - transition: "all 0.1s linear", - }, - "ons-fab.fab:active, ons-speed-dial-item.fab:active, button.fab:active": { - boxShadow: "0 0 6 rgba(0, 0, 0, 0.12)", - backgroundColor: "color-mod(#4a148c a(70%))", - transition: "all 0.2s ease", - }, - "ons-fab.fab:focus, ons-speed-dial-item.fab:focus, button.fab:focus": { - outline: "0", - }, - "ons-fab.fab:disabled, ons-fab.fab[disabled], ons-speed-dial-item.fab:disabled, ons-speed-dial-item.fab[disabled], button.fab:disabled, button.fab[disabled]": - { - backgroundColor: "color-mod(black alpha(50%))", - boxShadow: "none", - opacity: "0.3", - pointerEvents: "none", - }, - "ons-fab.fab__icon, ons-speed-dial-item.fab__icon, button.fab__icon": { - position: "relative", - overflow: "hidden", - height: "100%", - width: "100%", - display: "block", - borderRadius: "100%", - padding: "0", - zIndex: "100", - lineHeight: "56px", - }, - "ons-fab.fab--material, ons-speed-dial-item.fab--material, button.fab--material": { - position: "relative", - display: "inline-block", - boxSizing: "border-box", - backgroundClip: "padding-box", - padding: "0", - margin: "0", - font: "inherit", - color: "rgba(255, 255, 255, 1)", - background: "transparent", - border: "0 solid currentColor", - lineHeight: "56px", - fontFamily: '"Roboto", "Noto", sans-serif', - webkitFontSmoothing: "antialiased", - mozOsxFontSmoothing: "grayscale", - fontWeight: "400", - webkitUserSelect: "none", - mozUserSelect: "none", - msUserSelect: "none", - userSelect: "none", - width: "56px", - height: "56px", - textDecoration: "none", - fontSize: "25px", - - letterSpacing: "0", - verticalAlign: "middle", - textAlign: "center", - backgroundColor: theme.palette.primary.main, - // borderRadius: "50%", - overflow: "hidden", - boxShadow: "0 4px 5px 0 rgba(0, 0, 0, 0.14), 0 1px 10px 0 rgba(0, 0, 0, 0.12),\r\n 0 2px 4px -1px rgba(0, 0, 0, 0.4)", - transition: "all 0.2s ease-in-out", - }, - "ons-fab.fab--material:active, ons-speed-dial-item.fab--material:active, button.fab--material:active": { - boxShadow: "0 8px 10px 1px rgba(0, 0, 0, 0.14), 0 3px 14px 2px rgba(0, 0, 0, 0.12),\r\n 0 5px 5px -3px rgba(0, 0, 0, 0.4)", - backgroundColor: "rgba(255, 255, 255, 0.75)", - - transition: "all 0.2s ease", - }, - "ons-fab.fab--material:focus, ons-speed-dial-item.fab--material:focus, button.fab--material:focus": { - outline: "0", - }, - "ons-fab.fab--material__icon, ons-speed-dial-item.fab--material__icon, button.fab--material__icon": { - position: "relative", - overflow: "hidden", - height: "100%", - width: "100%", - display: "block", - borderRadius: "100%", - padding: "0", - zIndex: "100", - lineHeight: "56px", - }, - "ons-fab.fab--material:disabled, ons-fab.fab--material[disabled], ons-speed-dial-item.fab--material:disabled, ons-speed-dial-item.fab--material[disabled], button.fab--material:disabled, button.fab--material[disabled]": - { - backgroundColor: "color-mod(black alpha(50%))", - boxShadow: "none", - opacity: "0.3", - pointerEvents: "none", - }, - "ons-fab.fab--mini, ons-speed-dial-item.fab--mini, button.fab--mini": { - width: "40px", - height: "40px", - lineHeight: "40px", - }, - "ons-fab.fab--mini__icon, ons-speed-dial-item.fab--mini__icon, button.fab--mini__icon": { - lineHeight: "40px", - }, - "ons-fab.speed-dial__item, ons-speed-dial-item.speed-dial__item, button.speed-dial__item": { - position: "absolute", - webkitTransform: "scale(0)", - transform: "scale(0)", - }, - "ons-fab.fab--top__right, button.fab--top__right, .speed-dial.fab--top__right": { - top: "20px", - bottom: "auto", - right: "20px", - left: "auto", - position: "absolute", - }, - "ons-fab.fab--bottom__right, button.fab--bottom__right, .speed-dial.fab--bottom__right": { - top: "auto", - // bottom: "20px", - bottom: `calc(20px + ${view.getWindowBottomInsets()}px)`, - right: "20px", - left: "auto", - position: "absolute", - }, - "ons-fab.fab--top__left, button.fab--top__left, .speed-dial.fab--top__left": { - top: "20px", - bottom: "auto", - right: "auto", - left: "20px", - position: "absolute", - }, - "ons-fab.fab--bottom__left, button.fab--bottom__left, .speed-dial.fab--bottom__left": { - top: "auto", - bottom: "20px", - right: "auto", - left: "20px", - position: "absolute", - }, - "ons-fab.fab--top__center, button.fab--top__center, .speed-dial.fab--top__center": { - top: "20px", - bottom: "auto", - marginLeft: "-28px", - left: "50%", - right: "auto", - position: "absolute", - }, - "ons-fab.fab--bottom__center, button.fab--bottom__center, .speed-dial.fab--bottom__center": { - top: "auto", - bottom: "20px", - marginLeft: "-28px", - left: "50%", - right: "auto", - position: "absolute", - }, - ".modal": { - boxSizing: "border-box", - backgroundClip: "padding-box", - whiteSpace: "nowrap", - overflow: "hidden", - wordSpacing: "0", - padding: "0", - margin: "0", - font: "inherit", - color: "inherit", - background: "transparent", - border: "none", - lineHeight: "normal", - - fontFamily: '-apple-system, "Helvetica Neue", "Helvetica", "Arial", "Lucida Grande", sans-serif', - webkitFontSmoothing: "antialiased", - mozOsxFontSmoothing: "grayscale", - fontWeight: "400", - backgroundColor: "rgba(0, 0, 0, 0.7)", - position: "absolute", - left: "0", - right: "0", - top: "0", - bottom: "0", - width: "100%", - height: "100%", - display: "table", - zIndex: "2147483647", - }, - ".modal__content": { - boxSizing: "border-box", - backgroundClip: "padding-box", - whiteSpace: "normal", - overflow: "hidden", - wordSpacing: "0", - padding: "0", - margin: "0", - font: "inherit", - color: "#fff", - background: "transparent", - border: "none", - lineHeight: "normal", - - fontFamily: '-apple-system, "Helvetica Neue", "Helvetica", "Arial", "Lucida Grande", sans-serif', - webkitFontSmoothing: "antialiased", - mozOsxFontSmoothing: "grayscale", - fontWeight: "400", - display: "table-cell", - verticalAlign: "middle", - textAlign: "center", - }, - ".select-input": { - boxSizing: "border-box", - backgroundClip: "padding-box", - padding: "0 20px 0 0", - margin: "0", - font: "inherit", - color: "#1f1f21", - background: "transparent", - border: "none", - verticalAlign: "top", - outline: "none", - lineHeight: "32px", - fontFamily: '-apple-system, "Helvetica Neue", "Helvetica", "Arial", "Lucida Grande", sans-serif', - webkitFontSmoothing: "antialiased", - mozOsxFontSmoothing: "grayscale", - fontWeight: "400", - - backgroundColor: "transparent", - position: "relative", - fontSize: "17px", - height: "32px", - borderColor: "#ccc", - webkitAppearance: "none", - mozAppearance: "none", - appearance: "none", - display: "inline-block", - borderRadius: "0", - backgroundImage: - 'url("data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz4KPHN2ZyB3aWR0aD0iMTBweCIgaGVpZ2h0PSI1cHgiIHZpZXdCb3g9IjAgMCAxMCA1IiB2ZXJzaW9uPSIxLjEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiPgogICAgPCEtLSBHZW5lcmF0b3I6IFNrZXRjaCA0My4yICgzOTA2OSkgLSBodHRwOi8vd3d3LmJvaGVtaWFuY29kaW5nLmNvbS9za2V0Y2ggLS0+CiAgICA8dGl0bGU+c2VsZWN0LWFsbG93PC90aXRsZT4KICAgIDxkZXNjPkNyZWF0ZWQgd2l0aCBTa2V0Y2guPC9kZXNjPgogICAgPGRlZnM+PC9kZWZzPgogICAgPGcgaWQ9InNlbGVjdCIgc3Ryb2tlPSJub25lIiBzdHJva2Utd2lkdGg9IjEiIGZpbGw9Im5vbmUiIGZpbGwtcnVsZT0iZXZlbm9kZCI+CiAgICAgICAgPGcgaWQ9Imlvcy1zZWxlY3QiIHRyYW5zZm9ybT0idHJhbnNsYXRlKC0xOTguMDAwMDAwLCAtMTE0LjAwMDAwMCkiIGZpbGw9IiM3NTc1NzUiPgogICAgICAgICAgICA8ZyBpZD0ibWVudS1iYXItKy1vcGVuLW1lbnUiIHRyYW5zZm9ybT0idHJhbnNsYXRlKDEyMy4wMDAwMDAsIDEwMC4wMDAwMDApIj4KICAgICAgICAgICAgICAgIDxnIGlkPSJtZW51LWJhciI+CiAgICAgICAgICAgICAgICAgICAgPHBvbHlnb24gaWQ9InNlbGVjdC1hbGxvdyIgcG9pbnRzPSI3NSAxNCA4MCAxOSA4NSAxNCI+PC9wb2x5Z29uPgogICAgICAgICAgICAgICAgPC9nPgogICAgICAgICAgICA8L2c+CiAgICAgICAgPC9nPgogICAgPC9nPgo8L3N2Zz4=")', - backgroundRepeat: "no-repeat", - backgroundPosition: "right center", - borderBottom: "none", - }, - ".select-input::-ms-clear": { - display: "none", - }, - ".select-input::-webkit-input-placeholder": { - color: "#999", - }, - ".select-input:-ms-input-placeholder": { - color: "#999", - }, - ".select-input::-ms-input-placeholder": { - color: "#999", - }, - ".select-input::placeholder": { - color: "#999", - }, - ".select-input:disabled": { - opacity: "0.3", - pointerEvents: "none", - border: "none", - backgroundColor: "transparent", - }, - ".select-input:disabled::-webkit-input-placeholder": { - border: "none", - backgroundColor: "transparent", - color: "#999", - }, - ".select-input:disabled:-ms-input-placeholder": { - border: "none", - backgroundColor: "transparent", - color: "#999", - }, - ".select-input:disabled::-ms-input-placeholder": { - border: "none", - backgroundColor: "transparent", - color: "#999", - }, - ".select-input:disabled::placeholder": { - border: "none", - backgroundColor: "transparent", - color: "#999", - }, - ".select-input:invalid": { - border: "none", - backgroundColor: "transparent", - color: "#1f1f21", - }, - ".select-input[multiple]": { - height: "64px", - }, - ".select-input--material": { - fontFamily: '"Roboto", "Noto", sans-serif', - webkitFontSmoothing: "antialiased", - fontWeight: "400", - color: "#1f1f21", - fontSize: "15px", - backgroundImage: - 'url("data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz4KPHN2ZyB3aWR0aD0iMTBweCIgaGVpZ2h0PSI1cHgiIHZpZXdCb3g9IjAgMCAxMCA1IiB2ZXJzaW9uPSIxLjEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiPgogICAgPCEtLSBHZW5lcmF0b3I6IFNrZXRjaCA0My4yICgzOTA2OSkgLSBodHRwOi8vd3d3LmJvaGVtaWFuY29kaW5nLmNvbS9za2V0Y2ggLS0+CiAgICA8dGl0bGU+c2VsZWN0LWFsbG93PC90aXRsZT4KICAgIDxkZXNjPkNyZWF0ZWQgd2l0aCBTa2V0Y2guPC9kZXNjPgogICAgPGRlZnM+PC9kZWZzPgogICAgPGcgaWQ9InNlbGVjdCIgc3Ryb2tlPSJub25lIiBzdHJva2Utd2lkdGg9IjEiIGZpbGw9Im5vbmUiIGZpbGwtcnVsZT0iZXZlbm9kZCI+CiAgICAgICAgPGcgaWQ9Imlvcy1zZWxlY3QiIHRyYW5zZm9ybT0idHJhbnNsYXRlKC0xOTguMDAwMDAwLCAtMTE0LjAwMDAwMCkiIGZpbGw9IiM3NTc1NzUiPgogICAgICAgICAgICA8ZyBpZD0ibWVudS1iYXItKy1vcGVuLW1lbnUiIHRyYW5zZm9ybT0idHJhbnNsYXRlKDEyMy4wMDAwMDAsIDEwMC4wMDAwMDApIj4KICAgICAgICAgICAgICAgIDxnIGlkPSJtZW51LWJhciI+CiAgICAgICAgICAgICAgICAgICAgPHBvbHlnb24gaWQ9InNlbGVjdC1hbGxvdyIgcG9pbnRzPSI3NSAxNCA4MCAxOSA4NSAxNCI+PC9wb2x5Z29uPgogICAgICAgICAgICAgICAgPC9nPgogICAgICAgICAgICA8L2c+CiAgICAgICAgPC9nPgogICAgPC9nPgo8L3N2Zz4="),\r\n linear-gradient(to top, color-mod(black a(12%)) 50%, color-mod(black a(12%)) 50%)', - backgroundSize: "auto, 100% 1px", - backgroundRepeat: "no-repeat", - backgroundPosition: "right center, left bottom", - border: "none", - - webkitTransform: "translate3d(0, 0, 0)", - transform: "translate3d(0, 0, 0)", - }, - ".select-input--material__label": { - fontFamily: '"Roboto", "Noto", sans-serif', - webkitFontSmoothing: "antialiased", - fontWeight: "400", - color: "rgba(0, 0, 0, 0.81)", - - position: "absolute", - left: "0", - top: "2px", - fontSize: "16px", - pointerEvents: "none", - }, - ".select-input--material__label--active": { - color: "rgba(0, 0, 0, 0.15)", - - webkitTransform: "translate(0, -75%) scale(0.75)", - transform: "translate(0, -75%) scale(0.75)", - webkitTransformOrigin: "left top", - transformOrigin: "left top", - transition: "transform 0.1s ease-in, color 0.1s ease-in, -webkit-transform 0.1s ease-in", - }, - ".select-input--material::-webkit-input-placeholder": { - color: "rgba(0, 0, 0, 0.81)", - - lineHeight: "20px", - }, - ".select-input--material:-ms-input-placeholder": { - color: "rgba(0, 0, 0, 0.81)", - - lineHeight: "20px", - }, - ".select-input--material::-ms-input-placeholder": { - color: "rgba(0, 0, 0, 0.81)", - - lineHeight: "20px", - }, - ".select-input--material::placeholder": { - color: "rgba(0, 0, 0, 0.81)", - - lineHeight: "20px", - }, - "@keyframes material-select-input-animate": { - "0%": { - backgroundSize: "0% 2px, 100% 2px", - }, - "100%": { - backgroundSize: "100% 2px, 100% 2px", - }, - }, - ".select-input--underbar": { - border: "none", - borderBottom: "1px solid #ccc", - }, - ".select-input--underbar:disabled": { - opacity: "0.3", - pointerEvents: "none", - border: "none", - backgroundColor: "transparent", - - borderBottom: "1px solid #ccc", - }, - ".select-input--underbar:disabled::-webkit-input-placeholder": { - border: "none", - backgroundColor: "transparent", - color: "#999", - }, - ".select-input--underbar:disabled:-ms-input-placeholder": { - border: "none", - backgroundColor: "transparent", - color: "#999", - }, - ".select-input--underbar:disabled::-ms-input-placeholder": { - border: "none", - backgroundColor: "transparent", - color: "#999", - }, - ".select-input--underbar:disabled::placeholder": { - border: "none", - backgroundColor: "transparent", - color: "#999", - }, - ".select-input--underbar:invalid": { - border: "none", - backgroundColor: "transparent", - color: "#1f1f21", - - borderBottom: "1px solid #ccc", - }, - ".action-sheet": { - fontFamily: '-apple-system, "Helvetica Neue", "Helvetica", "Arial", "Lucida Grande", sans-serif', - webkitFontSmoothing: "antialiased", - mozOsxFontSmoothing: "grayscale", - fontWeight: "400", - position: "absolute", - left: "10px", - right: "10px", - bottom: "10px", - zIndex: "2", - }, - ".action-sheet-button": { - boxSizing: "border-box", - height: "56px", - fontSize: "20px", - textAlign: "center", - color: "#4a148c", - backgroundColor: "rgba(255, 255, 255, 0.9)", - - borderRadius: "0", - lineHeight: "56px", - border: "none", - webkitAppearance: "none", - mozAppearance: "none", - appearance: "none", - display: "block", - width: "100%", - textOverflow: "ellipsis", - whiteSpace: "nowrap", - overflow: "hidden", - backgroundSize: "100% 1px", - backgroundRepeat: "no-repeat", - backgroundPosition: "bottom", - backgroundImage: "linear-gradient(0deg, rgba(0, 0, 0, 0.1), rgba(0, 0, 0, 0.1) 100%)", - }, - ".action-sheet-button:first-of-type": { - borderTopLeftRadius: "12px", - borderTopRightRadius: "12px", - }, - ".action-sheet-button:active": { - backgroundColor: "#e9e9e9", - backgroundImage: "none", - }, - ".action-sheet-button:focus": { - outline: "none", - }, - ".action-sheet-button:nth-last-of-type(2)": { - borderBottomRightRadius: "12px", - borderBottomLeftRadius: "12px", - backgroundImage: "none", - }, - ".action-sheet-button:last-of-type": { - borderRadius: "12px", - margin: "8px 0 0 0", - backgroundColor: "#fff", - backgroundImage: "none", - fontWeight: "600", - }, - ".action-sheet-button:last-of-type:active": { - backgroundColor: "#e9e9e9", - }, - ".action-sheet-button--destructive": { - color: "#fe3824", - }, - ".action-sheet-title": { - boxSizing: "border-box", - height: "56px", - fontSize: "13px", - color: "#8f8e94", - textAlign: "center", - backgroundColor: "rgba(255, 255, 255, 0.9)", - - lineHeight: "56px", - textOverflow: "ellipsis", - whiteSpace: "nowrap", - overflow: "hidden", - backgroundSize: "100% 1px", - backgroundRepeat: "no-repeat", - backgroundPosition: "bottom", - backgroundImage: "linear-gradient(0deg, rgba(0, 0, 0, 0.1), rgba(0, 0, 0, 0.1) 100%)", - }, - ".action-sheet-title:first-of-type": { - borderTopLeftRadius: "12px", - borderTopRightRadius: "12px", - }, - ".action-sheet-icon": { - display: "none", - }, - ".action-sheet-mask": { - position: "absolute", - left: "0", - top: "0", - right: "0", - bottom: "0", - backgroundColor: "rgba(0, 0, 0, 0.1)", - - zIndex: "1", - }, - ".action-sheet--material": { - paddingBottom: view.getWindowBottomInsets(), - backgroundColor: theme.palette.background.default, - borderTop: `1px solid ${theme.palette.divider}`, - borderRadius: `${theme.shape.borderRadius}px ${theme.shape.borderRadius}px 0px 0px`, - left: "0", - right: "0", - bottom: "0", - boxShadow: "0 16px 24px 2px rgba(0, 0, 0, 0.14), 0 6px 30px 5px rgba(0, 0, 0, 0.12),\r\n 0 8px 10px -5px rgba(0, 0, 0, 0.4)", - }, - ".action-sheet-title--material": { - fontFamily: '"Roboto", "Noto", sans-serif', - webkitFontSmoothing: "antialiased", - fontWeight: "400", - backgroundImage: "none", - textAlign: "left", - height: "56px", - lineHeight: "56px", - fontSize: "16px", - padding: "0 0 0 16px", - borderRadius: `${theme.shape.borderRadius}px ${theme.shape.borderRadius}px 0px 0px`, - color: theme.palette.text.primary, - backgroundColor: theme.palette.background.default, - }, - ".action-sheet-title--material:first-of-type": { - borderRadius: `${theme.shape.borderRadius}px ${theme.shape.borderRadius}px 0px 0px`, - }, - ".action-sheet-button--material": { - fontFamily: '"Roboto", "Noto", sans-serif', - webkitFontSmoothing: "antialiased", - fontWeight: "400", - borderRadius: "0", - backgroundImage: "none", - height: "52px", - lineHeight: "52px", - textAlign: "left", - fontSize: "16px", - padding: "0 0 0 16px", - color: theme.palette.text.primary, - backgroundColor: theme.palette.background.default, - }, - ".action-sheet-button--material:first-of-type": { - borderRadius: `${theme.shape.borderRadius}px ${theme.shape.borderRadius}px 0px 0px`, - }, - ".action-sheet-button--material:nth-last-of-type(2)": { - borderRadius: 0, - }, - ".action-sheet-button--material:last-of-type": { - margin: "0", - borderRadius: 0, - backgroundColor: theme.palette.background.default, - }, - ".action-sheet-icon--material": { - display: "inline-block", - float: "left", - height: "52px", - lineHeight: "52px", - marginRight: "32px", - fontSize: "26px", - width: "0.8em", - textAlign: "center", - }, - ".action-sheet-mask--material": { - backgroundColor: "rgba(0, 0, 0, 0.5)", - }, - ".card": { - fontFamily: '-apple-system, "Helvetica Neue", "Helvetica", "Arial", "Lucida Grande", sans-serif', - webkitFontSmoothing: "antialiased", - mozOsxFontSmoothing: "grayscale", - fontWeight: "400", - boxShadow: "0 1px 2px rgba(0, 0, 0, 0.12)", - borderRadius: "8px", - backgroundColor: "white", - boxSizing: "border-box", - display: "block", - margin: "8px", - padding: "16px", - textAlign: "left", - wordWrap: "break-word", - }, - ".card__content": { - margin: "0", - fontSize: "14px", - lineHeight: "1.4", - color: "#030303", - }, - ".card__title": { - fontFamily: '-apple-system, "Helvetica Neue", "Helvetica", "Arial", "Lucida Grande", sans-serif', - webkitFontSmoothing: "antialiased", - mozOsxFontSmoothing: "grayscale", - fontWeight: "400", - - fontSize: "20px", - margin: "4px 0 8px 0", - padding: "0", - display: "block", - boxSizing: "border-box", - }, - ".card--material": { - backgroundColor: "white", - borderRadius: "8px", - boxShadow: "0 2px 2px 0 rgba(0, 0, 0, 0.14), 0 1px 5px 0 rgba(0, 0, 0, 0.12),\r\n 0 3px 1px -2px rgba(0, 0, 0, 0.2)", - fontFamily: '"Roboto", "Noto", sans-serif', - webkitFontSmoothing: "antialiased", - fontWeight: "400", - }, - ".card--material__content": { - fontSize: "14px", - lineHeight: "1.4", - color: "rgba(0, 0, 0, 0.54)", - }, - ".card--material__title": { - fontFamily: '"Roboto", "Noto", sans-serif', - webkitFontSmoothing: "antialiased", - fontWeight: "400", - fontSize: "24px", - margin: "8px 0 12px 0", - }, - ".toast": { - fontFamily: '-apple-system, "Helvetica Neue", "Helvetica", "Arial", "Lucida Grande", sans-serif', - webkitFontSmoothing: "antialiased", - mozOsxFontSmoothing: "grayscale", - fontWeight: "400", - position: "absolute", - zIndex: "2", - left: "8px", - right: "8px", - bottom: "0", - margin: "8px 0", - borderRadius: "8px", - backgroundColor: "rgba(0, 0, 0, 0.8)", - - display: "flex", - minHeight: "48px", - lineHeight: "1.5", - boxSizing: "border-box", - padding: "16px 16px", - }, - ".toast__message": { - fontSize: "14px", - color: "white", - webkitBoxFlex: "1", - webkitFlexGrow: "1", - flexGrow: "1", - textAlign: "left", - margin: "0 16px 0 0", - whiteSpace: "normal", - }, - ".toast__button": { - fontSize: "14px", - color: "white", - webkitBoxFlex: "0", - webkitFlexGrow: "0", - flexGrow: "0", - webkitAppearance: "none", - mozAppearance: "none", - appearance: "none", - border: "none", - backgroundColor: "transparent", - textTransform: "uppercase", - }, - ".toast__button:focus": { - outline: "none", - }, - ".toast__button:active": { - opacity: "0.4", - }, - ".toast--material": { - left: "0", - right: "0", - bottom: "0", - // margin: "0", - backgroundColor: theme.palette.background.paper, - borderRadius: theme.shape.borderRadius, - border: `1px solid ${theme.palette.divider}`, - boxShadow: - "rgb(255, 255, 255) 0px 0px 0px 0px, rgba(0, 0, 0, 0.05) 0px 0px 0px 1px, rgba(0, 0, 0, 0.1) 0px 10px 15px -3px, rgba(0, 0, 0, 0.05) 0px 4px 6px -2px", - padding: "16px 24px", - margin: `16px 16px calc(16px + ${view.getWindowBottomInsets()}px) 16px`, - }, - ".toast--material__message": { - fontFamily: '"Roboto", "Noto", sans-serif', - webkitFontSmoothing: "antialiased", - fontWeight: "400", - margin: "0 24px 0 0", - }, - ".toast--material__button": { - fontFamily: '"Roboto", "Noto", sans-serif', - webkitFontSmoothing: "antialiased", - fontWeight: "400", - color: "#4a148c", - }, - ".toolbar + .page__background": { - top: "44px", - }, - ".toolbar + .page__background + .page__content": { - top: "44px", - paddingTop: "0", - }, - ".page-with-bottom-toolbar > .page__content": { - bottom: "44px", - }, - ".toolbar.toolbar--material + .page__background": { - top: `calc(56px + ${view.getWindowTopInsets()}px)`, - paddingBottom: view.getWindowBottomInsets(), - }, - ".toolbar.toolbar--material + .page__background + .page__content": { - top: `calc(56px + ${view.getWindowTopInsets()}px)`, - // paddingBottom: view.getWindowBottomInsets(), - paddingTop: "0", - }, - ".toolbar.toolbar--transparent + .page__background": { - top: "0", - }, - ".toolbar.toolbar--transparent.toolbar--cover-content + .page__background + .page__content, .toolbar.toolbar--transparent.toolbar--cover-content\r\n + .page__background\r\n + .page__content\r\n .page_content": - { - top: "0", - paddingTop: "44px", - }, - ".toolbar.toolbar--material.toolbar--transparent.toolbar--cover-content\r\n + .page__background\r\n + .page__content, .toolbar.toolbar--material.toolbar--transparent.toolbar--cover-content\r\n + .page__background\r\n + .page__content\r\n .page_content": - { - top: "0", - paddingTop: "56px", - }, - ".tabbar:not(.tabbar--top)": { - paddingBottom: "0", - }, - "@media (orientation: landscape)": { - "html[onsflag-iphonex-landscape] .page__content": { - paddingLeft: "44px", - paddingRight: "44px", - bottom: "0", - paddingBottom: "21px", - }, - "html[onsflag-iphonex-landscape] .dialog .page__content, html[onsflag-iphonex-landscape] .modal .page__content": { - paddingLeft: "0", - paddingRight: "0", - }, - "html[onsflag-iphonex-landscape] .toolbar__left": { - paddingLeft: "44px", - }, - "html[onsflag-iphonex-landscape] .toolbar__right": { - paddingRight: "44px", - }, - "html[onsflag-iphonex-landscape] .bottom-bar": { - paddingRight: "44px", - paddingLeft: "44px", - bottom: "0", - boxSizing: "content-box", - paddingBottom: "21px", - }, - "html[onsflag-iphonex-landscape] .tabbar": { - paddingLeft: "44px", - paddingRight: "44px", - width: "calc(100% - 88px)", - }, - "html[onsflag-iphonex-landscape] .fab--bottom__left, html[onsflag-iphonex-landscape] .fab--bottom__center, html[onsflag-iphonex-landscape] .fab--bottom__right": - { - bottom: "21px", - }, - "html[onsflag-iphonex-landscape] .fab--top__left, html[onsflag-iphonex-landscape] .fab--bottom__left": { - left: "44px", - }, - "html[onsflag-iphonex-landscape] .fab--top__right, html[onsflag-iphonex-landscape] .fab--bottom__right": { - right: "44px", - }, - "html[onsflag-iphonex-landscape] .action-sheet": { - left: "calc((100vw - 100vh + 20px) / 2)", - right: "calc((100vw - 100vh + 20px) / 2)", - bottom: "33px", - }, - "html[onsflag-iphonex-landscape] .toast": { - left: "52px", - right: "52px", - bottom: "21px", - }, - "html[onsflag-iphonex-landscape] .dialog .bottom-bar, html[onsflag-iphonex-landscape] .page-with-bottom-toolbar > .page__content .bottom-bar, html[onsflag-iphonex-landscape] .tabbar__content:not(.tabbar--top__content) .bottom-bar": - { - bottom: "0", - boxSizing: "border-box", - paddingBottom: "0", - }, - "html[onsflag-iphonex-landscape] .dialog .page__content, html[onsflag-iphonex-landscape] .page-with-bottom-toolbar > .page__content .page__content, html[onsflag-iphonex-landscape] .tabbar__content:not(.tabbar--top__content) .page__content, html[onsflag-iphonex-landscape] .page-with-bottom-toolbar > .page__content": - { - bottom: "0", - paddingBottom: "0", - }, - "html[onsflag-iphonex-landscape] .page-with-bottom-toolbar > .page__content": { - bottom: "65px", - paddingBottom: "0", - }, - "html[onsflag-iphonex-landscape] .dialog .page-with-bottom-toolbar > .page__content, html[onsflag-iphonex-landscape] .page-with-bottom-toolbar > .page__content .page-with-bottom-toolbar > .page__content, html[onsflag-iphonex-landscape] .tabbar__content:not(.tabbar--top__content) .page-with-bottom-toolbar > .page__content": - { - bottom: "44px", - paddingBottom: "0", - }, - "html[onsflag-iphonex-landscape] .tabbar:not(.tabbar--top)": { - paddingBottom: "21px", - }, - "html[onsflag-iphonex-landscape] .dialog .tabbar:not(.tabbar--top), html[onsflag-iphonex-landscape] .page-with-bottom-toolbar > .page__content .tabbar:not(.tabbar--top), html[onsflag-iphonex-landscape] .tabbar__content:not(.tabbar--top__content) .tabbar:not(.tabbar--top)": - { - paddingBottom: "0", - }, - "html[onsflag-iphonex-landscape] .tabbar__content:not(.tabbar--top__content)": { - bottom: "70px", - }, - "html[onsflag-iphonex-landscape] .dialog .tabbar__content:not(.tabbar--top__content), html[onsflag-iphonex-landscape] .page-with-bottom-toolbar > .page__content .tabbar__content:not(.tabbar--top__content), html[onsflag-iphonex-landscape] .tabbar__content:not(.tabbar--top__content) .tabbar__content:not(.tabbar--top__content)": - { - bottom: "49px", - }, - "html[onsflag-iphonex-landscape] .page__content > .list:not(.list--inset)": { - marginLeft: "-44px", - marginRight: "-44px", - }, - "html[onsflag-iphonex-landscape] .page__content > .list:not(.list--inset) > .list-header": { - paddingLeft: "59px", - }, - "html[onsflag-iphonex-landscape] .page__content > .list:not(.list--inset) > .list-item": { - paddingLeft: "58px", - }, - "html[onsflag-iphonex-landscape]\r\n .page__content\r\n > .list:not(.list--inset)\r\n > .list-item--chevron:before": { - right: "60px", - }, - "html[onsflag-iphonex-landscape]\r\n .page__content\r\n > .list:not(.list--inset)\r\n > .list-item\r\n > .list-item__center:last-child": - { - paddingRight: "50px", - }, - "html[onsflag-iphonex-landscape]\r\n .page__content\r\n > .list:not(.list--inset)\r\n > .list-item\r\n > .list-item__right": - { - paddingRight: "56px", - }, - "html[onsflag-iphonex-landscape]\r\n .page__content\r\n > .list:not(.list--inset)\r\n > .list-item\r\n > .list-item--chevron__right": - { - paddingRight: "74px", - }, - "html[onsflag-iphonex-landscape] .dialog .page__content > .list:not(.list--inset)": { - marginLeft: "0", - marginRight: "0", - }, - "html[onsflag-iphonex-landscape] .dialog .page__content > .list:not(.list--inset) > .list-header": { - paddingLeft: "15px", - }, - "html[onsflag-iphonex-landscape] .dialog .page__content > .list:not(.list--inset) > .list-item": { - paddingLeft: "14px", - }, - "html[onsflag-iphonex-landscape]\r\n .dialog\r\n .page__content\r\n > .list:not(.list--inset)\r\n > .list-item--chevron:before": - { - right: "16px", - }, - "html[onsflag-iphonex-landscape]\r\n .dialog\r\n .page__content\r\n > .list:not(.list--inset)\r\n > .list-item\r\n > .list-item__center:last-child": - { - paddingRight: "6px", - }, - "html[onsflag-iphonex-landscape]\r\n .dialog\r\n .page__content\r\n > .list:not(.list--inset)\r\n > .list-item\r\n > .list-item__right": - { - paddingRight: "12px", - }, - "html[onsflag-iphonex-landscape]\r\n .dialog\r\n .page__content\r\n > .list:not(.list--inset)\r\n > .list-item\r\n > .list-item--chevron__right": - { - paddingRight: "30px", - }, - }, - "@media (orientation: portrait)": { - "html[onsflag-iphonex-portrait] .fab--top__left, html[onsflag-iphonex-portrait] .fab--top__center, html[onsflag-iphonex-portrait] .fab--top__right": - { - top: "64px", - }, - "html[onsflag-iphonex-portrait] .fab--bottom__left, html[onsflag-iphonex-portrait] .fab--bottom__center, html[onsflag-iphonex-portrait] .fab--bottom__right": - { - bottom: "34px", - }, - "html[onsflag-iphonex-portrait] .action-sheet": { - bottom: "48px", - }, - "html[onsflag-iphonex-portrait] .toast": { - bottom: "34px", - }, - "html[onsflag-iphonex-portrait] .toolbar": { - top: "0", - boxSizing: "content-box", - paddingTop: "44px", - }, - "html[onsflag-iphonex-portrait] .dialog .toolbar, html[onsflag-iphonex-portrait] .toolbar:not(.toolbar--cover-content)+.page__background+.page__content .toolbar, html[onsflag-iphonex-portrait] .tabbar--top__content .toolbar": - { - top: "0", - boxSizing: "border-box", - paddingTop: "0", - }, - "html[onsflag-iphonex-portrait] .bottom-bar": { - bottom: "0", - boxSizing: "content-box", - paddingBottom: "34px", - }, - "html[onsflag-iphonex-portrait] .dialog .bottom-bar, html[onsflag-iphonex-portrait] .page-with-bottom-toolbar > .page__content .bottom-bar, html[onsflag-iphonex-portrait] .tabbar__content:not(.tabbar--top__content) .bottom-bar": - { - bottom: "0", - boxSizing: "border-box", - paddingBottom: "0", - }, - "html[onsflag-iphonex-portrait] .page__content": { - top: "0", - paddingTop: "44px", - bottom: "0", - paddingBottom: "34px", - }, - "html[onsflag-iphonex-portrait] .dialog .page__content, html[onsflag-iphonex-portrait] .toolbar:not(.toolbar--cover-content)+.page__background+.page__content .page__content, html[onsflag-iphonex-portrait] .tabbar--top__content .page__content, html[onsflag-iphonex-portrait] .toolbar:not(.toolbar--cover-content)+.page__background+.page__content": - { - top: "0", - paddingTop: "0", - }, - "html[onsflag-iphonex-portrait] .dialog .page__content, html[onsflag-iphonex-portrait] .page-with-bottom-toolbar > .page__content .page__content, html[onsflag-iphonex-portrait] .tabbar__content:not(.tabbar--top__content) .page__content, html[onsflag-iphonex-portrait] .page-with-bottom-toolbar > .page__content": - { - bottom: "0", - paddingBottom: "0", - }, - "html[onsflag-iphonex-portrait] .toolbar:not(.toolbar--cover-content) + .page__background, html[onsflag-iphonex-portrait]\r\n .toolbar:not(.toolbar--cover-content)\r\n + .page__background\r\n + .page__content": - { - top: "88px", - paddingTop: "0", - }, - "html[onsflag-iphonex-portrait] .dialog .toolbar:not(.toolbar--cover-content)+.page__background, html[onsflag-iphonex-portrait] .dialog .toolbar:not(.toolbar--cover-content)+.page__background+.page__content, html[onsflag-iphonex-portrait] .toolbar:not(.toolbar--cover-content)+.page__background+.page__content .toolbar:not(.toolbar--cover-content)+.page__background, html[onsflag-iphonex-portrait] .toolbar:not(.toolbar--cover-content)+.page__background+.page__content .toolbar:not(.toolbar--cover-content)+.page__background+.page__content, html[onsflag-iphonex-portrait] .tabbar--top__content .toolbar:not(.toolbar--cover-content)+.page__background, html[onsflag-iphonex-portrait] .tabbar--top__content .toolbar:not(.toolbar--cover-content)+.page__background+.page__content": - { - top: "44px", - paddingTop: "0", - }, - "html[onsflag-iphonex-portrait] .page-with-bottom-toolbar > .page__content": { - bottom: "78px", - paddingBottom: "0", - }, - "html[onsflag-iphonex-portrait] .dialog .page-with-bottom-toolbar > .page__content, html[onsflag-iphonex-portrait] .page-with-bottom-toolbar > .page__content .page-with-bottom-toolbar > .page__content, html[onsflag-iphonex-portrait] .tabbar__content:not(.tabbar--top__content) .page-with-bottom-toolbar > .page__content": - { - bottom: "44px", - paddingBottom: "0", - }, - "html[onsflag-iphonex-portrait]\r\n .toolbar.toolbar--transparent.toolbar--cover-content\r\n + .page__background\r\n + .page__content, html[onsflag-iphonex-portrait]\r\n .toolbar.toolbar--transparent.toolbar--cover-content\r\n + .page__background\r\n + .page__content\r\n .page_content": - { - top: "0", - paddingTop: "88px", - }, - "html[onsflag-iphonex-portrait] .dialog .toolbar.toolbar--transparent.toolbar--cover-content+.page__background+.page__content, html[onsflag-iphonex-portrait] .dialog .toolbar.toolbar--transparent.toolbar--cover-content+.page__background+.page__content .page_content, html[onsflag-iphonex-portrait] .toolbar:not(.toolbar--cover-content)+.page__background+.page__content .toolbar.toolbar--transparent.toolbar--cover-content+.page__background+.page__content, html[onsflag-iphonex-portrait] .toolbar:not(.toolbar--cover-content)+.page__background+.page__content .toolbar.toolbar--transparent.toolbar--cover-content+.page__background+.page__content .page__content, html[onsflag-iphonex-portrait] .tabbar--top__content .toolbar.toolbar--transparent.toolbar--cover-content+.page__background+.page__content, html[onsflag-iphonex-portrait] .tabbar--top__content .toolbar.toolbar--transparent.toolbar--cover-content+.page__background+.page__content .page_content": - { - top: "0", - paddingTop: "44px", - }, - "html[onsflag-iphonex-portrait] .tabbar--top": { - paddingTop: "44px", - }, - "html[onsflag-iphonex-portrait] .dialog .tabbar--top, html[onsflag-iphonex-portrait] .toolbar:not(.toolbar--cover-content)+.page__background+.page__content .tabbar--top, html[onsflag-iphonex-portrait] .tabbar--top__content .tabbar--top": - { - paddingTop: "0", - }, - "html[onsflag-iphonex-portrait] .tabbar--top__content": { - top: "93px", - }, - "html[onsflag-iphonex-portrait] .dialog .tabbar--top__content, html[onsflag-iphonex-portrait] .toolbar:not(.toolbar--cover-content)+.page__background+.page__content .tabbar--top__content, html[onsflag-iphonex-portrait] .tabbar--top__content .tabbar--top__content": - { - top: "49px", - }, - "html[onsflag-iphonex-portrait] .tabbar:not(.tabbar--top):not(.tabbar--top)": { - paddingBottom: "34px", - }, - "html[onsflag-iphonex-portrait] .dialog .tabbar:not(.tabbar--top):not(.tabbar--top), html[onsflag-iphonex-portrait] .page-with-bottom-toolbar > .page__content .tabbar:not(.tabbar--top), html[onsflag-iphonex-portrait] .tabbar__content:not(.tabbar--top__content) .tabbar:not(.tabbar--top)": - { - paddingBottom: "0", - }, - "html[onsflag-iphonex-portrait] .tabbar__content:not(.tabbar--top__content)": { - bottom: "83px", - }, - "html[onsflag-iphonex-portrait] .dialog .tabbar__content:not(.tabbar--top__content), html[onsflag-iphonex-portrait] .page-with-bottom-toolbar > .page__content .tabbar__content:not(.tabbar--top__content), html[onsflag-iphonex-portrait] .tabbar__content:not(.tabbar--top__content) .tabbar__content:not(.tabbar--top__content)": - { - bottom: "49px", - }, - }, - })} - /> - ); -}; diff --git a/src/styles/onsenui.scss b/src/styles/onsenui.scss deleted file mode 100644 index 8bc93798..00000000 --- a/src/styles/onsenui.scss +++ /dev/null @@ -1,598 +0,0 @@ -ons-page, ons-navigator, -ons-tabbar, -ons-gesture-detector { - display: block; - touch-action: manipulation; /* Remove click delay */ -} -ons-navigator, -ons-tabbar, -ons-splitter, -ons-action-sheet, -ons-dialog, -ons-toast, -ons-alert-dialog { - position: absolute; - top: 0; - left: 0; - bottom: 0; - right: 0; - overflow: hidden; - touch-action: manipulation; /* Remove click delay */ -} -ons-toast { - pointer-events: none; -} -ons-toast .toast { - pointer-events: auto; -} -ons-tab { - -webkit-transform: translate3d(0, 0, 0); - transform: translate3d(0, 0, 0); -} -ons-page, ons-navigator, ons-tabbar, ons-dialog, ons-alert-dialog, ons-action-sheet, ons-toast { - z-index: 2; -} -ons-fab, ons-speed-dial { - z-index: 10001; -} -ons-toolbar:not([inline]), ons-bottom-toolbar { - position: absolute; - left: 0; - right: 0; - z-index: 10000; -} -ons-toolbar:not([inline]) { - top: 0; -} -ons-bottom-toolbar { - bottom: 0; -} -.page, .page__content, -.page--material, .page--material__content { - background-color: transparent !important; - background: transparent !important; -} -.page__content { - overflow: auto; - -webkit-overflow-scrolling: touch; - z-index: 0; - -ms-touch-action: pan-y; -} -.page.overflow-visible, -.page.overflow-visible .page, -.page.overflow-visible .page__content, -.page.overflow-visible ons-navigator, -.page.overflow-visible ons-splitter { - overflow: visible; -} -.page.overflow-visible .page__content.content-swiping, -.page.overflow-visible .page__content.content-swiping .page, -.page.overflow-visible .page__content.content-swiping .page__content { - overflow: auto; -} -.page[status-bar-fill] > .page__content { - top: 20px; -} -.page[status-bar-fill] > .toolbar { - padding-top: 20px; - box-sizing: content-box; -} -.page[status-bar-fill] > .toolbar:not(.toolbar--transparent) + .page__background, -.page[status-bar-fill] > .toolbar:not(.toolbar--cover-content) + .page__background + .page__content { - top: 64px; -} -.page[status-bar-fill] > .toolbar--material:not(.toolbar-transparent) + .page__background, -.page[status-bar-fill] > .toolbar--material:not(.toolbar--cover-content) + .page__background + .page__content { - top: 76px; -} -.page[status-bar-fill] > .toolbar.toolbar--transparent + .page__background { - top: 0; -} -ons-tabbar[status-bar-fill] > .tabbar--top__content { - top: 71px; -} -ons-tabbar[status-bar-fill] > .tabbar--top { - padding-top: 22px; -} -ons-tabbar[position="top"] .page[status-bar-fill] > .page__content { - top: 0px; -} -.toolbar + .page__background + .page__content ons-tabbar[status-bar-fill] > .tabbar--top { - top: 0px; -} -.toolbar + .page__background + .page__content ons-tabbar[status-bar-fill] > .tabbar--top__content { - top: 49px; -} -.page__content > .list:not(.list--material):first-child { - margin-top: -1px; /* Avoid double border with toolbar */ -} -.page--wrapper > .page__background { - display: none; -} -ons-action-sheet[disabled], -ons-dialog[disabled], -ons-alert-dialog[disabled], -ons-popover[disabled] { - pointer-events: none; - opacity: 0.75; -} -ons-list-item[disabled] { - pointer-events: none; -} -ons-range[disabled] { - opacity: 0.3; - pointer-events: none; -} -ons-pull-hook { - position: relative; - display: block; - margin: auto; - text-align: center; - z-index: 20002; -} -ons-splitter, ons-splitter-mask, ons-splitter-content, ons-splitter-side { - display: block; - position: absolute; - left: 0; - right: 0; - top: 0; - bottom: 0; - box-sizing: border-box; - z-index: 0; -} -ons-splitter-mask { - z-index: 4; - // background-color: rgba(0, 0, 0, 0.1); - display: none; - opacity: 0; - background-color: rgba(0, 0, 0, 0.5) !important; - backdrop-filter: blur(4px); - -webkit-backdrop-filter: blur(4px); -} -ons-splitter-content { - z-index: 2; -} -ons-splitter-side { - right: auto; - z-index: 2; -} -ons-splitter-side[side="right"] { - right: 0; - left: auto; -} -ons-splitter-side[mode="collapse"] { - z-index: 5; - left: auto; - right: 100%; -} -ons-splitter-side[side="right"][mode="collapse"] { - right: auto; - left: 100%; -} -ons-splitter-side[mode="split"] { - z-index: 3; -} -ons-toolbar-button > ons-icon[icon*="ion-"] { - font-size: 26px; -} -ons-range, ons-select { - display: inline-block; -} -ons-range > input { - min-width: 50px; - width: 100%; -} -ons-select > select { - width: 100%; -} -ons-carousel[disabled] { - pointer-events: none; - opacity: 0.75; -} -ons-carousel[fullscreen] { - height: 100%; -} -.ons-status-bar-mock { - position: absolute; - width: 100%; - height: 20px; - padding: 0 16px 0 6px; - box-sizing: border-box; - z-index: 30000; - display: -webkit-flex; - display: flex; - -webkit-justify-content: space-between; - justify-content: space-between; - font-size: 12px; - line-height: 20px; - font-family: Arial, Helvetica; -} -.ons-status-bar-mock i { - padding: 0 2px; -} -.ons-status-bar-mock.android { - color: white; - background-color: #222; - font-family: Roboto, Arial, Helvetica; -} -.ons-status-bar-mock.android ~ * { - top: 20px; - bottom: 0; - position: inherit; - width: 100%; -} -.ons-ios-scroll-fix .page:not(.page--wrapper)[shown] > .page__content { - overflow-y: hidden; -} -.ons-ios-scroll-fix ons-splitter-side > .page:not(.page--wrapper)[shown] > .page__content { - overflow-y: auto; -} -ons-row { - display: -webkit-flex; - display: -moz-flex; - display: flex; - -webkit-flex-wrap: wrap; - flex-wrap: wrap; - width: 100%; - box-sizing: border-box; -} -ons-row[vertical-align="top"], ons-row[align="top"] { - box-align: start; - -webkit-align-items: flex-start; - -moz-align-items: flex-start; - align-items: flex-start; -} -ons-row[vertical-align="bottom"], ons-row[align="bottom"] { - box-align: end; - -webkit-align-items: flex-end; - -moz-align-items: flex-end; - align-items: flex-end; -} -ons-row[vertical-align="center"], ons-row[align="center"] { - box-align: center; - -webkit-align-items: center; - -moz-align-items: center; - align-items: center; - text-align: inherit; -} -ons-row + ons-row { - padding-top: 0; -} -ons-col { - -webkit-flex: 1; - -moz-flex: 1; - flex: 1; - display: block; - width: 100%; - box-sizing: border-box; -} -ons-col[vertical-align="top"], ons-col[align="top"] { - -webkit-align-self: flex-start; - align-self: flex-start; -} -ons-col[vertical-align="bottom"], ons-col[align="bottom"] { - -webkit-align-self: flex-end; - align-self: flex-end; } -ons-col[vertical-align="center"], ons-col[align="center"] { - -webkit-align-self: center; - -moz-align-self: center; - -ms-flex-item-align: center; - text-align: inherit; -} -/* -Copyright 2013-2015 ASIAL CORPORATION - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. - - */ -.ons-icon { - display: inline-block; - line-height: inherit; - font-style: normal; - font-weight: normal; - font-size: inherit; - text-rendering: auto; - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; -} -.segment__button .ons-icon { - line-height: normal; - line-height: initial; -} -:not(ons-toolbar-button):not(ons-action-sheet-button):not(.segment__button) > .ons-icon--ion { - line-height: 0.75em; - vertical-align: -25%; -} -.ons-icon[spin] { - -webkit-animation: ons-icon-spin 2s infinite linear; - animation: ons-icon-spin 2s infinite linear; -} -@-webkit-keyframes ons-icon-spin { - 0% { - -webkit-transform: rotate(0deg); - } - 100% { - -webkit-transform: rotate(359deg); - } -} -@keyframes ons-icon-spin { - 0% { - -webkit-transform: rotate(0deg); - transform: rotate(0deg); - } - 100% { - -webkit-transform: rotate(359deg); - transform: rotate(359deg); - } -} -.ons-icon[rotate="90"] { - -webkit-transform: rotate(90deg); - transform: rotate(90deg); -} -.ons-icon[rotate="180"] { - -webkit-transform: rotate(180deg); - transform: rotate(180deg); -} -.ons-icon[rotate="270"] { - -webkit-transform: rotate(270deg); - transform: rotate(270deg); -} -.ons-icon[fixed-width] { - width: 1.28571429em; - text-align: center; -} -.ons-icon--lg { - font-size: 1.33333333em; - line-height: 0.75em; - vertical-align: -15%; -} -.ons-icon--2x { - font-size: 2em; -} -.ons-icon--3x { - font-size: 3em; -} -.ons-icon--4x { - font-size: 4em; -} -.ons-icon--5x { - font-size: 5em; -} -/** - * ons-icon with Font Awesome backward compatibility - */ -.ons-icon.fa { - font-family: 'Font Awesome 5 Brands', 'Font Awesome 5 Free'; - font-weight: 900; -} -ons-input, ons-radio, ons-checkbox, ons-search-input { - display: inline-block; - position: relative; -} -ons-input .text-input, -ons-search-input .search-input { - width: 100%; - display: inline-block; -} -ons-input .text-input__label:not(.text-input--material__label) { - display: none; -} -ons-input:not([float]) .text-input--material__label--active { - display: none; -} -ons-input[disabled], -ons-radio[disabled], -ons-checkbox[disabled], -ons-segment[disabled], -ons-search-input[disabled] { - opacity: 0.5; - pointer-events: none; -} -ons-input input.text-input--material::-webkit-input-placeholder { - color: transparent; -} -ons-input input.text-input--material::-moz-placeholder { - color: transparent; -} -ons-input input.text-input--material:-ms-input-placeholder { - color: transparent; -} -/* Suppress safe area support for pages in splitter sides */ -@media (orientation: landscape) { - html[onsflag-iphonex-landscape] ons-splitter-side[side="left"] .page__content { - padding-right: 0; - } - html[onsflag-iphonex-landscape] ons-splitter-side[side="right"] .page__content { - padding-left: 0; - } -} -/* Support the situation that a progress bar is located just below a toolbar */ -@media (orientation: landscape) { - html[onsflag-iphonex-landscape] .page__content > ons-progress-bar > .progress-bar { - margin-left: -44px; - margin-right: -44px; - width: calc(100% + 88px); - } -} -/* Lists in .page__content in splitter-sides */ -@media (orientation: landscape) { - /* Suppress left safe area support for pages in right splitter sides */ - html[onsflag-iphonex-landscape] ons-splitter-side[side="right"] .page__content > .list:not(.list--inset) { - margin-left: 0; - } - html[onsflag-iphonex-landscape] ons-splitter-side[side="right"] .page__content > .list:not(.list--inset) > .list-header { - padding-left: 15px; - } - html[onsflag-iphonex-landscape] ons-splitter-side[side="right"] .page__content > .list:not(.list--inset) > .list-item { - padding-left: 14px; - } - - /* Suppress right safe area support for pages in left splitter sides */ - html[onsflag-iphonex-landscape] ons-splitter-side[side="left"] .page__content > .list:not(.list--inset) { - margin-right: 0; - } - html[onsflag-iphonex-landscape] ons-splitter-side[side="left"] .page__content > .list:not(.list--inset) > .list-item--chevron:before { - right: 16px; - } - html[onsflag-iphonex-landscape] ons-splitter-side[side="left"] .page__content > .list:not(.list--inset) > .list-item > .list-item__center:last-child { - padding-right: 6px; - } - html[onsflag-iphonex-landscape] ons-splitter-side[side="left"] .page__content > .list:not(.list--inset) > .list-item > .list-item__right { - padding-right: 12px; - } - html[onsflag-iphonex-landscape] ons-splitter-side[side="left"] .page__content > .list:not(.list--inset) > .list-item > .list-item--chevron__right { - padding-right: 30px; - } -} -/* -Copyright 2013-2015 ASIAL CORPORATION - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. - - */ -ons-progress-bar { - display: block; -} -ons-progress-circular { - display: inline-block; - width: 32px; - height: 32px; -} -ons-progress-circular > svg.progress-circular { - width: 100%; - height: 100%; -} -/* -Copyright 2013-2015 ASIAL CORPORATION - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. - - */ -.ripple { - display: block; - position: absolute; - overflow: hidden; - top: 0; - left: 0; - right: 0; - bottom: 0; - pointer-events: none; -} -.ripple__background { - background: rgba(255, 255, 255, 0.2); - position: absolute; - top: 0; - left: 0; - right: 0; - bottom: 0; - opacity: 0; - pointer-events: none; -} -.ripple__wave { - background: rgba(255, 255, 255, 0.2); - width: 0; - height: 0; - border-radius: 50%; - position: absolute; - top: 0; - left: 0; - z-index: 0; - pointer-events: none; -} -/* FIXME */ -ons-list-item .ripple__wave, -ons-list-item .ripple__background, -.button--material--flat .ripple__wave, -.button--material--flat .ripple__background { - background: rgba(189, 189, 189, 0.3); -} -.ripple--light-gray__wave, -.ripple--light-gray__background { - background: rgba(189, 189, 189, 0.3); -} -/* -Copyright 2013-2015 ASIAL CORPORATION - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. - - */ -.ons-swiper { - overflow: hidden; - display: block; - box-sizing: border-box; -} -.ons-swiper-target { - display: -webkit-flex; - display: flex; - height: 100%; - z-index: 1; - -webkit-flex-direction: row; - flex-direction: row; -} -.ons-swiper-target--vertical { - -webkit-flex-direction: column; - flex-direction: column; -} -.ons-swiper-target > * { - height: inherit; - -webkit-flex-shrink: 0; - flex-shrink: 0; - box-sizing: border-box; - width: 100%; - position: relative !important; -} -.ons-swiper-target.active:not(.swiping) > .page:not([shown]) { - visibility: hidden; -} -.ons-swiper-tabbar .tabbar--material__button:after { - display: none; -} -.ons-swiper-blocker { - display: block; - position: absolute; - top: 0; - bottom: 0; - left: 0; - right: 0; - pointer-events: none -} diff --git a/src/typings/ModuleProps.ts b/src/typings/ModuleProps.ts deleted file mode 100644 index ed7298aa..00000000 --- a/src/typings/ModuleProps.ts +++ /dev/null @@ -1,22 +0,0 @@ -namespace ModuleProps { - export interface FoxProps { - minApi?: int; - maxApi?: int - minMagisk?: string | int; - needRamdisk?: boolean; - support?: string; - donate?: string; - config?: string; - changeBoot?: boolean; - } - - export interface RootObject { - id: string; - last_update: number; - zip_url: string; - notes_url: string; - prop_url: string; - } -} - -export default ModuleProps; diff --git a/src/typings/android/buildconfig.d.ts b/src/typings/android/buildconfig.d.ts deleted file mode 100644 index 3b83f473..00000000 --- a/src/typings/android/buildconfig.d.ts +++ /dev/null @@ -1,13 +0,0 @@ -interface NBuildConfig { - VERSION_CODE(): int; - VERSION_NAME(): string; - APPLICATION_ID(): string; - /** - * @deprecated - */ - SDK_INT(): int; - DEBUG(): bool; - BUILD_TYPE(): string; -} - -export default NBuildConfig; diff --git a/src/typings/android/os.d.ts b/src/typings/android/os.d.ts deleted file mode 100644 index 4b493e1f..00000000 --- a/src/typings/android/os.d.ts +++ /dev/null @@ -1,23 +0,0 @@ -interface NOS { - makeToast(content: string | undefined, duration: int| undefined): void; - getSchemeParam(param: string): string; - hasStoragePermission(): bool; - requestStoargePermission(): void; - open(link: string): void; - close(): void; - isPackageInstalled(targetPackage: string): bool; - launchAppByPackageName(targetPackage: string): void; - getMonetColor(id: string): string; - getColorRes(id: string): string; - setStatusBarColor(color: string, white?: bool): void; - setNavigationBarColor(color: string): void; - /** - * @deprecated - */ - log(tag: string, message: string): void; - logi(tag: string, message: T | T[]): void; - logw(tag: string, message: T | T[]): void; - loge(tag: string, message: T | T[]): void; -} - -export default NOS; diff --git a/src/typings/android/shell.d.ts b/src/typings/android/shell.d.ts deleted file mode 100644 index d42b5b05..00000000 --- a/src/typings/android/shell.d.ts +++ /dev/null @@ -1,7 +0,0 @@ -interface NShell { - exec(command: string): void; - result(command: string): string; - isAppGrantedRoot(): boolean; -} - -export default NShell; diff --git a/src/typings/declaration.d.ts b/src/typings/declaration.d.ts deleted file mode 100644 index b300f3fb..00000000 --- a/src/typings/declaration.d.ts +++ /dev/null @@ -1,706 +0,0 @@ -declare module "react-console"; - -declare module "react-onsenui" { - export type HTMLAttributes> = Partial, K>>; - export type InputHTMLAttributes> = Partial, K>>; - - export class Component

extends React.Component & P, S> {} - - export interface Modifiers_string { - default?: string | undefined; - material?: string | undefined; - } - - export interface Modifiers_number { - default?: number | undefined; - material?: number | undefined; - } - - export interface AnimationOptions { - duration?: number | undefined; - delay?: number | undefined; - timing?: string | undefined; - } - - export interface PullHookChangeEvent { - state: "initial" | "preaction" | "action"; - } - - export type NavigatorAnimationTypes = "slide" | "lift" | "fade" | "none" | string; - - export interface PageTransitionOptions { - animation?: NavigatorAnimationTypes | undefined; - animationOptions?: AnimationOptions | undefined; - callback?: (() => void) | undefined; - data?: any; - } - - export interface SwitchChangeEvent extends Event { - switch: HTMLElement; - value: boolean; - isInteractive: boolean; - } - - /*** splitter ***/ - export class SplitterSide extends Component< - { - children?: React.ReactNode; - side?: "left" | "right" | undefined; - collapse?: "portrait" | "landscape" | boolean | undefined; - isOpen?: boolean | undefined; - onOpen?(e?: Event): void; - onPreOpen?(e?: Event): void; - onPreClose?(e?: Event): void; - onModeChange?(e?: Event): void; - onClose?(e?: Event): void; - swipeable?: boolean | undefined; - swipeTargetWidth?: number | undefined; - width?: number | undefined; - animation?: "overlay" | "default" | undefined; - animationOptions?: AnimationOptions | undefined; - openThreshold?: number | undefined; - mode?: "collapse" | "split" | undefined; - }, - any - > {} - - export class SplitterContent extends Component<{ children?: React.ReactNode }> {} - - export class Splitter extends Component<{ children?: React.ReactNode }> {} - - /*** toolbar ***/ - - export class Toolbar extends Component< - { - children: React.ReactNode; - modifier?: string | undefined; - }, - any - > {} - - export class BottomToolbar extends Component< - { - modifier?: string | undefined; - }, - any - > {} - - export class ToolbarButton extends Component< - { - children: JSX.Element; - modifier?: string | undefined; - disabled?: boolean | undefined; - onClick?(e?: React.MouseEvent): void; - }, - any - > {} - - /*** icon ***/ - export class Icon extends Component< - { - modifier?: string | undefined; - icon?: string | Modifiers_string | undefined; - size?: number | Modifiers_number | undefined; - rotate?: 90 | 180 | 270 | undefined; - fixedWidth?: boolean | undefined; - spin?: boolean | undefined; - title?: string | undefined; - }, - any - > {} - - /*** page ***/ - export class Page extends Component< - { - children?: React.ReactNode; - contentStyle?: any; - modifier?: string | undefined; - renderModal?(): void; - renderToolbar?(): void; - renderBottomToolbar?(): void; - renderFixed?(): void; - onInit?(): void; - onShow?(): void; - onHide?(): void; - onInfiniteScroll?(doneCallback: () => void): void; - }, - any - > {} - - /*** Grid ***/ - export class Col extends Component< - { - verticalAlign?: "top" | "bottom" | "center" | undefined; - width?: string | undefined; - children?: React.ReactNode; - }, - any - > {} - - export class Row extends Component< - { - verticalAlign?: "top" | "bottom" | "center" | undefined; - children?: React.ReactNode; - }, - any - > {} - - /*** Navigation ***/ - export class BackButton extends Component< - { - modifier?: string | undefined; - onClick?(navigator?: Navigator): void; - context?: any; - }, - any - > {} - - export class Navigator extends Component< - { - renderPage(route: any, navigator?: Navigator): JSX.Element; - initialRouteStack?: string[] | undefined; - initialRoute?: any; - onPrePush?(): void; - onPostPush?(): void; - onPrePop?(): void; - onPostPop?(): void; - animation?: NavigatorAnimationTypes | undefined; - animationOptions?: AnimationOptions | undefined; - }, - any - > { - pages: any[]; - routes: any[]; - resetPage(route: any, options?: PageTransitionOptions): Promise; - resetPageStack(route: any, options?: PageTransitionOptions): Promise; - pushPage(route: any, options?: PageTransitionOptions): Promise; - popPage(options?: PageTransitionOptions): Promise; - } - - // Still incomplete, see https://onsen.io/v2/api/react/RouterNavigator.html - export class RouterNavigator extends Component< - { - routeConfig: any; - renderPage(route: any, navigator?: Navigator): JSX.Element; - swipeable?: boolean | "force" | undefined; - swipePop?: ((options: any) => void) | undefined; - swipeTargetWidth?: number | undefined; - animation?: string | undefined; - onPostPush(): void; - onPostPop(): void; - }, - any - > {} - - // https://github.com/OnsenUI/OnsenUI/blob/master/bindings/react/src/RouterUtil.js - export type Route = any; - export type RouterProcess = object; // incomplete - - export interface RouteConfig { - routeStack: Route[]; - processStack: RouterProcess[]; - } - - export const RouterUtil: { - init: (routes: Route[]) => RouteConfig; - replace: (config: { routeConfig: RouteConfig; route: Route; options?: any; key?: any }) => RouteConfig; - reset: (config: { routeConfig: RouteConfig; route: Route; options?: any; key?: any }) => RouteConfig; - push: (config: { routeConfig: RouteConfig; route: Route; options?: any; key?: any }) => RouteConfig; - pop: (config: { routeConfig: RouteConfig; options?: any; key?: any }) => RouteConfig; - postPush: (routeConfig: RouteConfig) => RouteConfig; - postPop: (routeConfig: RouteConfig) => RouteConfig; - }; - - /*** Carousel ***/ - export class Carousel extends Component< - { - direction?: "horizontal" | "vertical" | undefined; - fullscreen?: boolean | undefined; - overscrollable?: boolean | undefined; - centered?: boolean | undefined; - itemWidth?: number | string | undefined; - itemHeight?: number | string | undefined; - autoScroll?: boolean | undefined; - autoScrollRatio?: number | undefined; - swipeable?: boolean | undefined; - disabled?: boolean | undefined; - index?: number | undefined; - autoRefresh?: boolean | undefined; - onPostChange?(): void; - onRefresh?(): void; - onOverscroll?(): void; - animationOptions?: AnimationOptions | undefined; - }, - any - > {} - - export class CarouselItem extends Component< - { - modifier?: string | undefined; - }, - any - > {} - - /*** AlertDialog ***/ - export class AlertDialog extends Component< - { - children: React.ReactNode; - onCancel?(): void; - isOpen?: boolean | undefined; - isCancelable?: boolean | undefined; - isDisabled?: boolean | undefined; - animation?: "none" | "default" | undefined; - modifier?: string | undefined; - maskColor?: string | undefined; - animationOptions?: AnimationOptions | undefined; - onPreShow?(): void; - onPostShow?(): void; - onPreHide?(): void; - onPostHide?(): void; - }, - any - > {} - - export class AlertDialogButton extends Component< - { - onClick?(): void; - modifier?: string | undefined; - disabled?: boolean | undefined; - }, - any - > {} - - export class Dialog extends Component< - { - onCancel?(): void; - isOpen?: boolean | undefined; - isCancelable?: boolean | undefined; - isDisabled?: boolean | undefined; - animation?: "none" | "default" | undefined; - modifier?: string | undefined; - maskColor?: string | undefined; - animationOptions?: AnimationOptions | undefined; - onPreShow?(): void; - onPostShow?(): void; - onPreHide?(): void; - onPostHide?(): void; - }, - any - > {} - - export class Modal extends Component< - { - animation?: "fade" | "lift" | "none" | undefined; - animationOptions?: AnimationOptions | undefined; - onPreShow?(): void; - onPostShow?(): void; - onPreHide?(): void; - onPostHide?(): void; - isOpen?: boolean | undefined; - onDeviceBackButton?(): void; - }, - any - > {} - - export class Popover extends Component< - { - children?: React.ReactNode; - getTarget?(): /*React.ReactInstance |*/ any; - onCancel?(): void; - isOpen?: boolean | undefined; - isCancelable?: boolean | undefined; - isDisabled?: boolean | undefined; - animation?: "none" | "default" | undefined; - modifier?: string | undefined; - maskColor?: string | undefined; - animationOptions?: AnimationOptions | undefined; - onPreShow?(): void; - onOpen?(): void; - onHide?(): void; - onPostShow?(): void; - onPreHide?(): void; - onPostHide?(): void; - }, - any - > {} - - export class Toast extends Component< - { - isOpen: boolean; - animation?: "default" | "ascend" | "lift" | "fall" | "fade" | "none" | undefined; - modifier?: string | undefined; - animationOptions?: AnimationOptions | undefined; - onPreShow?(): void; - onPostShow?(): void; - onPreHide?(): void; - onPostHide?(): void; - onDeviceBackButton?(): void; - }, - any - > {} - - export class ActionSheet extends Component< - { - onCancel?(): void; - isOpen?: boolean | undefined; - isCancelable?: boolean | undefined; - isDisabled?: boolean | undefined; - animation?: string | undefined; - modifier?: string | undefined; - maskColor?: string | undefined; - animationOptions?: {} | undefined; - title?: string | undefined; - onPreShow?(): void; - onPostShow?(): void; - onPreHide?(): void; - onPostHide?(): void; - onDeviceBackButton?(): void; - }, - any - > {} - - export class ActionSheetButton extends Component< - { - modifier?: string | undefined; - icon?: string | undefined; - onClick?(e?: React.MouseEvent): void; - }, - any - > {} - - export class ProgressBar extends Component< - { - modifier?: string | undefined; - value?: number | undefined; - secondaryValue?: number | undefined; - indeterminate?: boolean | undefined; - }, - any - > {} - - export class ProgressCircular extends Component< - { - modifier?: string | undefined; - value?: number | undefined; - secondaryValue?: boolean | undefined; - indeterminate?: boolean | undefined; - }, - any - > {} - - export class Ripple extends Component< - { - color?: string | undefined; - background?: string | undefined; - disabled?: boolean | undefined; - modifier?: string | undefined; - }, - any - > {} - - /*** Forms ***/ - export class Fab extends Component< - { - modifier?: string | undefined; - ripple?: boolean | undefined; - position?: string | undefined; - disabled?: boolean | undefined; - onClick?(e?: React.MouseEvent): void; - name?: string | undefined; - }, - any - > {} - - export class Button extends Component< - { - children?: React.ReactNode; - modifier?: string | undefined; - disabled?: boolean | undefined; - ripple?: boolean | undefined; - name?: string | undefined; - icon?: string | undefined; - onClick?(e?: React.MouseEvent): void; - }, - any - > {} - - export class Input extends Component< - InputHTMLAttributes<"min" | "max" | "step"> & { - modifier?: string | undefined; - disabled?: boolean | undefined; - readOnly?: boolean | undefined; - onChange?: ((e: React.ChangeEvent) => void) | undefined; - onBlur?: ((e: React.FocusEvent) => void) | undefined; - onFocus?: ((e: React.FocusEvent) => void) | undefined; - value?: string | undefined; - defaultValue?: string | undefined; - checked?: boolean | undefined; - placeholder?: string | undefined; - type?: string | undefined; - inputId?: string | undefined; - float?: boolean | undefined; - name?: string | undefined; - autoFocus?: boolean | undefined; - }, - any - > {} - - export class Radio extends Component< - { - modifier?: string | undefined; - disabled?: boolean | undefined; - onChange?(e: Event): void; - value?: string | undefined; - checked?: boolean | undefined; - defaultChecked?: boolean | undefined; - inputId?: string | undefined; - name?: string | undefined; - }, - any - > {} - - export class Checkbox extends Component< - { - modifier?: string | undefined; - disabled?: boolean | undefined; - onChange?(e: Event): void; - value?: string | undefined; - checked?: boolean | undefined; - inputId?: string | undefined; - name?: string | undefined; - }, - any - > {} - - export class Range extends Component< - { - modifier?: string | undefined; - onChange?(e: Event): void; - value?: number | undefined; - disabled?: boolean | undefined; - }, - any - > {} - - export class SearchInput extends Component< - { - placeholder?: string; - modifier?: string | undefined; - disabled?: boolean | undefined; - onChange?(e: Event): void; - value?: string | undefined; - inputId?: string | undefined; - }, - any - > {} - - export class Select extends Component< - { - children?: React.ReactNode; - modifier?: string | undefined; - disabled?: boolean | undefined; - onChange?: ((e: React.ChangeEvent) => void) | undefined; - value?: string | undefined; - multiple?: boolean | undefined; - autofocus?: boolean | undefined; - required?: boolean | undefined; - form?: string | undefined; - size?: string | undefined; - name?: string | undefined; - }, - any - > {} - - export class Switch extends Component< - { - modifier?: string | undefined; - onChange?(e: SwitchChangeEvent): void; - checked?: boolean | undefined; - disabled?: boolean | undefined; - inputId?: string | undefined; - name?: string | undefined; - autoFocus?: boolean | undefined; - }, - any - > {} - - /** - * Tabs - */ - - export class Tab extends Component<{ - label?: string | undefined; - icon?: string | undefined; - }> {} - - export class TabActive extends Component {} - - export class TabInactive extends Component {} - - export interface TabbarRenderTab { - content: JSX.Element; - tab: JSX.Element; - } - - export class Tabbar extends Component< - { - index?: number; - renderTabs(index: number, tabbar: Tabbar): TabbarRenderTab[]; - position?: "bottom" | "top" | "auto" | undefined; - swipeable?: boolean | undefined; - ignoreEdgeWidth?: number | undefined; - animation?: "none" | "slide" | undefined; - animationOptions?: AnimationOptions | undefined; - tabBorder?: boolean | undefined; - onPreChange?(): void; - onPostChange?(): void; - onReactive?(): void; - onSwipe?(index: number, animationOptions: AnimationOptions): void; - }, - any - > {} - - /** - * Lists - */ - - export class LazyList extends Component< - { - modifier?: string | undefined; - length?: number | undefined; - renderRow(rowIndex: number): any; - calculateItemHeight(rowIndex: number): any; - }, - any - > {} - - export class List extends Component< - { - children: React.ReactNode; - modifier?: string | undefined; - dataSource?: T[] | undefined; - renderRow?(row: T, index?: number): JSX.Element | undefined; - renderFooter?(): JSX.Element | undefined; - renderHeader?(): JSX.Element | undefined; - }, - any - > {} - - export class ListHeader extends Component< - { - children?: React.ReactNode; - modifier?: string | undefined; - }, - any - > {} - - export class ListItem extends Component< - { - children?: React.ReactNode; - modifier?: string | undefined; - tappable?: boolean | undefined; - tapBackgroundColor?: string | undefined; - lockOnDrag?: boolean | undefined; - expandable?: boolean | undefined; - expanded?: boolean | undefined; - onClick?: React.MouseEventHandler | undefined; - }, - any - > {} - - export class ListTitle extends Component< - { - children: React.ReactNode; - modifier?: string | undefined; - onClick?: React.MouseEventHandler | undefined; - }, - any - > {} - - export class Card extends Component< - { - children?: React.ReactNode; - modifier?: string | undefined; - onClick?(e?: React.MouseEvent): void; - style?: React.CSSProperties; - }, - any - > {} - - /** - * Controls - */ - - /** Pull-to-refresh hook. */ - export class PullHook extends Component< - { - children: string; - onChange?(e: PullHookChangeEvent): void; - onLoad?(done: () => void): void; - onPull?(): void; - disabled?: boolean | undefined; - height?: number | undefined; - thresholdHeight?: number | undefined; - fixedContent?: boolean | undefined; - }, - any - > {} - - export class Segment extends Component< - { - index?: number | undefined; - tabbarId?: string | undefined; - modifier?: string | undefined; - onPostChange?(): void; - }, - any - > {} - - export class GestureDetector extends Component< - { - children?: React.ReactNode; - onDrag?(): void; - onDragLeft?(): void; - onDragRight?(): void; - onDragUp?(): void; - onDragDown?(): void; - onHold?(): void; - onRelease?(): void; - onSwipe?(): void; - onSwipeLeft?(): void; - onSwipeRight?(): void; - onSwipeUp?(): void; - onSwipeDown?(): void; - onTap?(): void; - onDoubleTap?(): void; - onPinch?(): void; - onPinchIn?(): void; - onPinchOut?(): void; - onTouch?(): void; - onTransform?(): void; - onRotate?(): void; - }, - any - > {} - - export type SpeedDialPosition = "top" | "right" | "bottom" | "left" | "top right" | "top left" | "bottom right" | "bottom left"; - export type SpeedDialDirection = "up" | "down" | "left" | "right"; - - export class SpeedDial extends Component< - { - modifier?: string | undefined; - position?: SpeedDialPosition | undefined; - direction?: SpeedDialDirection | undefined; - disabled?: boolean | undefined; - }, - any - > {} - - export class SpeedDialItem extends Component< - { - modifier?: string | undefined; - onClick?(e?: React.MouseEvent): void; - }, - any - > {} -} diff --git a/src/typings/global.d.ts b/src/typings/global.d.ts deleted file mode 100644 index 9b2c5dce..00000000 --- a/src/typings/global.d.ts +++ /dev/null @@ -1,346 +0,0 @@ -import { AlertColor } from "@mui/material/Alert"; -import { AvailableStrs, strs } from "./../locales/declaration"; -import { Theme } from "@mui/material"; -import { en_antifeatures } from "locales/antifeatures/en"; -import { RootManagerV2 } from "@Native/Shell"; - -export {}; - -declare module "*.d.ts" { - const value: string; - export default value; -} - -declare global { - type arr = Array; - type list = Array; - type str = string; - type Str = String; - type int = number; - type Int = Number; - type Void = void; - type Any = any; - type bool = boolean; - - type HTMLAttributes = React.DetailedHTMLProps & P, E>; - type AnchorHTMLAttributes = React.DetailedHTMLProps & P, E>; - - type VersionType = `${string}.${string}.${string}`; - - namespace JSX { - interface IntrinsicElements { - "mmrl-anchor": React.DetailedHTMLProps & { page?: string }, HTMLAnchorElement>; - - // Onsen Elements - "ons-toolbar-button": HTMLAttributes; - "ons-toolbar": HTMLAttributes; - "ons-page": HTMLAttributes; - "ons-splitter": HTMLAttributes; - "ons-splitter-content": HTMLAttributes; - "ons-splitter-side": HTMLAttributes; - "ons-navigator": HTMLAttributes; - "ons-tabbar": HTMLAttributes; - "ons-tab": HTMLAttributes; - "ons-gesture-detector": HTMLAttributes; - "ons-bottom-toolbar": HTMLAttributes; - "ons-fab": HTMLAttributes; - "ons-carousel": HTMLAttributes; - "ons-carousel-item": HTMLAttributes; - } - } - - interface NativeStorage extends Storage { - getItem(key: string, def?: string): string; - } - - /** - * Native window properties for Android - */ - interface AndroidWindow { - /** - * This is an Android only window object - */ - readonly __sufile__: I; - /** - * This is an Android only window object - */ - readonly __suzip__: I; - /** - * This is an Android only window object - */ - readonly __environment__: I; - - /** - * This is an Android only window object - */ - readonly __shell__: I; - /** - * This is an Android only window object - */ - readonly __buildconfig__: I; - /** - * This is an Android only window object - */ - readonly __os__: I; - /** - * TODO - */ - readonly __view__: I; - readonly __log__: I; - readonly __properties__: I; - /** - * `localStorage` like object to make support better with `useLocalStorage`. - * - * - This interface is not configurable - */ - readonly __nativeStorage__: NativeStorage; - - readonly __terminal__: I; - readonly __chooser__: I; - readonly __download__: I; - } - - export type MMRLTheme = Theme & { - palette?: { - primary?: { - header?: string; - }; - menuoutline?: string; - text?: { - link?: string; - }; - }; - }; - - interface AndroidNavigator {} - - interface Navigator extends AndroidNavigator { - app: { - exitApp: () => void; - }; - } - - interface Window extends AndroidWindow { - localStorage: NativeStorage; - } - - const Toast: { - LENGTH_LONG: "long"; - LENGTH_SHORT: "short"; - }; - - const WEB_BUILD_DATE: number; - - const __webpack__mode__: "production" | "development"; - - export interface RepoConfig { - name: string; - website?: string; - support?: string; - donate?: string; - submission?: any; - base_url: string; - max_num?: number; - enable_log?: boolean; - log_dir?: string; - } - - export interface Repo { - name: string; - website: string; - support: string; - donate: string; - submission: any; - metadata: Metadata; - modules: Module[]; - } - - export interface Metadata { - version: number; - timestamp: number; - } - - export interface BaseModule { - id: string; - name: string; - version: string; - versionCode: number; - author: string; - description: string; - } - - export type ModuleNoteColors = "success" | "green" | "info" | "blue" | "warning" | "yellow" | "error" | "red"; - export interface ModuleNote { - title?: string; - message?: string; - color?: ModuleNoteColors; - } - - export type ModuleFeaturesList = - | "service" - | "post_fs_data" - | "resetprop" - | "sepolicy" - | "zygisk" - | "webroot" - | "post_mount" - | "boot_completed" - | "modconf" - | "apks"; - - export type ModuleFeatures = Partial>; - - export type RootSoluctions = Partial, "unknown">, string>>; - - export interface Module extends BaseModule { - updateJson?: string; - added: number; - timestamp?: number; - size?: number; - track: Track; - versions: Version[]; - - features: ModuleFeatures; - - minApi?: number; - maxApi?: number; - - license?: string; - homepage?: string; - support?: string; - donate?: string; - cover?: string; - icon?: string; - require?: string[]; - screenshots?: string[]; - category?: string; - categories?: string[]; - stars?: number; - readme?: string; - note?: ModuleNote; - root?: RootSoluctions; - - /** - * Non-user definable - */ - verified: boolean; - - /** - * Local modules only - */ - __mmrl__local__module__?: boolean; - __mmrl_repo_source?: string[]; - } - - export interface BaseTrack { - type: string; - added: number; - source: string; - } - - export interface Track extends BaseTrack { - verified: boolean; - antifeatures?: string | string[]; - - /** - * @deprecated - */ - license: string; - /** - * @deprecated - */ - homepage: string; - /** - * @deprecated - */ - support: string; - /** - * @deprecated - */ - donate: string; - /** - * @deprecated - */ - cover?: string; - /** - * @deprecated - */ - icon?: string; - /** - * @deprecated - */ - require?: string[]; - /** - * @deprecated - */ - screenshots?: string[]; - /** - * @deprecated - */ - category?: string; - /** - * @deprecated - */ - categories?: string[]; - /** - * @deprecated - */ - readme?: string; - - /** - * Not Supported - */ - stars?: number; - } - - export interface Version { - timestamp: number; - version: string; - versionCode: number; - zipUrl: string; - size?: number; - changelog: string; - } - - export interface UpdateJson { - version: string; - versionCode: number; - zipUrl: string; - changelog: string; - } - - export interface LicenseSPX { - isDeprecatedLicenseId: boolean; - isFsfLibre: boolean; - licenseText: string; - standardLicenseTemplate: string; - name: string; - licenseId: string; - crossRef: CrossRef[]; - seeAlso: string[]; - isOsiApproved: boolean; - licenseTextHtml: string; - } - - export interface CrossRef { - match: string; - url: string; - isValid: boolean; - isLive: boolean; - timestamp: string; - isWayBackLink: boolean; - order: number; - } - - // OnsenUI Types - /** - * @extends {Event} - */ - export interface DeviceBackButtonEvent extends Event { - /** - * Runs the handler for the immediate parent that supports device back button. - * @returns {void} - */ - callParentHandler: () => void; - } -} diff --git a/src/util/Constants.ts b/src/util/Constants.ts deleted file mode 100644 index 963727a3..00000000 --- a/src/util/Constants.ts +++ /dev/null @@ -1,6 +0,0 @@ -const Constants = { - UserAgentAndroid: "MMRL", - GlobalMMRLTitle: "Magisk Module Repo Loader", -} - -export default Constants; diff --git a/src/util/INCLUDE_CORE.ts b/src/util/INCLUDE_CORE.ts deleted file mode 100644 index b394582b..00000000 --- a/src/util/INCLUDE_CORE.ts +++ /dev/null @@ -1,77 +0,0 @@ -export const INCLUDE_CORE= `GREEN="\\x1b[32m" -RED="\\x1b[31m" -CYAN="\\x1b[96m" -YELLOW="\\x1b[93m" -UNDERLINE="\\x1b[4m" -RESET="\\x1b[0m" - -function ui_info { echo "$GREEN- $RESET$1"; } -function ui_error { echo "RED! $RESET$2"; exit $1; } -function ui_warn { echo "$YELLOW? $RESET$1"; } -function mmrl_exec { echo "#!mmrl:$@"; } - -echo "$GREEN __ _____ _______ __ $RESET" -echo "$GREEN / |/ / |/ / __ \\/ / $RESET" -echo "$GREEN / /|_/ / /|_/ / /_/ / / $RESET" -echo "$GREEN / / / / / / / _, _/ /___$RESET" -echo "$GREEN/_/ /_/_/ /_/_/ |_/_____/$RESET" -echo "" -ui_info "Using version $CYAN$MMRL_VER$RESET" - -ui_info "Initialize BusyBox" -bb() { - case "$ROOTMANAGER" in - "Magisk") - ui_info "Found$CYAN Magisk's$RESET BusyBox" - exec $MSUBSU $@ - ;; - "KernelSU") - ui_info "Found$CYAN KernelSU's$RESET BusyBox" - exec $KSUBSU $@ - ;; - "APatchSU") - ui_info "Found$CYAN APatch's$RESET BusyBox" - exec $ASUBSU $@ - ;; - "Unknown") - ui_error 1 "Unable to find BusyBox" - ;; - *) - ui_error 1 "BusyBox error" - ;; - esac -} - -ui_info "Initialize downloader" -download_file() { - bb wget $URL -O "$1" - if [ $(echo $?) -eq 0 ]; then - ui_info "Successful downloaded $GREEN$NAME$RESET" - else - ui_error 1 "Something went wrong" - fi -} - -ui_info "Initialize install CLI" -install_cli() { - case "$ROOTMANAGER" in - "Magisk") - ui_info "Found$CYAN Magisk's$RESET CLI" - exec $MSUCLI --install-module "$1" - ;; - "KernelSU") - ui_info "Found$CYAN KernelSU's$RESET CLI" - exec $KSUCLI module install "$1" - ;; - "APatchSU") - ui_info "Found$CYAN APatch's$RESET CLI" - exec $ASUCLI module install "$1" - ;; - "Unknown") - ui_error 1 "Unable to find root manager" - ;; - *) - ui_error 1 "Install error" - ;; - esac -}` \ No newline at end of file diff --git a/src/util/RouterUtil.js b/src/util/RouterUtil.js deleted file mode 100644 index 3cc94e7e..00000000 --- a/src/util/RouterUtil.js +++ /dev/null @@ -1,161 +0,0 @@ -/* - * routeStack : [userRoute, userRoute2, ...] - * processStack: [ - * { type: push | pop | reset, route: userRoute }, - * { type: push | pop | reset, route: userRoute2 }, - * ... - * ] - */ - -export const RouterUtil = { - init: (routes) => { - return { - routeStack: [...routes], - processStack: [], - }; - }, - - replace: ({ routeConfig, route, options, key, props, context, extra }) => { - const config = { ...routeConfig }; - const routeStack = [...config.routeStack]; - let processStack = [...config.processStack]; - - if (key == null || processStack.filter((el) => el.key === key).length === 0) { - const process = { - type: "replace", - route, - options, - key, - props, - context, - extra, - }; - processStack = [...processStack, process]; - } - - return { - routeStack, - processStack, - }; - }, - - reset: ({ routeConfig, route, options, key, props, context, extra }) => { - const config = { ...routeConfig }; - const routeStack = [...config.routeStack]; - let processStack = [...config.processStack]; - - if (key == null || processStack.filter((el) => el.key === key).length === 0) { - const process = { - type: "reset", - route, - options, - key, - props, - context, - extra, - }; - - processStack = [...processStack, process]; - } - - return { - routeStack, - processStack, - }; - }, - - push: ({ routeConfig, route, options, key, props, context, extra }) => { - const config = { ...routeConfig }; - const routeStack = [...config.routeStack]; - let processStack = [...config.processStack]; - - if (key == null || config.processStack.filter((el) => el.key === key).length === 0) { - const process = { - type: "push", - route, - options, - key, - props, - context, - extra, - }; - - processStack = [...processStack, process]; - } - - return { - routeStack, - processStack, - }; - }, - - pop: ({ routeConfig, options, key }) => { - const config = { ...routeConfig }; - const routeStack = [...config.routeStack]; - let processStack = [...config.processStack]; - /** - * Safegaurd to ensure that not - * too many pages are popped from - * the stack. - */ - const pops = processStack.filter((x) => x.type === "pop").length; - - if (pops + 1 >= routeStack.length) { - console.warn("Page stack is already empty"); - return config; - } - - const process = { - type: "pop", - key, - options, - }; - - processStack = [...processStack, process]; - - return { - routeStack, - processStack, - }; - }, - - postPush: (routeConfig) => { - const config = { ...routeConfig }; - let routeStack = [...config.routeStack]; - const processStack = [...config.processStack]; - - const next = processStack.shift(); - const type = next.type; - let route = next.route; - - if (type === "push") { - if (route !== null) { - routeStack = [...routeStack, route]; - } - } else if (type === "reset") { - if (!Array.isArray(route)) route = [route]; - routeStack = route; - } else if (type === "replace") { - routeStack.pop(); - routeStack.push(route); - } - - return { - routeStack, - processStack, - }; - }, - - postPop: (routeConfig) => { - const config = { ...routeConfig }; - let routeStack = [...config.routeStack]; - let processStack = [...config.processStack]; - routeStack = routeStack.slice(0, routeStack.length - 1); - processStack = processStack.slice(1); - - return { - routeStack, - processStack, - }; - }, -}; diff --git a/src/util/configure-sample.ts b/src/util/configure-sample.ts deleted file mode 100644 index e31b8b48..00000000 --- a/src/util/configure-sample.ts +++ /dev/null @@ -1,31 +0,0 @@ -export const configureSample = `import React from "react"; -import { Page } from "@mmrl/ui"; -import { StringsProvider } from "@mmrl/providers"; -import { useStrings } from "@mmrl/hooks"; - -const strs = { - en: { - hello: "Hello" - }, - de: { - hello: "Hallo" - } -} - - -const Config = () => { - const { strings } = useStrings(); - return ( - {strings("hello")} - ) -} - -const Main = () => { - return ( - - - - ) -} - -export default Main`; diff --git a/src/util/createRegexURL.ts b/src/util/createRegexURL.ts deleted file mode 100644 index 4b323b14..00000000 --- a/src/util/createRegexURL.ts +++ /dev/null @@ -1,7 +0,0 @@ -function createRegexURL(domain: string | string[], tld: string | string[]): RegExp { - const parseDomain = Array.isArray(domain) ? domain.join("|") : domain; - const parseTld = Array.isArray(tld) ? tld.join("|") : tld; - return new RegExp(`(?:http(s)?:\\/\\/)?(www\\.)?(${parseDomain})\\.(${parseTld})(\\/[\\w-]+)?`, "i"); -} - -export { createRegexURL }; diff --git a/src/util/dapi-sample.ts b/src/util/dapi-sample.ts deleted file mode 100644 index 2b116ce7..00000000 --- a/src/util/dapi-sample.ts +++ /dev/null @@ -1,314 +0,0 @@ -export const dapiSample = `# Markdown: Syntax - -## Required modules - -- Node.js - -* [Overview](#overview) - * [Philosophy](#philosophy) - * [Inline HTML](#html) - * [Automatic Escaping for Special Characters](#autoescape) -* [Block Elements](#block) - * [Paragraphs and Line Breaks](#p) - * [Headers](#header) - * [Blockquotes](#blockquote) - * [Lists](#list) - * [Code Blocks](#precode) - * [Horizontal Rules](#hr) -* [Span Elements](#span) - * [Links](#link) - * [Emphasis](#em) - * [Code](#code) - * [Images](#img) -* [Miscellaneous](#misc) - * [Backslash Escapes](#backslash) - * [Automatic Links](#autolink) - - -**Note:** This document is itself written using Markdown; you -can [see the source for it by adding '.text' to the URL](/projects/markdown/syntax.text). - ----- - -## Overview - -### Philosophy - -Markdown is intended to be as easy-to-read and easy-to-write as is feasible. - -Readability, however, is emphasized above all else. A Markdown-formatted -document should be publishable as-is, as plain text, without looking -like it's been marked up with tags or formatting instructions. While -Markdown's syntax has been influenced by several existing text-to-HTML -filters -- including [Setext](http://docutils.sourceforge.net/mirror/setext.html), [atx](http://www.aaronsw.com/2002/atx/), [Textile](http://textism.com/tools/textile/), [reStructuredText](http://docutils.sourceforge.net/rst.html), -[Grutatext](http://www.triptico.com/software/grutatxt.html), and [EtText](http://ettext.taint.org/doc/) -- the single biggest source of -inspiration for Markdown's syntax is the format of plain text email. - -## Block Elements - -### Paragraphs and Line Breaks - -A paragraph is simply one or more consecutive lines of text, separated -by one or more blank lines. (A blank line is any line that looks like a -blank line -- a line containing nothing but spaces or tabs is considered -blank.) Normal paragraphs should not be indented with spaces or tabs. - -The implication of the "one or more consecutive lines of text" rule is -that Markdown supports "hard-wrapped" text paragraphs. This differs -significantly from most other text-to-HTML formatters (including Movable -Type's "Convert Line Breaks" option) which translate every line break -character in a paragraph into a \`
\` tag. - -When you *do* want to insert a \`
\` break tag using Markdown, you -end a line with two or more spaces, then type return. - -### Headers - -Markdown supports two styles of headers, [Setext] [1] and [atx] [2]. - -Optionally, you may "close" atx-style headers. This is purely -cosmetic -- you can use this if you think it looks better. The -closing hashes don't even need to match the number of hashes -used to open the header. (The number of opening hashes -determines the header level.) - - -### Blockquotes - -Markdown uses email-style \`>\` characters for blockquoting. If you're -familiar with quoting passages of text in an email message, then you -know how to create a blockquote in Markdown. It looks best if you hard -wrap the text and put a \`>\` before every line: - -> This is a blockquote with two paragraphs. Lorem ipsum dolor sit amet, -> consectetuer adipiscing elit. Aliquam hendrerit mi posuere lectus. -> Vestibulum enim wisi, viverra nec, fringilla in, laoreet vitae, risus. -> -> Donec sit amet nisl. Aliquam semper ipsum sit amet velit. Suspendisse -> id sem consectetuer libero luctus adipiscing. - -Markdown allows you to be lazy and only put the \`>\` before the first -line of a hard-wrapped paragraph: - -> This is a blockquote with two paragraphs. Lorem ipsum dolor sit amet, -consectetuer adipiscing elit. Aliquam hendrerit mi posuere lectus. -Vestibulum enim wisi, viverra nec, fringilla in, laoreet vitae, risus. - -> Donec sit amet nisl. Aliquam semper ipsum sit amet velit. Suspendisse -id sem consectetuer libero luctus adipiscing. - -Blockquotes can be nested (i.e. a blockquote-in-a-blockquote) by -adding additional levels of \`>\`: - -> This is the first level of quoting. -> -> > This is nested blockquote. -> -> Back to the first level. - -Blockquotes can contain other Markdown elements, including headers, lists, -and code blocks: - -> ## This is a header. -> -> 1. This is the first list item. -> 2. This is the second list item. -> -> Here's some example code: -> -> return shell_exec("echo $input | $markdown_script"); - -Any decent text editor should make email-style quoting easy. For -example, with BBEdit, you can make a selection and choose Increase -Quote Level from the Text menu. - - -### Lists - -Markdown supports ordered (numbered) and unordered (bulleted) lists. - -Unordered lists use asterisks, pluses, and hyphens -- interchangably --- as list markers: - -* Red -* Green -* Blue - -is equivalent to: - -+ Red -+ Green -+ Blue - -and: - -- Red -- Green -- Blue - -Ordered lists use numbers followed by periods: - -1. Bird -2. McHale -3. Parish - -It's important to note that the actual numbers you use to mark the -list have no effect on the HTML output Markdown produces. The HTML -Markdown produces from the above list is: - -If you instead wrote the list in Markdown like this: - -1. Bird -1. McHale -1. Parish - -or even: - -3. Bird -1. McHale -8. Parish - -you'd get the exact same HTML output. The point is, if you want to, -you can use ordinal numbers in your ordered Markdown lists, so that -the numbers in your source match the numbers in your published HTML. -But if you want to be lazy, you don't have to. - -To make lists look nice, you can wrap items with hanging indents: - -* Lorem ipsum dolor sit amet, consectetuer adipiscing elit. - Aliquam hendrerit mi posuere lectus. Vestibulum enim wisi, - viverra nec, fringilla in, laoreet vitae, risus. -* Donec sit amet nisl. Aliquam semper ipsum sit amet velit. - Suspendisse id sem consectetuer libero luctus adipiscing. - -But if you want to be lazy, you don't have to: - -* Lorem ipsum dolor sit amet, consectetuer adipiscing elit. -Aliquam hendrerit mi posuere lectus. Vestibulum enim wisi, -viverra nec, fringilla in, laoreet vitae, risus. -* Donec sit amet nisl. Aliquam semper ipsum sit amet velit. -Suspendisse id sem consectetuer libero luctus adipiscing. - -List items may consist of multiple paragraphs. Each subsequent -paragraph in a list item must be indented by either 4 spaces -or one tab: - -1. This is a list item with two paragraphs. Lorem ipsum dolor - sit amet, consectetuer adipiscing elit. Aliquam hendrerit - mi posuere lectus. - - Vestibulum enim wisi, viverra nec, fringilla in, laoreet - vitae, risus. Donec sit amet nisl. Aliquam semper ipsum - sit amet velit. - -2. Suspendisse id sem consectetuer libero luctus adipiscing. - -It looks nice if you indent every line of the subsequent -paragraphs, but here again, Markdown will allow you to be -lazy: - -* This is a list item with two paragraphs. - - This is the second paragraph in the list item. You're -only required to indent the first line. Lorem ipsum dolor -sit amet, consectetuer adipiscing elit. - -* Another item in the same list. - -To put a blockquote within a list item, the blockquote's \`>\` -delimiters need to be indented: - -* A list item with a blockquote: - - > This is a blockquote - > inside a list item. - -To put a code block within a list item, the code block needs -to be indented *twice* -- 8 spaces or two tabs: - -* A list item with a code block: - - - -### Code Blocks - -Pre-formatted code blocks are used for writing about programming or -markup source code. Rather than forming normal paragraphs, the lines -of a code block are interpreted literally. Markdown wraps a code block -in both \`

\` and \`\` tags.
-
-To produce a code block in Markdown, simply indent every line of the
-block by at least 4 spaces or 1 tab.
-
-This is a normal paragraph:
-
-    This is a code block.
-
-Here is an example of AppleScript:
-
-    tell application "Foo"
-        beep
-    end tell
-
-A code block continues until it reaches a line that is not indented
-(or the end of the article).
-
-Within a code block, ampersands (\`&\`) and angle brackets (\`<\` and \`>\`)
-are automatically converted into HTML entities. This makes it very
-easy to include example HTML source code using Markdown -- just paste
-it and indent it, and Markdown will handle the hassle of encoding the
-ampersands and angle brackets. For example, this:
-
-    
-
-Regular Markdown syntax is not processed within code blocks. E.g.,
-asterisks are just literal asterisks within a code block. This means
-it's also easy to use Markdown to write about Markdown's own syntax.
-
-\`\`\`
-tell application "Foo"
-    beep
-end tell
-\`\`\`
-
-## Span Elements
-
-### Links
-
-Markdown supports two style of links: *inline* and *reference*.
-
-In both styles, the link text is delimited by [square brackets].
-
-To create an inline link, use a set of regular parentheses immediately
-after the link text's closing square bracket. Inside the parentheses,
-put the URL where you want the link to point, along with an *optional*
-title for the link, surrounded in quotes. For example:
-
-This is [an example](http://example.com/) inline link.
-
-[This link](http://example.net/) has no title attribute.
-
-### Emphasis
-
-Markdown treats asterisks (\`*\`) and underscores (\`_\`) as indicators of
-emphasis. Text wrapped with one \`*\` or \`_\` will be wrapped with an
-HTML \`\` tag; double \`*\`'s or \`_\`'s will be wrapped with an HTML
-\`\` tag. E.g., this input:
-
-*single asterisks*
-
-_single underscores_
-
-**double asterisks**
-
-__double underscores__
-
-### Code
-
-To indicate a span of code, wrap it with backtick quotes (\`\` \` \`\`).
-Unlike a pre-formatted code block, a code span indicates code within a
-normal paragraph. For example:
-
-Use the \`printf()\` function.`
\ No newline at end of file
diff --git a/src/util/editorTheme.ts b/src/util/editorTheme.ts
deleted file mode 100644
index 4446bccf..00000000
--- a/src/util/editorTheme.ts
+++ /dev/null
@@ -1,475 +0,0 @@
-import * as monacoEditor from "monaco-editor/esm/vs/editor/editor.api";
-
-const editorTheme: monacoEditor.editor.IStandaloneThemeData = {
-  inherit: true,
-  base: "vs-dark",
-  colors: {
-    focusBorder: "#005cc5",
-    foreground: "#d1d5da",
-    descriptionForeground: "#959da5",
-    errorForeground: "#f97583",
-    "textLink.foreground": "#79b8ff",
-    "textLink.activeForeground": "#c8e1ff",
-    "textBlockQuote.background": "#24292e",
-    "textBlockQuote.border": "#444d56",
-    "textCodeBlock.background": "#2f363d",
-    "textPreformat.foreground": "#d1d5da",
-    "textSeparator.foreground": "#586069",
-    "button.background": "#176f2c",
-    "button.foreground": "#dcffe4",
-    "button.hoverBackground": "#22863a",
-    "checkbox.background": "#444d56",
-    "checkbox.border": "#1b1f23",
-    "dropdown.background": "#2f363d",
-    "dropdown.border": "#1b1f23",
-    "dropdown.foreground": "#e1e4e8",
-    "dropdown.listBackground": "#24292e",
-    "input.background": "#2f363d",
-    "input.border": "#1b1f23",
-    "input.foreground": "#e1e4e8",
-    "input.placeholderForeground": "#959da5",
-    "badge.foreground": "#c8e1ff",
-    "badge.background": "#044289",
-    "progressBar.background": "#0366d6",
-    "titleBar.activeForeground": "#e1e4e8",
-    "titleBar.activeBackground": "#24292e",
-    "titleBar.inactiveForeground": "#959da5",
-    "titleBar.inactiveBackground": "#1f2428",
-    "titleBar.border": "#1b1f23",
-    "activityBar.foreground": "#e1e4e8",
-    "activityBar.inactiveForeground": "#6a737d",
-    "activityBar.background": "#24292e",
-    "activityBarBadge.foreground": "#fff",
-    "activityBarBadge.background": "#0366d6",
-    "activityBar.activeBorder": "#f9826c",
-    "activityBar.border": "#1b1f23",
-    "sideBar.foreground": "#d1d5da",
-    "sideBar.background": "#1f2428",
-    "sideBar.border": "#1b1f23",
-    "sideBarTitle.foreground": "#e1e4e8",
-    "sideBarSectionHeader.foreground": "#e1e4e8",
-    "sideBarSectionHeader.background": "#1f2428",
-    "sideBarSectionHeader.border": "#1b1f23",
-    "list.hoverForeground": "#e1e4e8",
-    "list.inactiveSelectionForeground": "#e1e4e8",
-    "list.activeSelectionForeground": "#e1e4e8",
-    "list.hoverBackground": "#282e34",
-    "list.inactiveSelectionBackground": "#282e34",
-    "list.activeSelectionBackground": "#39414a",
-    "list.inactiveFocusBackground": "#1d2d3e",
-    "list.focusBackground": "#044289",
-    "tree.indentGuidesStroke": "#2f363d",
-    "notificationCenterHeader.foreground": "#959da5",
-    "notificationCenterHeader.background": "#24292e",
-    "notifications.foreground": "#e1e4e8",
-    "notifications.background": "#2f363d",
-    "notifications.border": "#1b1f23",
-    "notificationsErrorIcon.foreground": "#ea4a5a",
-    "notificationsWarningIcon.foreground": "#ffab70",
-    "notificationsInfoIcon.foreground": "#79b8ff",
-    "pickerGroup.border": "#444d56",
-    "pickerGroup.foreground": "#e1e4e8",
-    "quickInput.background": "#24292e",
-    "quickInput.foreground": "#e1e4e8",
-    "statusBar.foreground": "#d1d5da",
-    "statusBar.background": "#24292e",
-    "statusBar.border": "#1b1f23",
-    "statusBar.noFolderBackground": "#24292e",
-    "statusBar.debuggingBackground": "#931c06",
-    "statusBar.debuggingForeground": "#fff",
-    "editorGroupHeader.tabsBackground": "#1f2428",
-    "editorGroupHeader.tabsBorder": "#1b1f23",
-    "editorGroup.border": "#1b1f23",
-    "tab.activeForeground": "#e1e4e8",
-    "tab.inactiveForeground": "#959da5",
-    "tab.inactiveBackground": "#1f2428",
-    "tab.activeBackground": "#24292e",
-    "tab.hoverBackground": "#24292e",
-    "tab.unfocusedHoverBackground": "#24292e",
-    "tab.border": "#1b1f23",
-    "tab.unfocusedActiveBorderTop": "#1b1f23",
-    "tab.activeBorder": "#24292e",
-    "tab.unfocusedActiveBorder": "#24292e",
-    "tab.activeBorderTop": "#f9826c",
-    "breadcrumb.foreground": "#959da5",
-    "breadcrumb.focusForeground": "#e1e4e8",
-    "breadcrumb.activeSelectionForeground": "#d1d5da",
-    "breadcrumbPicker.background": "#2b3036",
-    "editor.foreground": "#e1e4e8",
-    "editor.background": "#24292e",
-    "editorWidget.background": "#1f2428",
-    "editor.foldBackground": "#282e33",
-    "editor.lineHighlightBackground": "#2b3036",
-    "editorLineNumber.foreground": "#444d56",
-    "editorLineNumber.activeForeground": "#e1e4e8",
-    "editorIndentGuide.background": "#2f363d",
-    "editorIndentGuide.activeBackground": "#444d56",
-    "editorWhitespace.foreground": "#444d56",
-    "editorCursor.foreground": "#c8e1ff",
-    "editor.findMatchBackground": "#ffd33d44",
-    "editor.findMatchHighlightBackground": "#ffd33d22",
-    "editor.inactiveSelectionBackground": "#3392FF22",
-    "editor.selectionBackground": "#3392FF44",
-    "editor.selectionHighlightBackground": "#17E5E633",
-    "editor.selectionHighlightBorder": "#17E5E600",
-    "editor.wordHighlightBackground": "#17E5E600",
-    "editor.wordHighlightStrongBackground": "#17E5E600",
-    "editor.wordHighlightBorder": "#17E5E699",
-    "editor.wordHighlightStrongBorder": "#17E5E666",
-    "editorBracketMatch.background": "#17E5E650",
-    "editorBracketMatch.border": "#17E5E600",
-    "editorGutter.modifiedBackground": "#2188ff",
-    "editorGutter.addedBackground": "#28a745",
-    "editorGutter.deletedBackground": "#ea4a5a",
-    "diffEditor.insertedTextBackground": "#28a74530",
-    "diffEditor.removedTextBackground": "#d73a4930",
-    "scrollbar.shadow": "#0008",
-    "scrollbarSlider.background": "#6a737d33",
-    "scrollbarSlider.hoverBackground": "#6a737d44",
-    "scrollbarSlider.activeBackground": "#6a737d88",
-    "editorOverviewRuler.border": "#1b1f23",
-    "panel.background": "#1f2428",
-    "panel.border": "#1b1f23",
-    "panelTitle.activeBorder": "#f9826c",
-    "panelTitle.activeForeground": "#e1e4e8",
-    "panelTitle.inactiveForeground": "#959da5",
-    "panelInput.border": "#2f363d",
-    "terminal.foreground": "#d1d5da",
-    "gitDecoration.addedResourceForeground": "#34d058",
-    "gitDecoration.modifiedResourceForeground": "#79b8ff",
-    "gitDecoration.deletedResourceForeground": "#ea4a5a",
-    "gitDecoration.untrackedResourceForeground": "#34d058",
-    "gitDecoration.ignoredResourceForeground": "#6a737d",
-    "gitDecoration.conflictingResourceForeground": "#ffab70",
-    "gitDecoration.submoduleResourceForeground": "#6a737d",
-    "debugToolBar.background": "#2b3036",
-    "editor.stackFrameHighlightBackground": "#a707",
-    "editor.focusedStackFrameHighlightBackground": "#b808",
-    "peekViewEditor.matchHighlightBackground": "#ffd33d33",
-    "peekViewResult.matchHighlightBackground": "#ffd33d33",
-    "peekViewEditor.background": "#1f242888",
-    "peekViewResult.background": "#1f2428",
-    "settings.headerForeground": "#e1e4e8",
-    "settings.modifiedItemIndicator": "#0366d6",
-    "welcomePage.buttonBackground": "#2f363d",
-    "welcomePage.buttonHoverBackground": "#444d56",
-  },
-  rules: [
-    {
-      foreground: "#6a737d",
-      token: "comment",
-    },
-    {
-      foreground: "#6a737d",
-      token: "punctuation.definition.comment",
-    },
-    {
-      foreground: "#6a737d",
-      token: "string.comment",
-    },
-    {
-      foreground: "#79b8ff",
-      token: "constant",
-    },
-    {
-      foreground: "#79b8ff",
-      token: "entity.name.constant",
-    },
-    {
-      foreground: "#79b8ff",
-      token: "variable.other.constant",
-    },
-    {
-      foreground: "#79b8ff",
-      token: "variable.language",
-    },
-    {
-      foreground: "#b392f0",
-      token: "entity",
-    },
-    {
-      foreground: "#b392f0",
-      token: "entity.name",
-    },
-    {
-      foreground: "#e1e4e8",
-      token: "variable.parameter.function",
-    },
-    {
-      foreground: "#85e89d",
-      token: "entity.name.tag",
-    },
-    {
-      foreground: "#f97583",
-      token: "keyword",
-    },
-    {
-      foreground: "#f97583",
-      token: "storage",
-    },
-    {
-      foreground: "#f97583",
-      token: "storage.type",
-    },
-    {
-      foreground: "#e1e4e8",
-      token: "storage.modifier.package",
-    },
-    {
-      foreground: "#e1e4e8",
-      token: "storage.modifier.import",
-    },
-    {
-      foreground: "#e1e4e8",
-      token: "storage.type.java",
-    },
-    {
-      foreground: "#9ecbff",
-      token: "string",
-    },
-    {
-      foreground: "#9ecbff",
-      token: "punctuation.definition.string",
-    },
-    {
-      foreground: "#9ecbff",
-      token: "string punctuation.section.embedded source",
-    },
-    {
-      foreground: "#79b8ff",
-      token: "support",
-    },
-    {
-      foreground: "#79b8ff",
-      token: "meta.property-name",
-    },
-    {
-      foreground: "#ffab70",
-      token: "variable",
-    },
-    {
-      foreground: "#e1e4e8",
-      token: "variable.other",
-    },
-    {
-      fontStyle: "italic",
-      foreground: "#fdaeb7",
-      token: "invalid.broken",
-    },
-    {
-      fontStyle: "italic",
-      foreground: "#fdaeb7",
-      token: "invalid.deprecated",
-    },
-    {
-      fontStyle: "italic",
-      foreground: "#fdaeb7",
-      token: "invalid.illegal",
-    },
-    {
-      fontStyle: "italic",
-      foreground: "#fdaeb7",
-      token: "invalid.unimplemented",
-    },
-    {
-      fontStyle: "italic underline",
-      background: "#f97583",
-      foreground: "#24292e",
-      token: "carriage-return",
-    },
-    {
-      foreground: "#fdaeb7",
-      token: "message.error",
-    },
-    {
-      foreground: "#e1e4e8",
-      token: "string source",
-    },
-    {
-      foreground: "#79b8ff",
-      token: "string variable",
-    },
-    {
-      foreground: "#dbedff",
-      token: "source.regexp",
-    },
-    {
-      foreground: "#dbedff",
-      token: "string.regexp",
-    },
-    {
-      foreground: "#dbedff",
-      token: "string.regexp.character-class",
-    },
-    {
-      foreground: "#dbedff",
-      token: "string.regexp constant.character.escape",
-    },
-    {
-      foreground: "#dbedff",
-      token: "string.regexp source.ruby.embedded",
-    },
-    {
-      foreground: "#dbedff",
-      token: "string.regexp string.regexp.arbitrary-repitition",
-    },
-    {
-      fontStyle: "bold",
-      foreground: "#85e89d",
-      token: "string.regexp constant.character.escape",
-    },
-    {
-      foreground: "#79b8ff",
-      token: "support.constant",
-    },
-    {
-      foreground: "#79b8ff",
-      token: "support.variable",
-    },
-    {
-      foreground: "#79b8ff",
-      token: "meta.module-reference",
-    },
-    {
-      foreground: "#ffab70",
-      token: "punctuation.definition.list.begin.markdown",
-    },
-    {
-      fontStyle: "bold",
-      foreground: "#79b8ff",
-      token: "markup.heading",
-    },
-    {
-      fontStyle: "bold",
-      foreground: "#79b8ff",
-      token: "markup.heading entity.name",
-    },
-    {
-      foreground: "#85e89d",
-      token: "markup.quote",
-    },
-    {
-      fontStyle: "italic",
-      foreground: "#e1e4e8",
-      token: "markup.italic",
-    },
-    {
-      fontStyle: "bold",
-      foreground: "#e1e4e8",
-      token: "markup.bold",
-    },
-    {
-      foreground: "#79b8ff",
-      token: "markup.raw",
-    },
-    {
-      background: "#86181d",
-      foreground: "#fdaeb7",
-      token: "markup.deleted",
-    },
-    {
-      background: "#86181d",
-      foreground: "#fdaeb7",
-      token: "meta.diff.header.from-file",
-    },
-    {
-      background: "#86181d",
-      foreground: "#fdaeb7",
-      token: "punctuation.definition.deleted",
-    },
-    {
-      background: "#144620",
-      foreground: "#85e89d",
-      token: "markup.inserted",
-    },
-    {
-      background: "#144620",
-      foreground: "#85e89d",
-      token: "meta.diff.header.to-file",
-    },
-    {
-      background: "#144620",
-      foreground: "#85e89d",
-      token: "punctuation.definition.inserted",
-    },
-    {
-      background: "#c24e00",
-      foreground: "#ffab70",
-      token: "markup.changed",
-    },
-    {
-      background: "#c24e00",
-      foreground: "#ffab70",
-      token: "punctuation.definition.changed",
-    },
-    {
-      foreground: "#2f363d",
-      background: "#79b8ff",
-      token: "markup.ignored",
-    },
-    {
-      foreground: "#2f363d",
-      background: "#79b8ff",
-      token: "markup.untracked",
-    },
-    {
-      foreground: "#b392f0",
-      fontStyle: "bold",
-      token: "meta.diff.range",
-    },
-    {
-      foreground: "#79b8ff",
-      token: "meta.diff.header",
-    },
-    {
-      fontStyle: "bold",
-      foreground: "#79b8ff",
-      token: "meta.separator",
-    },
-    {
-      foreground: "#79b8ff",
-      token: "meta.output",
-    },
-    {
-      foreground: "#d1d5da",
-      token: "brackethighlighter.tag",
-    },
-    {
-      foreground: "#d1d5da",
-      token: "brackethighlighter.curly",
-    },
-    {
-      foreground: "#d1d5da",
-      token: "brackethighlighter.round",
-    },
-    {
-      foreground: "#d1d5da",
-      token: "brackethighlighter.square",
-    },
-    {
-      foreground: "#d1d5da",
-      token: "brackethighlighter.angle",
-    },
-    {
-      foreground: "#d1d5da",
-      token: "brackethighlighter.quote",
-    },
-    {
-      foreground: "#fdaeb7",
-      token: "brackethighlighter.unmatched",
-    },
-    {
-      foreground: "#dbedff",
-      fontStyle: "underline",
-      token: "constant.other.reference.link",
-    },
-    {
-      foreground: "#dbedff",
-      fontStyle: "underline",
-      token: "string.other.link",
-    },
-  ],
-  encodedTokensColors: [],
-};
-
-export default editorTheme;
diff --git a/src/util/extname.ts b/src/util/extname.ts
deleted file mode 100644
index 4de14500..00000000
--- a/src/util/extname.ts
+++ /dev/null
@@ -1,58 +0,0 @@
-const CHAR_FORWARD_SLASH = 47; /* / */
-const CHAR_DOT = 46; /* . */
-
-export function extname(path: string) {
-  if (typeof path !== "string") {
-    throw new TypeError(`The "path" argument must be of type string. Received type ${typeof path}`);
-  }
-
-  let startDot = -1;
-  let startPart = 0;
-  let end = -1;
-  let matchedSlash = true;
-  // Track the state of characters (if any) we see before our first dot and
-  // after any path separator we find
-  let preDotState = 0;
-  for (let i = path.length - 1; i >= 0; --i) {
-    let code = path.charCodeAt(i);
-    if (code === CHAR_FORWARD_SLASH) {
-      // If we reached a path separator that was not part of a set of path
-      // separators at the end of the string, stop now
-      if (!matchedSlash) {
-        startPart = i + 1;
-        break;
-      }
-      continue;
-    }
-    if (end === -1) {
-      // We saw the first non-path separator, mark this as the end of our
-      // extension
-      matchedSlash = false;
-      end = i + 1;
-    }
-    if (code === CHAR_DOT) {
-      // If this is our first dot, mark it as the start of our extension
-      if (startDot === -1) {
-        startDot = i;
-      } else if (preDotState !== 1) {
-        preDotState = 1;
-      }
-    } else if (startDot !== -1) {
-      // We saw a non-dot and non-path separator before our dot, so we should
-      // have a good chance at having a non-empty extension
-      preDotState = -1;
-    }
-  }
-
-  if (
-    startDot === -1 ||
-    end === -1 ||
-    // We saw a non-dot character immediately before the dot
-    preDotState === 0 ||
-    // The (right-most) trimmed path component is exactly '..'
-    (preDotState === 1 && startDot === end - 1 && startDot === startPart + 1)
-  ) {
-    return "";
-  }
-  return path.slice(startDot, end);
-}
diff --git a/src/util/licenseTypes.ts b/src/util/licenseTypes.ts
deleted file mode 100644
index eac68516..00000000
--- a/src/util/licenseTypes.ts
+++ /dev/null
@@ -1,640 +0,0 @@
-export const licenseTypes = [
-  "0BSD",
-  "AAL",
-  "Abstyles",
-  "AdaCore-doc",
-  "Adobe-2006",
-  "Adobe-Display-PostScript",
-  "Adobe-Glyph",
-  "Adobe-Utopia",
-  "ADSL",
-  "AFL-1.1",
-  "AFL-1.2",
-  "AFL-2.0",
-  "AFL-2.1",
-  "AFL-3.0",
-  "Afmparse",
-  "AGPL-1.0",
-  "AGPL-1.0-only",
-  "AGPL-1.0-or-later",
-  "AGPL-3.0",
-  "AGPL-3.0-only",
-  "AGPL-3.0-or-later",
-  "Aladdin",
-  "AMDPLPA",
-  "AML",
-  "AML-glslang",
-  "AMPAS",
-  "ANTLR-PD",
-  "ANTLR-PD-fallback",
-  "Apache-1.0",
-  "Apache-1.1",
-  "Apache-2.0",
-  "APAFML",
-  "APL-1.0",
-  "App-s2p",
-  "APSL-1.0",
-  "APSL-1.1",
-  "APSL-1.2",
-  "APSL-2.0",
-  "Arphic-1999",
-  "Artistic-1.0",
-  "Artistic-1.0-cl8",
-  "Artistic-1.0-Perl",
-  "Artistic-2.0",
-  "ASWF-Digital-Assets-1.0",
-  "ASWF-Digital-Assets-1.1",
-  "Baekmuk",
-  "Bahyph",
-  "Barr",
-  "bcrypt-Solar-Designer",
-  "Beerware",
-  "Bitstream-Charter",
-  "Bitstream-Vera",
-  "BitTorrent-1.0",
-  "BitTorrent-1.1",
-  "blessing",
-  "BlueOak-1.0.0",
-  "Boehm-GC",
-  "Borceux",
-  "Brian-Gladman-2-Clause",
-  "Brian-Gladman-3-Clause",
-  "BSD-1-Clause",
-  "BSD-2-Clause",
-  "BSD-2-Clause-Darwin",
-  "BSD-2-Clause-FreeBSD",
-  "BSD-2-Clause-NetBSD",
-  "BSD-2-Clause-Patent",
-  "BSD-2-Clause-Views",
-  "BSD-3-Clause",
-  "BSD-3-Clause-acpica",
-  "BSD-3-Clause-Attribution",
-  "BSD-3-Clause-Clear",
-  "BSD-3-Clause-flex",
-  "BSD-3-Clause-HP",
-  "BSD-3-Clause-LBNL",
-  "BSD-3-Clause-Modification",
-  "BSD-3-Clause-No-Military-License",
-  "BSD-3-Clause-No-Nuclear-License",
-  "BSD-3-Clause-No-Nuclear-License-2014",
-  "BSD-3-Clause-No-Nuclear-Warranty",
-  "BSD-3-Clause-Open-MPI",
-  "BSD-3-Clause-Sun",
-  "BSD-4-Clause",
-  "BSD-4-Clause-Shortened",
-  "BSD-4-Clause-UC",
-  "BSD-4.3RENO",
-  "BSD-4.3TAHOE",
-  "BSD-Advertising-Acknowledgement",
-  "BSD-Attribution-HPND-disclaimer",
-  "BSD-Inferno-Nettverk",
-  "BSD-Protection",
-  "BSD-Source-beginning-file",
-  "BSD-Source-Code",
-  "BSD-Systemics",
-  "BSD-Systemics-W3Works",
-  "BSL-1.0",
-  "BUSL-1.1",
-  "bzip2-1.0.5",
-  "bzip2-1.0.6",
-  "C-UDA-1.0",
-  "CAL-1.0",
-  "CAL-1.0-Combined-Work-Exception",
-  "Caldera",
-  "Caldera-no-preamble",
-  "CATOSL-1.1",
-  "CC-BY-1.0",
-  "CC-BY-2.0",
-  "CC-BY-2.5",
-  "CC-BY-2.5-AU",
-  "CC-BY-3.0",
-  "CC-BY-3.0-AT",
-  "CC-BY-3.0-AU",
-  "CC-BY-3.0-DE",
-  "CC-BY-3.0-IGO",
-  "CC-BY-3.0-NL",
-  "CC-BY-3.0-US",
-  "CC-BY-4.0",
-  "CC-BY-NC-1.0",
-  "CC-BY-NC-2.0",
-  "CC-BY-NC-2.5",
-  "CC-BY-NC-3.0",
-  "CC-BY-NC-3.0-DE",
-  "CC-BY-NC-4.0",
-  "CC-BY-NC-ND-1.0",
-  "CC-BY-NC-ND-2.0",
-  "CC-BY-NC-ND-2.5",
-  "CC-BY-NC-ND-3.0",
-  "CC-BY-NC-ND-3.0-DE",
-  "CC-BY-NC-ND-3.0-IGO",
-  "CC-BY-NC-ND-4.0",
-  "CC-BY-NC-SA-1.0",
-  "CC-BY-NC-SA-2.0",
-  "CC-BY-NC-SA-2.0-DE",
-  "CC-BY-NC-SA-2.0-FR",
-  "CC-BY-NC-SA-2.0-UK",
-  "CC-BY-NC-SA-2.5",
-  "CC-BY-NC-SA-3.0",
-  "CC-BY-NC-SA-3.0-DE",
-  "CC-BY-NC-SA-3.0-IGO",
-  "CC-BY-NC-SA-4.0",
-  "CC-BY-ND-1.0",
-  "CC-BY-ND-2.0",
-  "CC-BY-ND-2.5",
-  "CC-BY-ND-3.0",
-  "CC-BY-ND-3.0-DE",
-  "CC-BY-ND-4.0",
-  "CC-BY-SA-1.0",
-  "CC-BY-SA-2.0",
-  "CC-BY-SA-2.0-UK",
-  "CC-BY-SA-2.1-JP",
-  "CC-BY-SA-2.5",
-  "CC-BY-SA-3.0",
-  "CC-BY-SA-3.0-AT",
-  "CC-BY-SA-3.0-DE",
-  "CC-BY-SA-3.0-IGO",
-  "CC-BY-SA-4.0",
-  "CC-PDDC",
-  "CC0-1.0",
-  "CDDL-1.0",
-  "CDDL-1.1",
-  "CDL-1.0",
-  "CDLA-Permissive-1.0",
-  "CDLA-Permissive-2.0",
-  "CDLA-Sharing-1.0",
-  "CECILL-1.0",
-  "CECILL-1.1",
-  "CECILL-2.0",
-  "CECILL-2.1",
-  "CECILL-B",
-  "CECILL-C",
-  "CERN-OHL-1.1",
-  "CERN-OHL-1.2",
-  "CERN-OHL-P-2.0",
-  "CERN-OHL-S-2.0",
-  "CERN-OHL-W-2.0",
-  "CFITSIO",
-  "check-cvs",
-  "checkmk",
-  "ClArtistic",
-  "Clips",
-  "CMU-Mach",
-  "CMU-Mach-nodoc",
-  "CNRI-Jython",
-  "CNRI-Python",
-  "CNRI-Python-GPL-Compatible",
-  "COIL-1.0",
-  "Community-Spec-1.0",
-  "Condor-1.1",
-  "copyleft-next-0.3.0",
-  "copyleft-next-0.3.1",
-  "Cornell-Lossless-JPEG",
-  "CPAL-1.0",
-  "CPL-1.0",
-  "CPOL-1.02",
-  "Cronyx",
-  "Crossword",
-  "CrystalStacker",
-  "CUA-OPL-1.0",
-  "Cube",
-  "curl",
-  "D-FSL-1.0",
-  "DEC-3-Clause",
-  "diffmark",
-  "DL-DE-BY-2.0",
-  "DL-DE-ZERO-2.0",
-  "DOC",
-  "Dotseqn",
-  "DRL-1.0",
-  "DRL-1.1",
-  "DSDP",
-  "dtoa",
-  "dvipdfm",
-  "ECL-1.0",
-  "ECL-2.0",
-  "eCos-2.0",
-  "EFL-1.0",
-  "EFL-2.0",
-  "eGenix",
-  "Elastic-2.0",
-  "Entessa",
-  "EPICS",
-  "EPL-1.0",
-  "EPL-2.0",
-  "ErlPL-1.1",
-  "etalab-2.0",
-  "EUDatagrid",
-  "EUPL-1.0",
-  "EUPL-1.1",
-  "EUPL-1.2",
-  "Eurosym",
-  "Fair",
-  "FBM",
-  "FDK-AAC",
-  "Ferguson-Twofish",
-  "Frameworx-1.0",
-  "FreeBSD-DOC",
-  "FreeImage",
-  "FSFAP",
-  "FSFAP-no-warranty-disclaimer",
-  "FSFUL",
-  "FSFULLR",
-  "FSFULLRWD",
-  "FTL",
-  "Furuseth",
-  "fwlw",
-  "GCR-docs",
-  "GD",
-  "GFDL-1.1",
-  "GFDL-1.1-invariants-only",
-  "GFDL-1.1-invariants-or-later",
-  "GFDL-1.1-no-invariants-only",
-  "GFDL-1.1-no-invariants-or-later",
-  "GFDL-1.1-only",
-  "GFDL-1.1-or-later",
-  "GFDL-1.2",
-  "GFDL-1.2-invariants-only",
-  "GFDL-1.2-invariants-or-later",
-  "GFDL-1.2-no-invariants-only",
-  "GFDL-1.2-no-invariants-or-later",
-  "GFDL-1.2-only",
-  "GFDL-1.2-or-later",
-  "GFDL-1.3",
-  "GFDL-1.3-invariants-only",
-  "GFDL-1.3-invariants-or-later",
-  "GFDL-1.3-no-invariants-only",
-  "GFDL-1.3-no-invariants-or-later",
-  "GFDL-1.3-only",
-  "GFDL-1.3-or-later",
-  "Giftware",
-  "GL2PS",
-  "Glide",
-  "Glulxe",
-  "GLWTPL",
-  "gnuplot",
-  "GPL-1.0",
-  "GPL-1.0+",
-  "GPL-1.0-only",
-  "GPL-1.0-or-later",
-  "GPL-2.0",
-  "GPL-2.0+",
-  "GPL-2.0-only",
-  "GPL-2.0-or-later",
-  "GPL-2.0-with-autoconf-exception",
-  "GPL-2.0-with-bison-exception",
-  "GPL-2.0-with-classpath-exception",
-  "GPL-2.0-with-font-exception",
-  "GPL-2.0-with-GCC-exception",
-  "GPL-3.0",
-  "GPL-3.0+",
-  "GPL-3.0-only",
-  "GPL-3.0-or-later",
-  "GPL-3.0-with-autoconf-exception",
-  "GPL-3.0-with-GCC-exception",
-  "Graphics-Gems",
-  "gSOAP-1.3b",
-  "gtkbook",
-  "HaskellReport",
-  "hdparm",
-  "Hippocratic-2.1",
-  "HP-1986",
-  "HP-1989",
-  "HPND",
-  "HPND-DEC",
-  "HPND-doc",
-  "HPND-doc-sell",
-  "HPND-export-US",
-  "HPND-export-US-modify",
-  "HPND-Fenneberg-Livingston",
-  "HPND-INRIA-IMAG",
-  "HPND-Kevlin-Henney",
-  "HPND-Markus-Kuhn",
-  "HPND-MIT-disclaimer",
-  "HPND-Pbmplus",
-  "HPND-sell-MIT-disclaimer-xserver",
-  "HPND-sell-regexpr",
-  "HPND-sell-variant",
-  "HPND-sell-variant-MIT-disclaimer",
-  "HPND-UC",
-  "HTMLTIDY",
-  "IBM-pibs",
-  "ICU",
-  "IEC-Code-Components-EULA",
-  "IJG",
-  "IJG-short",
-  "ImageMagick",
-  "iMatix",
-  "Imlib2",
-  "Info-ZIP",
-  "Inner-Net-2.0",
-  "Intel",
-  "Intel-ACPI",
-  "Interbase-1.0",
-  "IPA",
-  "IPL-1.0",
-  "ISC",
-  "ISC-Veillard",
-  "Jam",
-  "JasPer-2.0",
-  "JPL-image",
-  "JPNIC",
-  "JSON",
-  "Kastrup",
-  "Kazlib",
-  "Knuth-CTAN",
-  "LAL-1.2",
-  "LAL-1.3",
-  "Latex2e",
-  "Latex2e-translated-notice",
-  "Leptonica",
-  "LGPL-2.0",
-  "LGPL-2.0+",
-  "LGPL-2.0-only",
-  "LGPL-2.0-or-later",
-  "LGPL-2.1",
-  "LGPL-2.1+",
-  "LGPL-2.1-only",
-  "LGPL-2.1-or-later",
-  "LGPL-3.0",
-  "LGPL-3.0+",
-  "LGPL-3.0-only",
-  "LGPL-3.0-or-later",
-  "LGPLLR",
-  "Libpng",
-  "libpng-2.0",
-  "libselinux-1.0",
-  "libtiff",
-  "libutil-David-Nugent",
-  "LiLiQ-P-1.1",
-  "LiLiQ-R-1.1",
-  "LiLiQ-Rplus-1.1",
-  "Linux-man-pages-1-para",
-  "Linux-man-pages-copyleft",
-  "Linux-man-pages-copyleft-2-para",
-  "Linux-man-pages-copyleft-var",
-  "Linux-OpenIB",
-  "LOOP",
-  "LPD-document",
-  "LPL-1.0",
-  "LPL-1.02",
-  "LPPL-1.0",
-  "LPPL-1.1",
-  "LPPL-1.2",
-  "LPPL-1.3a",
-  "LPPL-1.3c",
-  "lsof",
-  "Lucida-Bitmap-Fonts",
-  "LZMA-SDK-9.11-to-9.20",
-  "LZMA-SDK-9.22",
-  "Mackerras-3-Clause",
-  "Mackerras-3-Clause-acknowledgment",
-  "magaz",
-  "mailprio",
-  "MakeIndex",
-  "Martin-Birgmeier",
-  "McPhee-slideshow",
-  "metamail",
-  "Minpack",
-  "MirOS",
-  "MIT",
-  "MIT-0",
-  "MIT-advertising",
-  "MIT-CMU",
-  "MIT-enna",
-  "MIT-feh",
-  "MIT-Festival",
-  "MIT-Khronos-old",
-  "MIT-Modern-Variant",
-  "MIT-open-group",
-  "MIT-testregex",
-  "MIT-Wu",
-  "MITNFA",
-  "MMIXware",
-  "Motosoto",
-  "MPEG-SSG",
-  "mpi-permissive",
-  "mpich2",
-  "MPL-1.0",
-  "MPL-1.1",
-  "MPL-2.0",
-  "MPL-2.0-no-copyleft-exception",
-  "mplus",
-  "MS-LPL",
-  "MS-PL",
-  "MS-RL",
-  "MTLL",
-  "MulanPSL-1.0",
-  "MulanPSL-2.0",
-  "Multics",
-  "Mup",
-  "NAIST-2003",
-  "NASA-1.3",
-  "Naumen",
-  "NBPL-1.0",
-  "NCGL-UK-2.0",
-  "NCSA",
-  "Net-SNMP",
-  "NetCDF",
-  "Newsletr",
-  "NGPL",
-  "NICTA-1.0",
-  "NIST-PD",
-  "NIST-PD-fallback",
-  "NIST-Software",
-  "NLOD-1.0",
-  "NLOD-2.0",
-  "NLPL",
-  "Nokia",
-  "NOSL",
-  "Noweb",
-  "NPL-1.0",
-  "NPL-1.1",
-  "NPOSL-3.0",
-  "NRL",
-  "NTP",
-  "NTP-0",
-  "Nunit",
-  "O-UDA-1.0",
-  "OCCT-PL",
-  "OCLC-2.0",
-  "ODbL-1.0",
-  "ODC-By-1.0",
-  "OFFIS",
-  "OFL-1.0",
-  "OFL-1.0-no-RFN",
-  "OFL-1.0-RFN",
-  "OFL-1.1",
-  "OFL-1.1-no-RFN",
-  "OFL-1.1-RFN",
-  "OGC-1.0",
-  "OGDL-Taiwan-1.0",
-  "OGL-Canada-2.0",
-  "OGL-UK-1.0",
-  "OGL-UK-2.0",
-  "OGL-UK-3.0",
-  "OGTSL",
-  "OLDAP-1.1",
-  "OLDAP-1.2",
-  "OLDAP-1.3",
-  "OLDAP-1.4",
-  "OLDAP-2.0",
-  "OLDAP-2.0.1",
-  "OLDAP-2.1",
-  "OLDAP-2.2",
-  "OLDAP-2.2.1",
-  "OLDAP-2.2.2",
-  "OLDAP-2.3",
-  "OLDAP-2.4",
-  "OLDAP-2.5",
-  "OLDAP-2.6",
-  "OLDAP-2.7",
-  "OLDAP-2.8",
-  "OLFL-1.3",
-  "OML",
-  "OpenPBS-2.3",
-  "OpenSSL",
-  "OpenSSL-standalone",
-  "OpenVision",
-  "OPL-1.0",
-  "OPL-UK-3.0",
-  "OPUBL-1.0",
-  "OSET-PL-2.1",
-  "OSL-1.0",
-  "OSL-1.1",
-  "OSL-2.0",
-  "OSL-2.1",
-  "OSL-3.0",
-  "PADL",
-  "Parity-6.0.0",
-  "Parity-7.0.0",
-  "PDDL-1.0",
-  "PHP-3.0",
-  "PHP-3.01",
-  "Pixar",
-  "Plexus",
-  "pnmstitch",
-  "PolyForm-Noncommercial-1.0.0",
-  "PolyForm-Small-Business-1.0.0",
-  "PostgreSQL",
-  "PSF-2.0",
-  "psfrag",
-  "psutils",
-  "Python-2.0",
-  "Python-2.0.1",
-  "python-ldap",
-  "Qhull",
-  "QPL-1.0",
-  "QPL-1.0-INRIA-2004",
-  "radvd",
-  "Rdisc",
-  "RHeCos-1.1",
-  "RPL-1.1",
-  "RPL-1.5",
-  "RPSL-1.0",
-  "RSA-MD",
-  "RSCPL",
-  "Ruby",
-  "SAX-PD",
-  "SAX-PD-2.0",
-  "Saxpath",
-  "SCEA",
-  "SchemeReport",
-  "Sendmail",
-  "Sendmail-8.23",
-  "SGI-B-1.0",
-  "SGI-B-1.1",
-  "SGI-B-2.0",
-  "SGI-OpenGL",
-  "SGP4",
-  "SHL-0.5",
-  "SHL-0.51",
-  "SimPL-2.0",
-  "SISSL",
-  "SISSL-1.2",
-  "SL",
-  "Sleepycat",
-  "SMLNJ",
-  "SMPPL",
-  "SNIA",
-  "snprintf",
-  "softSurfer",
-  "Soundex",
-  "Spencer-86",
-  "Spencer-94",
-  "Spencer-99",
-  "SPL-1.0",
-  "ssh-keyscan",
-  "SSH-OpenSSH",
-  "SSH-short",
-  "SSLeay-standalone",
-  "SSPL-1.0",
-  "StandardML-NJ",
-  "SugarCRM-1.1.3",
-  "Sun-PPP",
-  "SunPro",
-  "SWL",
-  "swrule",
-  "Symlinks",
-  "TAPR-OHL-1.0",
-  "TCL",
-  "TCP-wrappers",
-  "TermReadKey",
-  "TGPPL-1.0",
-  "TMate",
-  "TORQUE-1.1",
-  "TOSL",
-  "TPDL",
-  "TPL-1.0",
-  "TTWL",
-  "TTYP0",
-  "TU-Berlin-1.0",
-  "TU-Berlin-2.0",
-  "UCAR",
-  "UCL-1.0",
-  "ulem",
-  "UMich-Merit",
-  "Unicode-3.0",
-  "Unicode-DFS-2015",
-  "Unicode-DFS-2016",
-  "Unicode-TOU",
-  "UnixCrypt",
-  "Unlicense",
-  "UPL-1.0",
-  "URT-RLE",
-  "Vim",
-  "VOSTROM",
-  "VSL-1.0",
-  "W3C",
-  "W3C-19980720",
-  "W3C-20150513",
-  "w3m",
-  "Watcom-1.0",
-  "Widget-Workshop",
-  "Wsuipa",
-  "WTFPL",
-  "wxWindows",
-  "X11",
-  "X11-distribute-modifications-variant",
-  "Xdebug-1.03",
-  "Xerox",
-  "Xfig",
-  "XFree86-1.1",
-  "xinetd",
-  "xkeyboard-config-Zinoviev",
-  "xlock",
-  "Xnet",
-  "xpp",
-  "XSkat",
-  "YPL-1.0",
-  "YPL-1.1",
-  "Zed",
-  "Zeeff",
-  "Zend-2.0",
-  "Zimbra-1.3",
-  "Zimbra-1.4",
-  "Zlib",
-  "zlib-acknowledgement",
-  "ZPL-1.1",
-  "ZPL-2.0",
-  "ZPL-2.1",
-];
diff --git a/src/util/licenses.json b/src/util/licenses.json
deleted file mode 100644
index e5fc47c4..00000000
--- a/src/util/licenses.json
+++ /dev/null
@@ -1,399 +0,0 @@
-[
-  {
-    "name": "@babel/runtime",
-    "author": null,
-    "license": "MIT",
-    "description": "babel's modular runtime helpers",
-    "version": "7.25.6",
-    "source": "https://www.npmjs.com/package/@babel/runtime"
-  },
-  {
-    "name": "@babel/standalone",
-    "author": null,
-    "license": "MIT",
-    "description": "Standalone build of Babel for use in non-Node.js environments.",
-    "version": "7.24.0",
-    "source": "https://www.npmjs.com/package/@babel/standalone"
-  },
-  {
-    "name": "@emotion/react",
-    "author": null,
-    "license": "MIT",
-    "version": "11.11.3",
-    "source": "https://www.npmjs.com/package/@emotion/react"
-  },
-  {
-    "name": "@emotion/styled",
-    "author": null,
-    "license": "MIT",
-    "description": "styled API for emotion",
-    "version": "11.11.0",
-    "source": "https://www.npmjs.com/package/@emotion/styled"
-  },
-  {
-    "name": "@giscus/react",
-    "author": null,
-    "version": "2.4.0",
-    "source": "https://www.npmjs.com/package/@giscus/react"
-  },
-  {
-    "name": "@monaco-editor/react",
-    "author": null,
-    "license": "MIT",
-    "description": "Monaco Editor for React - use the monaco-editor in any React application without needing to use webpack (or rollup/parcel/etc) configuration files / plugins",
-    "version": "4.6.0",
-    "source": "https://www.npmjs.com/package/@monaco-editor/react"
-  },
-  {
-    "name": "@mui/icons-material",
-    "author": null,
-    "license": "MIT",
-    "description": "Material Design icons distributed as SVG React components.",
-    "version": "5.16.5",
-    "source": "https://www.npmjs.com/package/@mui/icons-material"
-  },
-  {
-    "name": "@mui/lab",
-    "author": null,
-    "license": "MIT",
-    "description": "Laboratory for new MUI modules.",
-    "version": "5.0.0-alpha.160",
-    "source": "https://www.npmjs.com/package/@mui/lab"
-  },
-  {
-    "name": "@mui/material",
-    "author": null,
-    "license": "MIT",
-    "description": "Material UI is an open-source React component library that implements Google's Material Design. It's comprehensive and can be used in production out of the box.",
-    "version": "5.16.7",
-    "source": "https://www.npmjs.com/package/@mui/material"
-  },
-  {
-    "name": "@nyariv/sandboxjs",
-    "author": null,
-    "license": "MIT",
-    "description": "Javascript sandboxing library.",
-    "version": "0.8.23",
-    "source": "https://www.npmjs.com/package/@nyariv/sandboxjs"
-  },
-  {
-    "name": "@primer/octicons-react",
-    "author": null,
-    "license": "MIT",
-    "description": "A scalable set of icons handcrafted with <3 by GitHub.",
-    "version": "19.9.0",
-    "source": "https://www.npmjs.com/package/@primer/octicons-react"
-  },
-  {
-    "name": "@zenfs/core",
-    "author": null,
-    "license": "MIT",
-    "description": "A filesystem, anywhere",
-    "version": "0.17.1",
-    "source": "https://www.npmjs.com/package/@zenfs/core"
-  },
-  {
-    "name": "@zenfs/dom",
-    "author": null,
-    "license": "MIT",
-    "description": "DOM backends for ZenFS",
-    "version": "0.2.15",
-    "source": "https://www.npmjs.com/package/@zenfs/dom"
-  },
-  {
-    "name": "ajv",
-    "author": null,
-    "license": "MIT",
-    "description": "Another JSON Schema Validator",
-    "version": "8.12.0",
-    "source": "https://www.npmjs.com/package/ajv"
-  },
-  {
-    "name": "anser",
-    "author": null,
-    "license": "MIT",
-    "description": "A low level parser for ANSI sequences.",
-    "version": "2.1.1",
-    "source": "https://www.npmjs.com/package/anser"
-  },
-  {
-    "name": "axios",
-    "author": null,
-    "license": "MIT",
-    "description": "Promise based HTTP client for the browser and node.js",
-    "version": "1.6.5",
-    "source": "https://www.npmjs.com/package/axios"
-  },
-  {
-    "name": "default-composer",
-    "author": null,
-    "license": "MIT",
-    "description": "A JavaScript library that allows you to set default values for nested objects",
-    "version": "0.6.0",
-    "source": "https://www.npmjs.com/package/default-composer"
-  },
-  {
-    "name": "eruda",
-    "author": null,
-    "license": "MIT",
-    "description": "Console for Mobile Browsers",
-    "version": "3.0.1",
-    "source": "https://www.npmjs.com/package/eruda"
-  },
-  {
-    "name": "escape-carriage",
-    "author": null,
-    "license": "MIT",
-    "description": "Escape carriage return the right way.",
-    "version": "1.3.1",
-    "source": "https://www.npmjs.com/package/escape-carriage"
-  },
-  {
-    "name": "flatlist-react",
-    "author": null,
-    "license": "MIT",
-    "description": "A helpful utility component to handle lists in react like a champ",
-    "version": "1.5.14",
-    "source": "https://www.npmjs.com/package/flatlist-react"
-  },
-  {
-    "name": "googlers-tools",
-    "author": null,
-    "license": "GPL-3.0",
-    "description": "My own tools / scripts that I use.",
-    "version": "1.4.5",
-    "source": "https://www.npmjs.com/package/googlers-tools"
-  },
-  {
-    "name": "highlight.js",
-    "author": null,
-    "license": "BSD-3-Clause",
-    "description": "Syntax highlighting with language autodetection.",
-    "version": "11.9.0",
-    "source": "https://www.npmjs.com/package/highlight.js"
-  },
-  {
-    "name": "ini",
-    "author": null,
-    "license": "ISC",
-    "description": "An ini encoder/decoder for node",
-    "version": "4.1.1",
-    "source": "https://www.npmjs.com/package/ini"
-  },
-  {
-    "name": "linkify-it",
-    "author": null,
-    "license": "MIT",
-    "description": "Links recognition library with FULL unicode support",
-    "version": "5.0.0",
-    "source": "https://www.npmjs.com/package/linkify-it"
-  },
-  {
-    "name": "localforage",
-    "author": null,
-    "license": "Apache-2.0",
-    "description": "Offline storage, improved.",
-    "version": "1.10.0",
-    "source": "https://www.npmjs.com/package/localforage"
-  },
-  {
-    "name": "markdown-to-jsx",
-    "author": null,
-    "license": "MIT",
-    "description": "Convert markdown to JSX with ease for React and React-like projects. Super lightweight and highly configurable.",
-    "version": "7.4.0",
-    "source": "https://www.npmjs.com/package/markdown-to-jsx"
-  },
-  {
-    "name": "material-icons",
-    "author": null,
-    "license": "Apache-2.0",
-    "description": "Latest icon fonts and CSS for self-hosting material design icons.",
-    "version": "1.13.12",
-    "source": "https://www.npmjs.com/package/material-icons"
-  },
-  {
-    "name": "material-ui-confirm",
-    "author": null,
-    "license": "MIT",
-    "description": "Simple confirmation dialogs built on top of @mui/material",
-    "version": "3.0.16",
-    "source": "https://www.npmjs.com/package/material-ui-confirm"
-  },
-  {
-    "name": "modfs",
-    "author": null,
-    "license": "MIT",
-    "description": "ModFS is a json format processor and also used in MMRL as the ModFS system",
-    "version": "1.4.2",
-    "source": "https://www.npmjs.com/package/modfs"
-  },
-  {
-    "name": "monaco-editor",
-    "author": null,
-    "license": "MIT",
-    "description": "A browser based code editor",
-    "version": "0.48.0",
-    "source": "https://www.npmjs.com/package/monaco-editor"
-  },
-  {
-    "name": "monaco-editor-core",
-    "author": null,
-    "license": "MIT",
-    "description": "A browser based code editor",
-    "version": "0.50.0",
-    "source": "https://www.npmjs.com/package/monaco-editor-core"
-  },
-  {
-    "name": "monaco-languageclient",
-    "author": "TypeFox GmbH",
-    "license": "MIT",
-    "description": "Monaco Language client implementation",
-    "version": "6.6.1",
-    "source": "https://www.npmjs.com/package/monaco-languageclient"
-  },
-  {
-    "name": "object-assign",
-    "author": "Sindre Sorhus",
-    "license": "MIT",
-    "description": "ES2015 `Object.assign()` ponyfill",
-    "version": "4.1.1",
-    "source": "https://www.npmjs.com/package/object-assign"
-  },
-  {
-    "name": "onsenui",
-    "author": null,
-    "license": "Apache-2.0",
-    "description": "HTML5 Mobile Framework & UI Components",
-    "version": "2.12.8",
-    "source": "https://www.npmjs.com/package/onsenui"
-  },
-  {
-    "name": "properties-file",
-    "author": null,
-    "license": "MIT",
-    "description": ".properties file parser, editor, formatter and Webpack loader.",
-    "version": "3.3.16",
-    "source": "https://www.npmjs.com/package/properties-file"
-  },
-  {
-    "name": "react",
-    "author": null,
-    "license": "MIT",
-    "description": "React is a JavaScript library for building user interfaces.",
-    "version": "18.2.0",
-    "source": "https://www.npmjs.com/package/react"
-  },
-  {
-    "name": "react-device-detect",
-    "author": "Michael Laktionov",
-    "license": "MIT",
-    "description": "Detect device type and render your component according to it",
-    "version": "2.2.3",
-    "source": "https://www.npmjs.com/package/react-device-detect"
-  },
-  {
-    "name": "react-disappear",
-    "author": null,
-    "license": "MIT",
-    "description": "Detects if the inner children are visible",
-    "version": "1.1.3",
-    "source": "https://www.npmjs.com/package/react-disappear"
-  },
-  {
-    "name": "react-dom",
-    "author": null,
-    "license": "MIT",
-    "description": "React package for working with the DOM.",
-    "version": "18.2.0",
-    "source": "https://www.npmjs.com/package/react-dom"
-  },
-  {
-    "name": "react-fast-marquee",
-    "author": null,
-    "license": "MIT",
-    "description": "A lightweight React component that utilizes the power of CSS animations to create silky smooth marquees.",
-    "version": "1.6.2",
-    "source": "https://www.npmjs.com/package/react-fast-marquee"
-  },
-  {
-    "name": "react-onsenui",
-    "author": null,
-    "license": "Apache-2.0",
-    "description": "Onsen UI - React Components for Hybrid Cordova/PhoneGap Apps with Material Design and iOS UI components",
-    "version": "1.13.3",
-    "source": "https://www.npmjs.com/package/react-onsenui"
-  },
-  {
-    "name": "react-render-tools",
-    "author": null,
-    "license": "GPL-3.0",
-    "description": "Simple tools to make react render easier to use",
-    "version": "1.0.1",
-    "source": "https://www.npmjs.com/package/react-render-tools"
-  },
-  {
-    "name": "react-syntax-highlighter",
-    "author": null,
-    "license": "MIT",
-    "description": "syntax highlighting component for react with prismjs or highlightjs ast using inline styles",
-    "version": "15.5.0",
-    "source": "https://www.npmjs.com/package/react-syntax-highlighter"
-  },
-  {
-    "name": "react-transition-group",
-    "author": null,
-    "license": "BSD-3-Clause",
-    "description": "A react component toolset for managing animations",
-    "version": "4.4.5",
-    "source": "https://www.npmjs.com/package/react-transition-group"
-  },
-  {
-    "name": "react-zoom-pan-pinch",
-    "author": null,
-    "license": "MIT",
-    "description": "Zoom and pan html elements in easy way",
-    "version": "3.3.0",
-    "source": "https://www.npmjs.com/package/react-zoom-pan-pinch"
-  },
-  {
-    "name": "reflect-metadata",
-    "author": "Ron Buckton",
-    "license": "Apache-2.0",
-    "description": "Polyfill for Metadata Reflection API",
-    "version": "0.2.2",
-    "source": "https://www.npmjs.com/package/reflect-metadata"
-  },
-  {
-    "name": "underscore",
-    "author": null,
-    "license": "MIT",
-    "description": "JavaScript's functional programming helper library.",
-    "version": "1.13.6",
-    "source": "https://www.npmjs.com/package/underscore"
-  },
-  {
-    "name": "usehooks-ts",
-    "author": null,
-    "license": "MIT",
-    "description": "React hook library, ready to use, written in Typescript.",
-    "version": "3.1.0",
-    "source": "https://www.npmjs.com/package/usehooks-ts"
-  },
-  {
-    "name": "uuid",
-    "author": null,
-    "license": "MIT",
-    "description": "RFC9562 UUIDs",
-    "version": "10.0.0",
-    "source": "https://www.npmjs.com/package/uuid"
-  },
-  {
-    "name": "yaml",
-    "author": null,
-    "license": "ISC",
-    "description": "JavaScript parser and stringifier for YAML",
-    "version": "2.3.4",
-    "source": "https://www.npmjs.com/package/yaml"
-  }
-]
\ No newline at end of file
diff --git a/src/util/native-licenses.json b/src/util/native-licenses.json
deleted file mode 100644
index 341a5b78..00000000
--- a/src/util/native-licenses.json
+++ /dev/null
@@ -1,90 +0,0 @@
-[
-    {
-        "name": "androidx.browser:browser",
-        "description": "androidx.browser:browser:1.8.0",
-        "version": "1.8.0",
-        "license": "null",
-        "author": "null",
-        "repository": "https://mvnrepository.com/artifact/androidx.browser/browser/1.8.0"
-    },
-    {
-        "name": "androidx.annotation:annotation",
-        "description": "androidx.annotation:annotation:1.8.2",
-        "version": "1.8.2",
-        "license": "null",
-        "author": "null",
-        "repository": "https://mvnrepository.com/artifact/androidx.annotation/annotation/1.8.2"
-    },
-    {
-        "name": "androidx.appcompat:appcompat",
-        "description": "androidx.appcompat:appcompat:1.7.0",
-        "version": "1.7.0",
-        "license": "null",
-        "author": "null",
-        "repository": "https://mvnrepository.com/artifact/androidx.appcompat/appcompat/1.7.0"
-    },
-    {
-        "name": "androidx.core:core",
-        "description": "androidx.core:core:1.13.1",
-        "version": "1.13.1",
-        "license": "null",
-        "author": "null",
-        "repository": "https://mvnrepository.com/artifact/androidx.core/core/1.13.1"
-    },
-    {
-        "name": "androidx.webkit:webkit",
-        "description": "androidx.webkit:webkit:1.11.0",
-        "version": "1.11.0",
-        "license": "null",
-        "author": "null",
-        "repository": "https://mvnrepository.com/artifact/androidx.webkit/webkit/1.11.0"
-    },
-    {
-        "name": "androidx.profileinstaller:profileinstaller",
-        "description": "androidx.profileinstaller:profileinstaller:1.3.1",
-        "version": "1.3.1",
-        "license": "null",
-        "author": "null",
-        "repository": "https://mvnrepository.com/artifact/androidx.profileinstaller/profileinstaller/1.3.1"
-    },
-    {
-        "name": "androidx.room:room-runtime",
-        "description": "androidx.room:room-runtime:2.6.1",
-        "version": "2.6.1",
-        "license": "null",
-        "author": "null",
-        "repository": "https://mvnrepository.com/artifact/androidx.room/room-runtime/2.6.1"
-    },
-    {
-        "name": "com.github.topjohnwu.libsu:core",
-        "description": "com.github.topjohnwu.libsu:core:5.2.1",
-        "version": "5.2.1",
-        "license": "null",
-        "author": "null",
-        "repository": "https://mvnrepository.com/artifact/com.github.topjohnwu.libsu/core/5.2.1"
-    },
-    {
-        "name": "com.github.topjohnwu.libsu:io",
-        "description": "com.github.topjohnwu.libsu:io:5.2.1",
-        "version": "5.2.1",
-        "license": "null",
-        "author": "null",
-        "repository": "https://mvnrepository.com/artifact/com.github.topjohnwu.libsu/io/5.2.1"
-    },
-    {
-        "name": "org.apache.cordova:framework",
-        "description": "org.apache.cordova:framework:12.0.1",
-        "version": "12.0.1",
-        "license": "null",
-        "author": "null",
-        "repository": "https://mvnrepository.com/artifact/org.apache.cordova/framework/12.0.1"
-    },
-    {
-        "name": "com.squareup.okhttp3:okhttp",
-        "description": "com.squareup.okhttp3:okhttp:4.12.0",
-        "version": "4.12.0",
-        "license": "null",
-        "author": "null",
-        "repository": "https://mvnrepository.com/artifact/com.squareup.okhttp3/okhttp/4.12.0"
-    }
-]
\ No newline at end of file
diff --git a/src/util/onsCustomElement.tsx b/src/util/onsCustomElement.tsx
deleted file mode 100644
index b9ccdc1d..00000000
--- a/src/util/onsCustomElement.tsx
+++ /dev/null
@@ -1,87 +0,0 @@
-// https://github.com/OnsenUI/OnsenUI/blob/90c0aeb2b2acadfefb66a3da038b6b09cfb5b9c8/react-onsenui/src/onsCustomElement.jsx
-import { Box, BoxProps, styled } from "@mui/material";
-import React, { useRef, useEffect } from "react";
-const kebabize = (camelString: string) => camelString.replace(/([a-zA-Z])([A-Z])/g, "$1-$2").toLowerCase();
-
-const addDeprecated = (props, deprecated) => {
-  const propsCopy = { ...props };
-
-  const nameMap = {
-    className: "class",
-    ...deprecated,
-  };
-
-  for (const [oldName, newName] of Object.entries(nameMap)) {
-    if (propsCopy[newName] === undefined && propsCopy[oldName] !== undefined) {
-      propsCopy[newName] = propsCopy[oldName];
-      delete propsCopy[oldName];
-    }
-  }
-
-  return propsCopy;
-};
-
-function useCustomElementListener(ref, prop, handler) {
-  const event = prop.slice(2).toLowerCase();
-  useEffect(() => {
-    const current = ref.current;
-    current.addEventListener(event, handler);
-
-    return function cleanup() {
-      current.removeEventListener(event, handler);
-    };
-  }, [ref, handler]);
-}
-
-interface CustomElementOptions {
-  notAttributes?: string[];
-  deprecated?: {
-    [name: string]: string;
-  };
-}
-
-function useCustomElement

(props: P, options?: CustomElementOptions, ref?: any) { - const notAttributes = options?.notAttributes || []; - const deprecated = options?.deprecated || {}; - - const properties = {}; - for (const [prop, value] of Object.entries(addDeprecated(props, deprecated))) { - const jsName = kebabize(prop); - - if (notAttributes.includes(prop)) { - useEffect(() => { - ref.current[prop] = value; - }); - } else if (/^on[A-Z]/.test(prop)) { - useCustomElementListener(ref, prop, value); - } else if (typeof value === "boolean") { - properties[jsName] = value ? "" : null; - } else if (typeof value === "object" && value !== null) { - properties[jsName] = JSON.stringify(value); - } else { - properties[jsName] = value; - } - } - - return { properties }; -} - -export default function onsCustomElement( - WrappedComponent: keyof JSX.IntrinsicElements, - options?: CustomElementOptions -) { - return styled( - React.forwardRef, P> & P>((props, _ref) => { - const ref = _ref || useRef(); - - const { children, style, ...rest } = props; - const { properties } = useCustomElement, P>, "style" | "children">>( - rest, - options, - ref - ); - - return React.createElement(WrappedComponent, { ref: ref, key: WrappedComponent, style: style, ...properties }, children); - }) - ); -} diff --git a/src/util/path.js b/src/util/path.js deleted file mode 100644 index 6afda41c..00000000 --- a/src/util/path.js +++ /dev/null @@ -1,495 +0,0 @@ -function assertPath(path) { - if (typeof path !== "string") { - throw new TypeError("Path must be a string. Received " + JSON.stringify(path)); - } -} - -// Resolves . and .. elements in a path with directory names -function normalizeStringPosix(path, allowAboveRoot) { - var res = ""; - var lastSegmentLength = 0; - var lastSlash = -1; - var dots = 0; - var code; - for (var i = 0; i <= path.length; ++i) { - if (i < path.length) code = path.charCodeAt(i); - else if (code === 47 /*/*/) break; - else code = 47 /*/*/; - if (code === 47 /*/*/) { - if (lastSlash === i - 1 || dots === 1) { - // NOOP - } else if (lastSlash !== i - 1 && dots === 2) { - if ( - res.length < 2 || - lastSegmentLength !== 2 || - res.charCodeAt(res.length - 1) !== 46 /*.*/ || - res.charCodeAt(res.length - 2) !== 46 /*.*/ - ) { - if (res.length > 2) { - var lastSlashIndex = res.lastIndexOf("/"); - if (lastSlashIndex !== res.length - 1) { - if (lastSlashIndex === -1) { - res = ""; - lastSegmentLength = 0; - } else { - res = res.slice(0, lastSlashIndex); - lastSegmentLength = res.length - 1 - res.lastIndexOf("/"); - } - lastSlash = i; - dots = 0; - continue; - } - } else if (res.length === 2 || res.length === 1) { - res = ""; - lastSegmentLength = 0; - lastSlash = i; - dots = 0; - continue; - } - } - if (allowAboveRoot) { - if (res.length > 0) res += "/.."; - else res = ".."; - lastSegmentLength = 2; - } - } else { - if (res.length > 0) res += "/" + path.slice(lastSlash + 1, i); - else res = path.slice(lastSlash + 1, i); - lastSegmentLength = i - lastSlash - 1; - } - lastSlash = i; - dots = 0; - } else if (code === 46 /*.*/ && dots !== -1) { - ++dots; - } else { - dots = -1; - } - } - return res; -} - -function _format(sep, pathObject) { - var dir = pathObject.dir || pathObject.root; - var base = pathObject.base || (pathObject.name || "") + (pathObject.ext || ""); - if (!dir) { - return base; - } - if (dir === pathObject.root) { - return dir + base; - } - return dir + sep + base; -} - -class Path { - cwd = undefined; - sep = "/"; - delimiter = ":"; - win32 = null; - posix = null; - - constructor(cwd) { - this.cwd = cwd; - } - - resolve() { - var resolvedPath = ""; - var resolvedAbsolute = false; - - for (var i = arguments.length - 1; i >= -1 && !resolvedAbsolute; i--) { - var path; - if (i >= 0) path = arguments[i]; - else { - if (this.cwd === undefined) this.cwd = "/"; - path = this.cwd; - } - - assertPath(path); - - // Skip empty entries - if (path.length === 0) { - continue; - } - - resolvedPath = path + "/" + resolvedPath; - resolvedAbsolute = path.charCodeAt(0) === 47 /*/*/; - } - - // At this point the path should be resolved to a full absolute path, but - // handle relative paths to be safe (might happen when process.cwd() fails) - - // Normalize the path - resolvedPath = normalizeStringPosix(resolvedPath, !resolvedAbsolute); - - if (resolvedAbsolute) { - if (resolvedPath.length > 0) return "/" + resolvedPath; - else return "/"; - } else if (resolvedPath.length > 0) { - return resolvedPath; - } else { - return "."; - } - } - - normalize(path) { - assertPath(path); - - if (path.length === 0) return "."; - - var isAbsolute = path.charCodeAt(0) === 47; /*/*/ - var trailingSeparator = path.charCodeAt(path.length - 1) === 47; /*/*/ - - // Normalize the path - path = normalizeStringPosix(path, !isAbsolute); - - if (path.length === 0 && !isAbsolute) path = "."; - if (path.length > 0 && trailingSeparator) path += "/"; - - if (isAbsolute) return "/" + path; - return path; - } - - isAbsolute(path) { - assertPath(path); - return path.length > 0 && path.charCodeAt(0) === 47 /*/*/; - } - - join() { - if (arguments.length === 0) return "."; - var joined; - for (var i = 0; i < arguments.length; ++i) { - var arg = arguments[i]; - assertPath(arg); - if (arg.length > 0) { - if (joined === undefined) joined = arg; - else joined += "/" + arg; - } - } - if (joined === undefined) return "."; - return this.normalize(joined); - } - - relative(from, to) { - assertPath(from); - assertPath(to); - - if (from === to) return ""; - - from = this.resolve(from); - to = this.resolve(to); - - if (from === to) return ""; - - // Trim any leading backslashes - var fromStart = 1; - for (; fromStart < from.length; ++fromStart) { - if (from.charCodeAt(fromStart) !== 47 /*/*/) break; - } - var fromEnd = from.length; - var fromLen = fromEnd - fromStart; - - // Trim any leading backslashes - var toStart = 1; - for (; toStart < to.length; ++toStart) { - if (to.charCodeAt(toStart) !== 47 /*/*/) break; - } - var toEnd = to.length; - var toLen = toEnd - toStart; - - // Compare paths to find the longest common path from root - var length = fromLen < toLen ? fromLen : toLen; - var lastCommonSep = -1; - var i = 0; - for (; i <= length; ++i) { - if (i === length) { - if (toLen > length) { - if (to.charCodeAt(toStart + i) === 47 /*/*/) { - // We get here if `from` is the exact base path for `to`. - // For example: from='/foo/bar'; to='/foo/bar/baz' - return to.slice(toStart + i + 1); - } else if (i === 0) { - // We get here if `from` is the root - // For example: from='/'; to='/foo' - return to.slice(toStart + i); - } - } else if (fromLen > length) { - if (from.charCodeAt(fromStart + i) === 47 /*/*/) { - // We get here if `to` is the exact base path for `from`. - // For example: from='/foo/bar/baz'; to='/foo/bar' - lastCommonSep = i; - } else if (i === 0) { - // We get here if `to` is the root. - // For example: from='/foo'; to='/' - lastCommonSep = 0; - } - } - break; - } - var fromCode = from.charCodeAt(fromStart + i); - var toCode = to.charCodeAt(toStart + i); - if (fromCode !== toCode) break; - else if (fromCode === 47 /*/*/) lastCommonSep = i; - } - - var out = ""; - // Generate the relative path based on the path difference between `to` - // and `from` - for (i = fromStart + lastCommonSep + 1; i <= fromEnd; ++i) { - if (i === fromEnd || from.charCodeAt(i) === 47 /*/*/) { - if (out.length === 0) out += ".."; - else out += "/.."; - } - } - - // Lastly, append the rest of the destination (`to`) path that comes after - // the common path parts - if (out.length > 0) return out + to.slice(toStart + lastCommonSep); - else { - toStart += lastCommonSep; - if (to.charCodeAt(toStart) === 47 /*/*/) ++toStart; - return to.slice(toStart); - } - } - - _makeLong(path) { - return path; - } - - dirname(path) { - assertPath(path); - if (path.length === 0) return "."; - var code = path.charCodeAt(0); - var hasRoot = code === 47; /*/*/ - var end = -1; - var matchedSlash = true; - for (var i = path.length - 1; i >= 1; --i) { - code = path.charCodeAt(i); - if (code === 47 /*/*/) { - if (!matchedSlash) { - end = i; - break; - } - } else { - // We saw the first non-path separator - matchedSlash = false; - } - } - - if (end === -1) return hasRoot ? "/" : "."; - if (hasRoot && end === 1) return "//"; - return path.slice(0, end); - } - - basename(path, ext) { - if (ext !== undefined && typeof ext !== "string") throw new TypeError('"ext" argument must be a string'); - assertPath(path); - - var start = 0; - var end = -1; - var matchedSlash = true; - var i; - - if (ext !== undefined && ext.length > 0 && ext.length <= path.length) { - if (ext.length === path.length && ext === path) return ""; - var extIdx = ext.length - 1; - var firstNonSlashEnd = -1; - for (i = path.length - 1; i >= 0; --i) { - var code = path.charCodeAt(i); - if (code === 47 /*/*/) { - // If we reached a path separator that was not part of a set of path - // separators at the end of the string, stop now - if (!matchedSlash) { - start = i + 1; - break; - } - } else { - if (firstNonSlashEnd === -1) { - // We saw the first non-path separator, remember this index in case - // we need it if the extension ends up not matching - matchedSlash = false; - firstNonSlashEnd = i + 1; - } - if (extIdx >= 0) { - // Try to match the explicit extension - if (code === ext.charCodeAt(extIdx)) { - if (--extIdx === -1) { - // We matched the extension, so mark this as the end of our path - // component - end = i; - } - } else { - // Extension does not match, so our result is the entire path - // component - extIdx = -1; - end = firstNonSlashEnd; - } - } - } - } - - if (start === end) end = firstNonSlashEnd; - else if (end === -1) end = path.length; - return path.slice(start, end); - } else { - for (i = path.length - 1; i >= 0; --i) { - if (path.charCodeAt(i) === 47 /*/*/) { - // If we reached a path separator that was not part of a set of path - // separators at the end of the string, stop now - if (!matchedSlash) { - start = i + 1; - break; - } - } else if (end === -1) { - // We saw the first non-path separator, mark this as the end of our - // path component - matchedSlash = false; - end = i + 1; - } - } - - if (end === -1) return ""; - return path.slice(start, end); - } - } - - extname(path) { - assertPath(path); - var startDot = -1; - var startPart = 0; - var end = -1; - var matchedSlash = true; - // Track the state of characters (if any) we see before our first dot and - // after any path separator we find - var preDotState = 0; - for (var i = path.length - 1; i >= 0; --i) { - var code = path.charCodeAt(i); - if (code === 47 /*/*/) { - // If we reached a path separator that was not part of a set of path - // separators at the end of the string, stop now - if (!matchedSlash) { - startPart = i + 1; - break; - } - continue; - } - if (end === -1) { - // We saw the first non-path separator, mark this as the end of our - // extension - matchedSlash = false; - end = i + 1; - } - if (code === 46 /*.*/) { - // If this is our first dot, mark it as the start of our extension - if (startDot === -1) startDot = i; - else if (preDotState !== 1) preDotState = 1; - } else if (startDot !== -1) { - // We saw a non-dot and non-path separator before our dot, so we should - // have a good chance at having a non-empty extension - preDotState = -1; - } - } - - if ( - startDot === -1 || - end === -1 || - // We saw a non-dot character immediately before the dot - preDotState === 0 || - // The (right-most) trimmed path component is exactly '..' - (preDotState === 1 && startDot === end - 1 && startDot === startPart + 1) - ) { - return ""; - } - return path.slice(startDot, end); - } - - format(pathObject) { - if (pathObject === null || typeof pathObject !== "object") { - throw new TypeError('The "pathObject" argument must be of type Object. Received type ' + typeof pathObject); - } - return _format("/", pathObject); - } - - parse(path) { - assertPath(path); - - var ret = { root: "", dir: "", base: "", ext: "", name: "" }; - if (path.length === 0) return ret; - var code = path.charCodeAt(0); - var isAbsolute = code === 47; /*/*/ - var start; - if (isAbsolute) { - ret.root = "/"; - start = 1; - } else { - start = 0; - } - var startDot = -1; - var startPart = 0; - var end = -1; - var matchedSlash = true; - var i = path.length - 1; - - // Track the state of characters (if any) we see before our first dot and - // after any path separator we find - var preDotState = 0; - - // Get non-dir info - for (; i >= start; --i) { - code = path.charCodeAt(i); - if (code === 47 /*/*/) { - // If we reached a path separator that was not part of a set of path - // separators at the end of the string, stop now - if (!matchedSlash) { - startPart = i + 1; - break; - } - continue; - } - if (end === -1) { - // We saw the first non-path separator, mark this as the end of our - // extension - matchedSlash = false; - end = i + 1; - } - if (code === 46 /*.*/) { - // If this is our first dot, mark it as the start of our extension - if (startDot === -1) startDot = i; - else if (preDotState !== 1) preDotState = 1; - } else if (startDot !== -1) { - // We saw a non-dot and non-path separator before our dot, so we should - // have a good chance at having a non-empty extension - preDotState = -1; - } - } - - if ( - startDot === -1 || - end === -1 || - // We saw a non-dot character immediately before the dot - preDotState === 0 || - // The (right-most) trimmed path component is exactly '..' - (preDotState === 1 && startDot === end - 1 && startDot === startPart + 1) - ) { - if (end !== -1) { - if (startPart === 0 && isAbsolute) ret.base = ret.name = path.slice(1, end); - else ret.base = ret.name = path.slice(startPart, end); - } - } else { - if (startPart === 0 && isAbsolute) { - ret.name = path.slice(1, startDot); - ret.base = path.slice(1, end); - } else { - ret.name = path.slice(startPart, startDot); - ret.base = path.slice(startPart, end); - } - ret.ext = path.slice(startDot, end); - } - - if (startPart > 0) ret.dir = path.slice(0, startPart - 1); - else if (isAbsolute) ret.dir = "/"; - - return ret; - } -} - -const path = new Path(undefined); -export { path, Path }; diff --git a/src/util/stringFormat.ts b/src/util/stringFormat.ts deleted file mode 100644 index a73d100e..00000000 --- a/src/util/stringFormat.ts +++ /dev/null @@ -1,49 +0,0 @@ -export function formatString(template: string, object: object): string { - return template.replace(/\<(\w+(\.\w+)*)\>/gi, (match, key) => { - const keys = key.split("."); - let value = object; - for (const k of keys) { - if (k in value) { - value = value[k]; - } else { - return match; - } - } - return formatString(String(value), object); - }); - } - - export function formatObjectEntries(object: O): O { - const formatValue = (value: any): any => { - if (typeof value === "string") { - return value.replace(/\<(\w+(\.\w+)*)\>/gi, (match, key) => { - const keys = key.split("."); - let tempValue = object; - for (const k of keys) { - if (k in tempValue) { - tempValue = tempValue[k]; - } else { - return match; - } - } - return formatValue(tempValue); - }); - } else if (Array.isArray(value)) { - return value.map((item: any) => formatValue(item)); - } else if (typeof value === "object" && value !== null) { - const formattedObject: any = {}; - for (const prop in value) { - formattedObject[prop] = formatValue(value[prop]); - } - return formattedObject; - } - return value; - }; - - const formattedObject: any = {}; - for (const key in object) { - const formattedValue = formatValue(object[key]); - formattedObject[key] = formattedValue; - } - return formattedObject; - } \ No newline at end of file diff --git a/src/util/util.ts b/src/util/util.ts deleted file mode 100644 index 78deb330..00000000 --- a/src/util/util.ts +++ /dev/null @@ -1,3 +0,0 @@ -export function isLiteralObject(a: O) { - return !!a && a.constructor === Object; -} diff --git a/tsconfig.json b/tsconfig.json deleted file mode 100644 index 879ded84..00000000 --- a/tsconfig.json +++ /dev/null @@ -1,46 +0,0 @@ -{ - "compilerOptions": { - "outDir": "./app/src/main/assets", - "allowJs": true, - "declaration": false, - "experimentalDecorators": true, - "emitDecoratorMetadata": true, - "declarationMap": false, - "typeRoots": ["src/typings/global.d.ts"], - "sourceMap": true, - "strict": true, - "noImplicitReturns": false, - "noImplicitAny": false, - "module": "ES2022", - "lib": ["ES2023.Array", "ES2023", "ES2018", "dom"], - "allowSyntheticDefaultImports": true, - "moduleResolution": "node", - "target": "ES2022", - "resolveJsonModule": true, - "esModuleInterop": true, - "jsx": "react-jsx", - "baseUrl": "./src", - "paths": { - "@Native/*": ["native/*"], - "@Hooks/*": ["hooks/*"], - "@Builders/*": ["builders/*"], - "@Components/*": ["components/*"], - "@Types/*": ["typings/*"], - "@Util/*": ["util/*"], - "@Bootloader": ["index.tsx"], - "@Package": ["./../package.json"], - "@Styles/*": ["styles/*"], - "@Activitys/*": ["activitys/*"], - "@Strings": ["language/core/index.ts"], - "@Annotation/*": ["annotation/*"], - "@Loclaes/*": ["loclaes/*"] - } - }, - "ts-node": { - "compilerOptions": { - "module": "CommonJS", - "types": ["node"] - } - }, - "include": ["./src/**/*"] -} diff --git a/web/app.html b/web/app.html deleted file mode 100644 index 18b633ae..00000000 --- a/web/app.html +++ /dev/null @@ -1,38 +0,0 @@ - - - - - - - - - - - - - - - - - <%= htmlWebpackPlugin.options.opt.title %> - - - - - - - - - - - - - - - - - - - - - diff --git a/web/assets/APatchSULogo.png b/web/assets/APatchSULogo.png deleted file mode 100644 index dc53887c..00000000 Binary files a/web/assets/APatchSULogo.png and /dev/null differ diff --git a/web/assets/KernelSULogo.png b/web/assets/KernelSULogo.png deleted file mode 100644 index cddff920..00000000 Binary files a/web/assets/KernelSULogo.png and /dev/null differ diff --git a/web/assets/MMRL-Badge.svg b/web/assets/MMRL-Badge.svg deleted file mode 100644 index 29b174fb..00000000 --- a/web/assets/MMRL-Badge.svg +++ /dev/null @@ -1,31 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/web/assets/MMRL-CLI-Cover.png b/web/assets/MMRL-CLI-Cover.png deleted file mode 100644 index f4ff82c2..00000000 Binary files a/web/assets/MMRL-CLI-Cover.png and /dev/null differ diff --git a/web/assets/MMRL-Cover.png b/web/assets/MMRL-Cover.png deleted file mode 100644 index c9e5c931..00000000 Binary files a/web/assets/MMRL-Cover.png and /dev/null differ diff --git a/web/assets/MagiskSULogo.png b/web/assets/MagiskSULogo.png deleted file mode 100644 index 8d8b91bf..00000000 Binary files a/web/assets/MagiskSULogo.png and /dev/null differ diff --git a/web/assets/favicons/android-chrome-192x192.png b/web/assets/favicons/android-chrome-192x192.png deleted file mode 100644 index bd7bdf6a..00000000 Binary files a/web/assets/favicons/android-chrome-192x192.png and /dev/null differ diff --git a/web/assets/favicons/android-chrome-512x512.png b/web/assets/favicons/android-chrome-512x512.png deleted file mode 100644 index 9d8bacc1..00000000 Binary files a/web/assets/favicons/android-chrome-512x512.png and /dev/null differ diff --git a/web/assets/favicons/apple-touch-icon.png b/web/assets/favicons/apple-touch-icon.png deleted file mode 100644 index e01129cd..00000000 Binary files a/web/assets/favicons/apple-touch-icon.png and /dev/null differ diff --git a/web/assets/favicons/browserconfig.xml b/web/assets/favicons/browserconfig.xml deleted file mode 100644 index 4c3d55f0..00000000 --- a/web/assets/favicons/browserconfig.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - #da532c - - - diff --git a/web/assets/favicons/favicon-16x16.png b/web/assets/favicons/favicon-16x16.png deleted file mode 100644 index 20af75f0..00000000 Binary files a/web/assets/favicons/favicon-16x16.png and /dev/null differ diff --git a/web/assets/favicons/favicon-32x32.png b/web/assets/favicons/favicon-32x32.png deleted file mode 100644 index 41638ab5..00000000 Binary files a/web/assets/favicons/favicon-32x32.png and /dev/null differ diff --git a/web/assets/favicons/favicon.ico b/web/assets/favicons/favicon.ico deleted file mode 100644 index 5ae51f00..00000000 Binary files a/web/assets/favicons/favicon.ico and /dev/null differ diff --git a/web/assets/favicons/mstile-150x150.png b/web/assets/favicons/mstile-150x150.png deleted file mode 100644 index 3bd553ca..00000000 Binary files a/web/assets/favicons/mstile-150x150.png and /dev/null differ diff --git a/web/assets/favicons/safari-pinned-tab.svg b/web/assets/favicons/safari-pinned-tab.svg deleted file mode 100644 index 8e94582c..00000000 --- a/web/assets/favicons/safari-pinned-tab.svg +++ /dev/null @@ -1,21 +0,0 @@ - - - - -Created by potrace 1.14, written by Peter Selinger 2001-2017 - - - - - diff --git a/web/assets/favicons/site.webmanifest b/web/assets/favicons/site.webmanifest deleted file mode 100644 index 28be9b54..00000000 --- a/web/assets/favicons/site.webmanifest +++ /dev/null @@ -1,19 +0,0 @@ -{ - "name": "", - "short_name": "", - "icons": [ - { - "src": "assets/favicon/android-chrome-192x192.png", - "sizes": "192x192", - "type": "image/png" - }, - { - "src": "assets/favicon/android-chrome-512x512.png", - "sizes": "512x512", - "type": "image/png" - } - ], - "theme_color": "#ffffff", - "background_color": "#ffffff", - "display": "standalone" -} diff --git a/web/assets/gradient-1080px.png b/web/assets/gradient-1080px.png deleted file mode 100644 index 8c392a19..00000000 Binary files a/web/assets/gradient-1080px.png and /dev/null differ diff --git a/web/assets/gradient-512px.png b/web/assets/gradient-512px.png deleted file mode 100644 index 5528d7b0..00000000 Binary files a/web/assets/gradient-512px.png and /dev/null differ diff --git a/web/assets/icon-xmas-512px.png b/web/assets/icon-xmas-512px.png deleted file mode 100644 index 5e169d02..00000000 Binary files a/web/assets/icon-xmas-512px.png and /dev/null differ diff --git a/web/cordova/cordova-js-src/android/nativeapiprovider.js b/web/cordova/cordova-js-src/android/nativeapiprovider.js deleted file mode 100644 index 2e9aa67b..00000000 --- a/web/cordova/cordova-js-src/android/nativeapiprovider.js +++ /dev/null @@ -1,36 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. -*/ - -/** - * Exports the ExposedJsApi.java object if available, otherwise exports the PromptBasedNativeApi. - */ - -var nativeApi = this._cordovaNative || require('cordova/android/promptbasednativeapi'); -var currentApi = nativeApi; - -module.exports = { - get: function() { return currentApi; }, - setPreferPrompt: function(value) { - currentApi = value ? require('cordova/android/promptbasednativeapi') : nativeApi; - }, - // Used only by tests. - set: function(value) { - currentApi = value; - } -}; diff --git a/web/cordova/cordova-js-src/android/promptbasednativeapi.js b/web/cordova/cordova-js-src/android/promptbasednativeapi.js deleted file mode 100644 index f7fb6bc7..00000000 --- a/web/cordova/cordova-js-src/android/promptbasednativeapi.js +++ /dev/null @@ -1,35 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. -*/ - -/** - * Implements the API of ExposedJsApi.java, but uses prompt() to communicate. - * This is used pre-JellyBean, where addJavascriptInterface() is disabled. - */ - -module.exports = { - exec: function(bridgeSecret, service, action, callbackId, argsJson) { - return prompt(argsJson, 'gap:'+JSON.stringify([bridgeSecret, service, action, callbackId])); - }, - setNativeToJsBridgeMode: function(bridgeSecret, value) { - prompt(value, 'gap_bridge_mode:' + bridgeSecret); - }, - retrieveJsMessages: function(bridgeSecret, fromOnlineEvent) { - return prompt(+fromOnlineEvent, 'gap_poll:' + bridgeSecret); - } -}; diff --git a/web/cordova/cordova-js-src/exec.js b/web/cordova/cordova-js-src/exec.js deleted file mode 100644 index fa8b41be..00000000 --- a/web/cordova/cordova-js-src/exec.js +++ /dev/null @@ -1,283 +0,0 @@ -/* - * - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - * -*/ - -/** - * Execute a cordova command. It is up to the native side whether this action - * is synchronous or asynchronous. The native side can return: - * Synchronous: PluginResult object as a JSON string - * Asynchronous: Empty string "" - * If async, the native side will cordova.callbackSuccess or cordova.callbackError, - * depending upon the result of the action. - * - * @param {Function} success The success callback - * @param {Function} fail The fail callback - * @param {String} service The name of the service to use - * @param {String} action Action to be run in cordova - * @param {String[]} [args] Zero or more arguments to pass to the method - */ -var cordova = require('cordova'), - nativeApiProvider = require('cordova/android/nativeapiprovider'), - utils = require('cordova/utils'), - base64 = require('cordova/base64'), - channel = require('cordova/channel'), - jsToNativeModes = { - PROMPT: 0, - JS_OBJECT: 1 - }, - nativeToJsModes = { - // Polls for messages using the JS->Native bridge. - POLLING: 0, - // For LOAD_URL to be viable, it would need to have a work-around for - // the bug where the soft-keyboard gets dismissed when a message is sent. - LOAD_URL: 1, - // For the ONLINE_EVENT to be viable, it would need to intercept all event - // listeners (both through addEventListener and window.ononline) as well - // as set the navigator property itself. - ONLINE_EVENT: 2 - }, - jsToNativeBridgeMode, // Set lazily. - nativeToJsBridgeMode = nativeToJsModes.ONLINE_EVENT, - pollEnabled = false, - bridgeSecret = -1; - -var messagesFromNative = []; -var isProcessing = false; -var resolvedPromise = typeof Promise == 'undefined' ? null : Promise.resolve(); -var nextTick = resolvedPromise ? function(fn) { resolvedPromise.then(fn); } : function(fn) { setTimeout(fn); }; - -function androidExec(success, fail, service, action, args) { - if (bridgeSecret < 0) { - // If we ever catch this firing, we'll need to queue up exec()s - // and fire them once we get a secret. For now, I don't think - // it's possible for exec() to be called since plugins are parsed but - // not run until until after onNativeReady. - throw new Error('exec() called without bridgeSecret'); - } - // Set default bridge modes if they have not already been set. - // By default, we use the failsafe, since addJavascriptInterface breaks too often - if (jsToNativeBridgeMode === undefined) { - androidExec.setJsToNativeBridgeMode(jsToNativeModes.JS_OBJECT); - } - - // Process any ArrayBuffers in the args into a string. - for (var i = 0; i < args.length; i++) { - if (utils.typeName(args[i]) == 'ArrayBuffer') { - args[i] = base64.fromArrayBuffer(args[i]); - } - } - - var callbackId = service + cordova.callbackId++, - argsJson = JSON.stringify(args); - - if (success || fail) { - cordova.callbacks[callbackId] = {success:success, fail:fail}; - } - - var msgs = nativeApiProvider.get().exec(bridgeSecret, service, action, callbackId, argsJson); - // If argsJson was received by Java as null, try again with the PROMPT bridge mode. - // This happens in rare circumstances, such as when certain Unicode characters are passed over the bridge on a Galaxy S2. See CB-2666. - if (jsToNativeBridgeMode == jsToNativeModes.JS_OBJECT && msgs === "@Null arguments.") { - androidExec.setJsToNativeBridgeMode(jsToNativeModes.PROMPT); - androidExec(success, fail, service, action, args); - androidExec.setJsToNativeBridgeMode(jsToNativeModes.JS_OBJECT); - } else if (msgs) { - messagesFromNative.push(msgs); - // Always process async to avoid exceptions messing up stack. - nextTick(processMessages); - } -} - -androidExec.init = function() { - bridgeSecret = +prompt('', 'gap_init:' + nativeToJsBridgeMode); - channel.onNativeReady.fire(); -}; - -function pollOnceFromOnlineEvent() { - pollOnce(true); -} - -function pollOnce(opt_fromOnlineEvent) { - if (bridgeSecret < 0) { - // This can happen when the NativeToJsMessageQueue resets the online state on page transitions. - // We know there's nothing to retrieve, so no need to poll. - return; - } - var msgs = nativeApiProvider.get().retrieveJsMessages(bridgeSecret, !!opt_fromOnlineEvent); - if (msgs) { - messagesFromNative.push(msgs); - // Process sync since we know we're already top-of-stack. - processMessages(); - } -} - -function pollingTimerFunc() { - if (pollEnabled) { - pollOnce(); - setTimeout(pollingTimerFunc, 50); - } -} - -function hookOnlineApis() { - function proxyEvent(e) { - cordova.fireWindowEvent(e.type); - } - // The network module takes care of firing online and offline events. - // It currently fires them only on document though, so we bridge them - // to window here (while first listening for exec()-releated online/offline - // events). - window.addEventListener('online', pollOnceFromOnlineEvent, false); - window.addEventListener('offline', pollOnceFromOnlineEvent, false); - cordova.addWindowEventHandler('online'); - cordova.addWindowEventHandler('offline'); - document.addEventListener('online', proxyEvent, false); - document.addEventListener('offline', proxyEvent, false); -} - -hookOnlineApis(); - -androidExec.jsToNativeModes = jsToNativeModes; -androidExec.nativeToJsModes = nativeToJsModes; - -androidExec.setJsToNativeBridgeMode = function(mode) { - if (mode == jsToNativeModes.JS_OBJECT && !window._cordovaNative) { - mode = jsToNativeModes.PROMPT; - } - nativeApiProvider.setPreferPrompt(mode == jsToNativeModes.PROMPT); - jsToNativeBridgeMode = mode; -}; - -androidExec.setNativeToJsBridgeMode = function(mode) { - if (mode == nativeToJsBridgeMode) { - return; - } - if (nativeToJsBridgeMode == nativeToJsModes.POLLING) { - pollEnabled = false; - } - - nativeToJsBridgeMode = mode; - // Tell the native side to switch modes. - // Otherwise, it will be set by androidExec.init() - if (bridgeSecret >= 0) { - nativeApiProvider.get().setNativeToJsBridgeMode(bridgeSecret, mode); - } - - if (mode == nativeToJsModes.POLLING) { - pollEnabled = true; - setTimeout(pollingTimerFunc, 1); - } -}; - -function buildPayload(payload, message) { - var payloadKind = message.charAt(0); - if (payloadKind == 's') { - payload.push(message.slice(1)); - } else if (payloadKind == 't') { - payload.push(true); - } else if (payloadKind == 'f') { - payload.push(false); - } else if (payloadKind == 'N') { - payload.push(null); - } else if (payloadKind == 'n') { - payload.push(+message.slice(1)); - } else if (payloadKind == 'A') { - var data = message.slice(1); - payload.push(base64.toArrayBuffer(data)); - } else if (payloadKind == 'S') { - payload.push(window.atob(message.slice(1))); - } else if (payloadKind == 'M') { - var multipartMessages = message.slice(1); - while (multipartMessages !== "") { - var spaceIdx = multipartMessages.indexOf(' '); - var msgLen = +multipartMessages.slice(0, spaceIdx); - var multipartMessage = multipartMessages.substr(spaceIdx + 1, msgLen); - multipartMessages = multipartMessages.slice(spaceIdx + msgLen + 1); - buildPayload(payload, multipartMessage); - } - } else { - payload.push(JSON.parse(message)); - } -} - -// Processes a single message, as encoded by NativeToJsMessageQueue.java. -function processMessage(message) { - var firstChar = message.charAt(0); - if (firstChar == 'J') { - // This is deprecated on the .java side. It doesn't work with CSP enabled. - eval(message.slice(1)); - } else if (firstChar == 'S' || firstChar == 'F') { - var success = firstChar == 'S'; - var keepCallback = message.charAt(1) == '1'; - var spaceIdx = message.indexOf(' ', 2); - var status = +message.slice(2, spaceIdx); - var nextSpaceIdx = message.indexOf(' ', spaceIdx + 1); - var callbackId = message.slice(spaceIdx + 1, nextSpaceIdx); - var payloadMessage = message.slice(nextSpaceIdx + 1); - var payload = []; - buildPayload(payload, payloadMessage); - cordova.callbackFromNative(callbackId, success, status, payload, keepCallback); - } else { - console.log("processMessage failed: invalid message: " + JSON.stringify(message)); - } -} - -function processMessages() { - // Check for the reentrant case. - if (isProcessing) { - return; - } - if (messagesFromNative.length === 0) { - return; - } - isProcessing = true; - try { - var msg = popMessageFromQueue(); - // The Java side can send a * message to indicate that it - // still has messages waiting to be retrieved. - if (msg == '*' && messagesFromNative.length === 0) { - nextTick(pollOnce); - return; - } - processMessage(msg); - } finally { - isProcessing = false; - if (messagesFromNative.length > 0) { - nextTick(processMessages); - } - } -} - -function popMessageFromQueue() { - var messageBatch = messagesFromNative.shift(); - if (messageBatch == '*') { - return '*'; - } - - var spaceIdx = messageBatch.indexOf(' '); - var msgLen = +messageBatch.slice(0, spaceIdx); - var message = messageBatch.substr(spaceIdx + 1, msgLen); - messageBatch = messageBatch.slice(spaceIdx + msgLen + 1); - if (messageBatch) { - messagesFromNative.unshift(messageBatch); - } - return message; -} - -module.exports = androidExec; diff --git a/web/cordova/cordova-js-src/platform.js b/web/cordova/cordova-js-src/platform.js deleted file mode 100644 index 2bfd0247..00000000 --- a/web/cordova/cordova-js-src/platform.js +++ /dev/null @@ -1,125 +0,0 @@ -/* - * - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - * -*/ - -// The last resume event that was received that had the result of a plugin call. -var lastResumeEvent = null; - -module.exports = { - id: 'android', - bootstrap: function() { - var channel = require('cordova/channel'), - cordova = require('cordova'), - exec = require('cordova/exec'), - modulemapper = require('cordova/modulemapper'); - - // Get the shared secret needed to use the bridge. - exec.init(); - - // TODO: Extract this as a proper plugin. - modulemapper.clobbers('cordova/plugin/android/app', 'navigator.app'); - - var APP_PLUGIN_NAME = Number(cordova.platformVersion.split('.')[0]) >= 4 ? 'CoreAndroid' : 'App'; - - // Inject a listener for the backbutton on the document. - var backButtonChannel = cordova.addDocumentEventHandler('backbutton'); - backButtonChannel.onHasSubscribersChange = function() { - // If we just attached the first handler or detached the last handler, - // let native know we need to override the back button. - exec(null, null, APP_PLUGIN_NAME, "overrideBackbutton", [this.numHandlers == 1]); - }; - - // Add hardware MENU and SEARCH button handlers - cordova.addDocumentEventHandler('menubutton'); - cordova.addDocumentEventHandler('searchbutton'); - - function bindButtonChannel(buttonName) { - // generic button bind used for volumeup/volumedown buttons - var volumeButtonChannel = cordova.addDocumentEventHandler(buttonName + 'button'); - volumeButtonChannel.onHasSubscribersChange = function() { - exec(null, null, APP_PLUGIN_NAME, "overrideButton", [buttonName, this.numHandlers == 1]); - }; - } - // Inject a listener for the volume buttons on the document. - bindButtonChannel('volumeup'); - bindButtonChannel('volumedown'); - - // The resume event is not "sticky", but it is possible that the event - // will contain the result of a plugin call. We need to ensure that the - // plugin result is delivered even after the event is fired (CB-10498) - var cordovaAddEventListener = document.addEventListener; - - document.addEventListener = function(evt, handler, capture) { - cordovaAddEventListener(evt, handler, capture); - - if (evt === 'resume' && lastResumeEvent) { - handler(lastResumeEvent); - } - }; - - // Let native code know we are all done on the JS side. - // Native code will then un-hide the WebView. - channel.onCordovaReady.subscribe(function() { - exec(onMessageFromNative, null, APP_PLUGIN_NAME, 'messageChannel', []); - exec(null, null, APP_PLUGIN_NAME, "show", []); - }); - } -}; - -function onMessageFromNative(msg) { - var cordova = require('cordova'); - var action = msg.action; - - switch (action) - { - // Button events - case 'backbutton': - case 'menubutton': - case 'searchbutton': - // App life cycle events - case 'pause': - // Volume events - case 'volumedownbutton': - case 'volumeupbutton': - cordova.fireDocumentEvent(action); - break; - case 'resume': - if(arguments.length > 1 && msg.pendingResult) { - if(arguments.length === 2) { - msg.pendingResult.result = arguments[1]; - } else { - // The plugin returned a multipart message - var res = []; - for(var i = 1; i < arguments.length; i++) { - res.push(arguments[i]); - } - msg.pendingResult.result = res; - } - - // Save the plugin result so that it can be delivered to the js - // even if they miss the initial firing of the event - lastResumeEvent = msg; - } - cordova.fireDocumentEvent(action, msg); - break; - default: - throw new Error('Unknown event action ' + action); - } -} diff --git a/web/cordova/cordova-js-src/plugin/android/app.js b/web/cordova/cordova-js-src/plugin/android/app.js deleted file mode 100644 index 22cf96e8..00000000 --- a/web/cordova/cordova-js-src/plugin/android/app.js +++ /dev/null @@ -1,108 +0,0 @@ -/* - * - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - * -*/ - -var exec = require('cordova/exec'); -var APP_PLUGIN_NAME = Number(require('cordova').platformVersion.split('.')[0]) >= 4 ? 'CoreAndroid' : 'App'; - -module.exports = { - /** - * Clear the resource cache. - */ - clearCache:function() { - exec(null, null, APP_PLUGIN_NAME, "clearCache", []); - }, - - /** - * Load the url into the webview or into new browser instance. - * - * @param url The URL to load - * @param props Properties that can be passed in to the activity: - * wait: int => wait msec before loading URL - * loadingDialog: "Title,Message" => display a native loading dialog - * loadUrlTimeoutValue: int => time in msec to wait before triggering a timeout error - * clearHistory: boolean => clear webview history (default=false) - * openExternal: boolean => open in a new browser (default=false) - * - * Example: - * navigator.app.loadUrl("http://server/myapp/index.html", {wait:2000, loadingDialog:"Wait,Loading App", loadUrlTimeoutValue: 60000}); - */ - loadUrl:function(url, props) { - exec(null, null, APP_PLUGIN_NAME, "loadUrl", [url, props]); - }, - - /** - * Cancel loadUrl that is waiting to be loaded. - */ - cancelLoadUrl:function() { - exec(null, null, APP_PLUGIN_NAME, "cancelLoadUrl", []); - }, - - /** - * Clear web history in this web view. - * Instead of BACK button loading the previous web page, it will exit the app. - */ - clearHistory:function() { - exec(null, null, APP_PLUGIN_NAME, "clearHistory", []); - }, - - /** - * Go to previous page displayed. - * This is the same as pressing the backbutton on Android device. - */ - backHistory:function() { - exec(null, null, APP_PLUGIN_NAME, "backHistory", []); - }, - - /** - * Override the default behavior of the Android back button. - * If overridden, when the back button is pressed, the "backKeyDown" JavaScript event will be fired. - * - * Note: The user should not have to call this method. Instead, when the user - * registers for the "backbutton" event, this is automatically done. - * - * @param override T=override, F=cancel override - */ - overrideBackbutton:function(override) { - exec(null, null, APP_PLUGIN_NAME, "overrideBackbutton", [override]); - }, - - /** - * Override the default behavior of the Android volume button. - * If overridden, when the volume button is pressed, the "volume[up|down]button" - * JavaScript event will be fired. - * - * Note: The user should not have to call this method. Instead, when the user - * registers for the "volume[up|down]button" event, this is automatically done. - * - * @param button volumeup, volumedown - * @param override T=override, F=cancel override - */ - overrideButton:function(button, override) { - exec(null, null, APP_PLUGIN_NAME, "overrideButton", [button, override]); - }, - - /** - * Exit and terminate the application. - */ - exitApp:function() { - return exec(null, null, APP_PLUGIN_NAME, "exitApp", []); - } -}; diff --git a/web/cordova/cordova.js b/web/cordova/cordova.js deleted file mode 100644 index 065914c2..00000000 --- a/web/cordova/cordova.js +++ /dev/null @@ -1,2087 +0,0 @@ -// Platform: cordova-android -// cordova-js 6.1.0 -/* - Licensed to the Apache Software Foundation (ASF) under one - or more contributor license agreements. See the NOTICE file - distributed with this work for additional information - regarding copyright ownership. The ASF licenses this file - to you under the Apache License, Version 2.0 (the - "License"); you may not use this file except in compliance - with the License. You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, - software distributed under the License is distributed on an - "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - KIND, either express or implied. See the License for the - specific language governing permissions and limitations - under the License. -*/ - -if (window["__os__"] != undefined) { - (function () { - var PLATFORM_VERSION_BUILD_LABEL = "10.1.2"; - // file: src/scripts/require.js - var require; - var define; - - (function () { - var modules = {}; - // Stack of moduleIds currently being built. - var requireStack = []; - // Map of module ID -> index into requireStack of modules currently being built. - var inProgressModules = {}; - var SEPARATOR = "."; - - function build(module) { - var factory = module.factory; - var localRequire = function (id) { - var resultantId = id; - // Its a relative path, so lop off the last portion and add the id (minus "./") - if (id.charAt(0) === ".") { - resultantId = - module.id.slice(0, module.id.lastIndexOf(SEPARATOR)) + - SEPARATOR + - id.slice(2); - } - return require(resultantId); - }; - module.exports = {}; - delete module.factory; - factory(localRequire, module.exports, module); - return module.exports; - } - - require = function (id) { - if (!modules[id]) { - throw new Error("module " + id + " not found"); - } else if (id in inProgressModules) { - var cycle = - requireStack.slice(inProgressModules[id]).join("->") + "->" + id; - throw new Error("Cycle in require graph: " + cycle); - } - if (modules[id].factory) { - try { - inProgressModules[id] = requireStack.length; - requireStack.push(id); - return build(modules[id]); - } finally { - delete inProgressModules[id]; - requireStack.pop(); - } - } - return modules[id].exports; - }; - - define = function (id, factory) { - if (Object.prototype.hasOwnProperty.call(modules, id)) { - throw new Error("module " + id + " already defined"); - } - - modules[id] = { - id: id, - factory: factory, - }; - }; - - define.remove = function (id) { - delete modules[id]; - }; - - define.moduleMap = modules; - })(); - - // Export for use in node - if (typeof module === "object" && typeof require === "function") { - module.exports.require = require; - module.exports.define = define; - } - - // file: src/cordova.js - define("cordova", function (require, exports, module) { - // Workaround for Windows 10 in hosted environment case - // http://www.w3.org/html/wg/drafts/html/master/browsers.html#named-access-on-the-window-object - if (window.cordova && !(window.cordova instanceof HTMLElement)) { - throw new Error("cordova already defined"); - } - - var channel = require("cordova/channel"); - var platform = require("cordova/platform"); - - /** - * Intercept calls to addEventListener + removeEventListener and handle deviceready, - * resume, and pause events. - */ - var m_document_addEventListener = document.addEventListener; - var m_document_removeEventListener = document.removeEventListener; - var m_window_addEventListener = window.addEventListener; - var m_window_removeEventListener = window.removeEventListener; - - /** - * Houses custom event handlers to intercept on document + window event listeners. - */ - var documentEventHandlers = {}; - var windowEventHandlers = {}; - - document.addEventListener = function (evt, handler, capture) { - var e = evt.toLowerCase(); - if (typeof documentEventHandlers[e] !== "undefined") { - documentEventHandlers[e].subscribe(handler); - } else { - m_document_addEventListener.call(document, evt, handler, capture); - } - }; - - window.addEventListener = function (evt, handler, capture) { - var e = evt.toLowerCase(); - if (typeof windowEventHandlers[e] !== "undefined") { - windowEventHandlers[e].subscribe(handler); - } else { - m_window_addEventListener.call(window, evt, handler, capture); - } - }; - - document.removeEventListener = function (evt, handler, capture) { - var e = evt.toLowerCase(); - // If unsubscribing from an event that is handled by a plugin - if (typeof documentEventHandlers[e] !== "undefined") { - documentEventHandlers[e].unsubscribe(handler); - } else { - m_document_removeEventListener.call(document, evt, handler, capture); - } - }; - - window.removeEventListener = function (evt, handler, capture) { - var e = evt.toLowerCase(); - // If unsubscribing from an event that is handled by a plugin - if (typeof windowEventHandlers[e] !== "undefined") { - windowEventHandlers[e].unsubscribe(handler); - } else { - m_window_removeEventListener.call(window, evt, handler, capture); - } - }; - - function createEvent(type, data) { - var event = document.createEvent("Events"); - event.initEvent(type, false, false); - if (data) { - for (var i in data) { - if (Object.prototype.hasOwnProperty.call(data, i)) { - event[i] = data[i]; - } - } - } - return event; - } - - var cordova = { - define: define, - require: require, - version: PLATFORM_VERSION_BUILD_LABEL, - platformVersion: PLATFORM_VERSION_BUILD_LABEL, - platformId: platform.id, - - /** - * Methods to add/remove your own addEventListener hijacking on document + window. - */ - addWindowEventHandler: function (event) { - return (windowEventHandlers[event] = channel.create(event)); - }, - addStickyDocumentEventHandler: function (event) { - return (documentEventHandlers[event] = channel.createSticky(event)); - }, - addDocumentEventHandler: function (event) { - return (documentEventHandlers[event] = channel.create(event)); - }, - removeWindowEventHandler: function (event) { - delete windowEventHandlers[event]; - }, - removeDocumentEventHandler: function (event) { - delete documentEventHandlers[event]; - }, - - /** - * Retrieve original event handlers that were replaced by Cordova - * - * @return object - */ - getOriginalHandlers: function () { - return { - document: { - addEventListener: m_document_addEventListener, - removeEventListener: m_document_removeEventListener, - }, - window: { - addEventListener: m_window_addEventListener, - removeEventListener: m_window_removeEventListener, - }, - }; - }, - - /** - * Method to fire event from native code - * bNoDetach is required for events which cause an exception which needs to be caught in native code - */ - fireDocumentEvent: function (type, data, bNoDetach) { - var evt = createEvent(type, data); - if (typeof documentEventHandlers[type] !== "undefined") { - if (bNoDetach) { - documentEventHandlers[type].fire(evt); - } else { - setTimeout(function () { - // Fire deviceready on listeners that were registered before cordova.js was loaded. - if (type === "deviceready") { - document.dispatchEvent(evt); - } - documentEventHandlers[type].fire(evt); - }, 0); - } - } else { - document.dispatchEvent(evt); - } - }, - - fireWindowEvent: function (type, data) { - var evt = createEvent(type, data); - if (typeof windowEventHandlers[type] !== "undefined") { - setTimeout(function () { - windowEventHandlers[type].fire(evt); - }, 0); - } else { - window.dispatchEvent(evt); - } - }, - - /** - * Plugin callback mechanism. - */ - // Randomize the starting callbackId to avoid collisions after refreshing or navigating. - // This way, it's very unlikely that any new callback would get the same callbackId as an old callback. - callbackId: Math.floor(Math.random() * 2000000000), - callbacks: {}, - callbackStatus: { - NO_RESULT: 0, - OK: 1, - CLASS_NOT_FOUND_EXCEPTION: 2, - ILLEGAL_ACCESS_EXCEPTION: 3, - INSTANTIATION_EXCEPTION: 4, - MALFORMED_URL_EXCEPTION: 5, - IO_EXCEPTION: 6, - INVALID_ACTION: 7, - JSON_EXCEPTION: 8, - ERROR: 9, - }, - - /** - * Called by native code when returning successful result from an action. - */ - callbackSuccess: function (callbackId, args) { - cordova.callbackFromNative( - callbackId, - true, - args.status, - [args.message], - args.keepCallback - ); - }, - - /** - * Called by native code when returning error result from an action. - */ - callbackError: function (callbackId, args) { - // TODO: Deprecate callbackSuccess and callbackError in favour of callbackFromNative. - // Derive success from status. - cordova.callbackFromNative( - callbackId, - false, - args.status, - [args.message], - args.keepCallback - ); - }, - - /** - * Called by native code when returning the result from an action. - */ - callbackFromNative: function ( - callbackId, - isSuccess, - status, - args, - keepCallback - ) { - try { - var callback = cordova.callbacks[callbackId]; - if (callback) { - if (isSuccess && status === cordova.callbackStatus.OK) { - callback.success && callback.success.apply(null, args); - } else if (!isSuccess) { - callback.fail && callback.fail.apply(null, args); - } - /* - else - Note, this case is intentionally not caught. - this can happen if isSuccess is true, but callbackStatus is NO_RESULT - which is used to remove a callback from the list without calling the callbacks - typically keepCallback is false in this case - */ - // Clear callback if not expecting any more results - if (!keepCallback) { - delete cordova.callbacks[callbackId]; - } - } - } catch (err) { - var msg = - "Error in " + - (isSuccess ? "Success" : "Error") + - " callbackId: " + - callbackId + - " : " + - err; - cordova.fireWindowEvent("cordovacallbackerror", { - message: msg, - error: err, - }); - throw err; - } - }, - - addConstructor: function (func) { - channel.onCordovaReady.subscribe(function () { - try { - func(); - } catch (e) { - console.log("Failed to run constructor: " + e); - } - }); - }, - }; - - module.exports = cordova; - }); - - // file: ../../cordova-js-src/android/nativeapiprovider.js - define( - "cordova/android/nativeapiprovider", - function (require, exports, module) { - /** - * Exports the ExposedJsApi.java object if available, otherwise exports the PromptBasedNativeApi. - */ - - var nativeApi = - this._cordovaNative || - require("cordova/android/promptbasednativeapi"); - var currentApi = nativeApi; - - module.exports = { - get: function () { - return currentApi; - }, - setPreferPrompt: function (value) { - currentApi = value - ? require("cordova/android/promptbasednativeapi") - : nativeApi; - }, - // Used only by tests. - set: function (value) { - currentApi = value; - }, - }; - } - ); - - // file: ../../cordova-js-src/android/promptbasednativeapi.js - define( - "cordova/android/promptbasednativeapi", - function (require, exports, module) { - /** - * Implements the API of ExposedJsApi.java, but uses prompt() to communicate. - * This is used pre-JellyBean, where addJavascriptInterface() is disabled. - */ - - module.exports = { - exec: function (bridgeSecret, service, action, callbackId, argsJson) { - return prompt( - argsJson, - "gap:" + - JSON.stringify([bridgeSecret, service, action, callbackId]) - ); - }, - setNativeToJsBridgeMode: function (bridgeSecret, value) { - prompt(value, "gap_bridge_mode:" + bridgeSecret); - }, - retrieveJsMessages: function (bridgeSecret, fromOnlineEvent) { - return prompt(+fromOnlineEvent, "gap_poll:" + bridgeSecret); - }, - }; - } - ); - - // file: src/common/argscheck.js - define("cordova/argscheck", function (require, exports, module) { - var utils = require("cordova/utils"); - - var moduleExports = module.exports; - - var typeMap = { - A: "Array", - D: "Date", - N: "Number", - S: "String", - F: "Function", - O: "Object", - }; - - function extractParamName(callee, argIndex) { - return /\(\s*([^)]*?)\s*\)/.exec(callee)[1].split(/\s*,\s*/)[argIndex]; - } - - /** - * Checks the given arguments' types and throws if they are not as expected. - * - * `spec` is a string where each character stands for the required type of the - * argument at the same position. In other words: the character at `spec[i]` - * specifies the required type for `args[i]`. The characters in `spec` are the - * first letter of the required type's name. The supported types are: - * - * Array, Date, Number, String, Function, Object - * - * Lowercase characters specify arguments that must not be `null` or `undefined` - * while uppercase characters allow those values to be passed. - * - * Finally, `*` can be used to allow any type at the corresponding position. - * - * @example - * function foo (arr, opts) { - * // require `arr` to be an Array and `opts` an Object, null or undefined - * checkArgs('aO', 'my.package.foo', arguments); - * // ... - * } - * @param {String} spec - the type specification for `args` as described above - * @param {String} functionName - full name of the callee. - * Used in the error message - * @param {Array|arguments} args - the arguments to be checked against `spec` - * @param {Function} [opt_callee=args.callee] - the recipient of `args`. - * Used to extract parameter names for the error message - * @throws {TypeError} if args do not satisfy spec - */ - function checkArgs(spec, functionName, args, opt_callee) { - if (!moduleExports.enableChecks) { - return; - } - var errMsg = null; - var typeName; - for (var i = 0; i < spec.length; ++i) { - var c = spec.charAt(i); - var cUpper = c.toUpperCase(); - var arg = args[i]; - // Asterix means allow anything. - if (c === "*") { - continue; - } - typeName = utils.typeName(arg); - if ((arg === null || arg === undefined) && c === cUpper) { - continue; - } - if (typeName !== typeMap[cUpper]) { - errMsg = "Expected " + typeMap[cUpper]; - break; - } - } - if (errMsg) { - errMsg += ", but got " + typeName + "."; - errMsg = - 'Wrong type for parameter "' + - extractParamName(opt_callee || args.callee, i) + - '" of ' + - functionName + - ": " + - errMsg; - // Don't log when running unit tests. - if (typeof jasmine === "undefined") { - console.error(errMsg); - } - throw TypeError(errMsg); - } - } - - function getValue(value, defaultValue) { - return value === undefined ? defaultValue : value; - } - - moduleExports.checkArgs = checkArgs; - moduleExports.getValue = getValue; - moduleExports.enableChecks = true; - }); - - // file: src/common/base64.js - define("cordova/base64", function (require, exports, module) { - var base64 = exports; - - base64.fromArrayBuffer = function (arrayBuffer) { - var array = new Uint8Array(arrayBuffer); - return uint8ToBase64(array); - }; - - base64.toArrayBuffer = function (str) { - var decodedStr = atob(str); - var arrayBuffer = new ArrayBuffer(decodedStr.length); - var array = new Uint8Array(arrayBuffer); - for (var i = 0, len = decodedStr.length; i < len; i++) { - array[i] = decodedStr.charCodeAt(i); - } - return arrayBuffer; - }; - - // ------------------------------------------------------------------------------ - - /* This code is based on the performance tests at http://jsperf.com/b64tests - * This 12-bit-at-a-time algorithm was the best performing version on all - * platforms tested. - */ - - var b64_6bit = - "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; - var b64_12bit; - - var b64_12bitTable = function () { - b64_12bit = []; - for (var i = 0; i < 64; i++) { - for (var j = 0; j < 64; j++) { - b64_12bit[i * 64 + j] = b64_6bit[i] + b64_6bit[j]; - } - } - b64_12bitTable = function () { - return b64_12bit; - }; - return b64_12bit; - }; - - function uint8ToBase64(rawData) { - var numBytes = rawData.byteLength; - var output = ""; - var segment; - var table = b64_12bitTable(); - for (var i = 0; i < numBytes - 2; i += 3) { - segment = (rawData[i] << 16) + (rawData[i + 1] << 8) + rawData[i + 2]; - output += table[segment >> 12]; - output += table[segment & 0xfff]; - } - if (numBytes - i === 2) { - segment = (rawData[i] << 16) + (rawData[i + 1] << 8); - output += table[segment >> 12]; - output += b64_6bit[(segment & 0xfff) >> 6]; - output += "="; - } else if (numBytes - i === 1) { - segment = rawData[i] << 16; - output += table[segment >> 12]; - output += "=="; - } - return output; - } - }); - - // file: src/common/builder.js - define("cordova/builder", function (require, exports, module) { - var utils = require("cordova/utils"); - - function each(objects, func, context) { - for (var prop in objects) { - if (Object.prototype.hasOwnProperty.call(objects, prop)) { - func.apply(context, [objects[prop], prop]); - } - } - } - - function clobber(obj, key, value) { - var needsProperty = false; - try { - obj[key] = value; - } catch (e) { - needsProperty = true; - } - // Getters can only be overridden by getters. - if (needsProperty || obj[key] !== value) { - utils.defineGetter(obj, key, function () { - return value; - }); - } - } - - function assignOrWrapInDeprecateGetter(obj, key, value, message) { - if (message) { - utils.defineGetter(obj, key, function () { - console.log(message); - delete obj[key]; - clobber(obj, key, value); - return value; - }); - } else { - clobber(obj, key, value); - } - } - - function include(parent, objects, clobber, merge) { - each(objects, function (obj, key) { - try { - var result = obj.path ? require(obj.path) : {}; - - if (clobber) { - // Clobber if it doesn't exist. - if (typeof parent[key] === "undefined") { - assignOrWrapInDeprecateGetter( - parent, - key, - result, - obj.deprecated - ); - } else if (typeof obj.path !== "undefined") { - // If merging, merge properties onto parent, otherwise, clobber. - if (merge) { - recursiveMerge(parent[key], result); - } else { - assignOrWrapInDeprecateGetter( - parent, - key, - result, - obj.deprecated - ); - } - } - result = parent[key]; - } else { - // Overwrite if not currently defined. - if (typeof parent[key] === "undefined") { - assignOrWrapInDeprecateGetter( - parent, - key, - result, - obj.deprecated - ); - } else { - // Set result to what already exists, so we can build children into it if they exist. - result = parent[key]; - } - } - - if (obj.children) { - include(result, obj.children, clobber, merge); - } - } catch (e) { - utils.alert( - "Exception building Cordova JS globals: " + - e + - ' for key "' + - key + - '"' - ); - } - }); - } - - /** - * Merge properties from one object onto another recursively. Properties from - * the src object will overwrite existing target property. - * - * @param target Object to merge properties into. - * @param src Object to merge properties from. - */ - function recursiveMerge(target, src) { - for (var prop in src) { - if (Object.prototype.hasOwnProperty.call(src, prop)) { - if (target.prototype && target.prototype.constructor === target) { - // If the target object is a constructor override off prototype. - clobber(target.prototype, prop, src[prop]); - } else { - if ( - typeof src[prop] === "object" && - typeof target[prop] === "object" - ) { - recursiveMerge(target[prop], src[prop]); - } else { - clobber(target, prop, src[prop]); - } - } - } - } - } - - exports.buildIntoButDoNotClobber = function (objects, target) { - include(target, objects, false, false); - }; - exports.buildIntoAndClobber = function (objects, target) { - include(target, objects, true, false); - }; - exports.buildIntoAndMerge = function (objects, target) { - include(target, objects, true, true); - }; - exports.recursiveMerge = recursiveMerge; - exports.assignOrWrapInDeprecateGetter = assignOrWrapInDeprecateGetter; - }); - - // file: src/common/channel.js - define("cordova/channel", function (require, exports, module) { - var utils = require("cordova/utils"); - var nextGuid = 1; - - /** - * Custom pub-sub "channel" that can have functions subscribed to it - * This object is used to define and control firing of events for - * cordova initialization, as well as for custom events thereafter. - * - * The order of events during page load and Cordova startup is as follows: - * - * onDOMContentLoaded* Internal event that is received when the web page is loaded and parsed. - * onNativeReady* Internal event that indicates the Cordova native side is ready. - * onCordovaReady* Internal event fired when all Cordova JavaScript objects have been created. - * onDeviceReady* User event fired to indicate that Cordova is ready - * onResume User event fired to indicate a start/resume lifecycle event - * onPause User event fired to indicate a pause lifecycle event - * - * The events marked with an * are sticky. Once they have fired, they will stay in the fired state. - * All listeners that subscribe after the event is fired will be executed right away. - * - * The only Cordova events that user code should register for are: - * deviceready Cordova native code is initialized and Cordova APIs can be called from JavaScript - * pause App has moved to background - * resume App has returned to foreground - * - * Listeners can be registered as: - * document.addEventListener("deviceready", myDeviceReadyListener, false); - * document.addEventListener("resume", myResumeListener, false); - * document.addEventListener("pause", myPauseListener, false); - * - * The DOM lifecycle events should be used for saving and restoring state - * window.onload - * window.onunload - * - */ - - /** - * Channel - * @constructor - * @param type String the channel name - */ - var Channel = function (type, sticky) { - this.type = type; - // Map of guid -> function. - this.handlers = {}; - // 0 = Non-sticky, 1 = Sticky non-fired, 2 = Sticky fired. - this.state = sticky ? 1 : 0; - // Used in sticky mode to remember args passed to fire(). - this.fireArgs = null; - // Used by onHasSubscribersChange to know if there are any listeners. - this.numHandlers = 0; - // Function that is called when the first listener is subscribed, or when - // the last listener is unsubscribed. - this.onHasSubscribersChange = null; - }; - var channel = { - /** - * Calls the provided function only after all of the channels specified - * have been fired. All channels must be sticky channels. - */ - join: function (h, c) { - var len = c.length; - var i = len; - var f = function () { - if (!--i) h(); - }; - for (var j = 0; j < len; j++) { - if (c[j].state === 0) { - throw Error("Can only use join with sticky channels."); - } - c[j].subscribe(f); - } - if (!len) h(); - }, - - create: function (type) { - return (channel[type] = new Channel(type, false)); - }, - createSticky: function (type) { - return (channel[type] = new Channel(type, true)); - }, - - /** - * cordova Channels that must fire before "deviceready" is fired. - */ - deviceReadyChannelsArray: [], - deviceReadyChannelsMap: {}, - - /** - * Indicate that a feature needs to be initialized before it is ready to be used. - * This holds up Cordova's "deviceready" event until the feature has been initialized - * and Cordova.initComplete(feature) is called. - * - * @param feature {String} The unique feature name - */ - waitForInitialization: function (feature) { - if (feature) { - var c = channel[feature] || this.createSticky(feature); - this.deviceReadyChannelsMap[feature] = c; - this.deviceReadyChannelsArray.push(c); - } - }, - - /** - * Indicate that initialization code has completed and the feature is ready to be used. - * - * @param feature {String} The unique feature name - */ - initializationComplete: function (feature) { - var c = this.deviceReadyChannelsMap[feature]; - if (c) { - c.fire(); - } - }, - }; - - function checkSubscriptionArgument(argument) { - if ( - typeof argument !== "function" && - typeof argument.handleEvent !== "function" - ) { - throw new Error( - "Must provide a function or an EventListener object " + - "implementing the handleEvent interface." - ); - } - } - - /** - * Subscribes the given function to the channel. Any time that - * Channel.fire is called so too will the function. - * Optionally specify an execution context for the function - * and a guid that can be used to stop subscribing to the channel. - * Returns the guid. - */ - Channel.prototype.subscribe = function ( - eventListenerOrFunction, - eventListener - ) { - checkSubscriptionArgument(eventListenerOrFunction); - var handleEvent, guid; - - if ( - eventListenerOrFunction && - typeof eventListenerOrFunction === "object" - ) { - // Received an EventListener object implementing the handleEvent interface - handleEvent = eventListenerOrFunction.handleEvent; - eventListener = eventListenerOrFunction; - } else { - // Received a function to handle event - handleEvent = eventListenerOrFunction; - } - - if (this.state === 2) { - handleEvent.apply(eventListener || this, this.fireArgs); - return; - } - - guid = eventListenerOrFunction.observer_guid; - if (typeof eventListener === "object") { - handleEvent = utils.close(eventListener, handleEvent); - } - - if (!guid) { - // First time any channel has seen this subscriber - guid = "" + nextGuid++; - } - handleEvent.observer_guid = guid; - eventListenerOrFunction.observer_guid = guid; - - // Don't add the same handler more than once. - if (!this.handlers[guid]) { - this.handlers[guid] = handleEvent; - this.numHandlers++; - if (this.numHandlers === 1) { - this.onHasSubscribersChange && this.onHasSubscribersChange(); - } - } - }; - - /** - * Unsubscribes the function with the given guid from the channel. - */ - Channel.prototype.unsubscribe = function (eventListenerOrFunction) { - checkSubscriptionArgument(eventListenerOrFunction); - var handleEvent, guid, handler; - - if ( - eventListenerOrFunction && - typeof eventListenerOrFunction === "object" - ) { - // Received an EventListener object implementing the handleEvent interface - handleEvent = eventListenerOrFunction.handleEvent; - } else { - // Received a function to handle event - handleEvent = eventListenerOrFunction; - } - - guid = handleEvent.observer_guid; - handler = this.handlers[guid]; - if (handler) { - delete this.handlers[guid]; - this.numHandlers--; - if (this.numHandlers === 0) { - this.onHasSubscribersChange && this.onHasSubscribersChange(); - } - } - }; - - /** - * Calls all functions subscribed to this channel. - */ - Channel.prototype.fire = function (e) { - var fireArgs = Array.prototype.slice.call(arguments); - // Apply stickiness. - if (this.state === 1) { - this.state = 2; - this.fireArgs = fireArgs; - } - if (this.numHandlers) { - // Copy the values first so that it is safe to modify it from within - // callbacks. - var toCall = []; - for (var item in this.handlers) { - toCall.push(this.handlers[item]); - } - for (var i = 0; i < toCall.length; ++i) { - toCall[i].apply(this, fireArgs); - } - if (this.state === 2 && this.numHandlers) { - this.numHandlers = 0; - this.handlers = {}; - this.onHasSubscribersChange && this.onHasSubscribersChange(); - } - } - }; - - // defining them here so they are ready super fast! - // DOM event that is received when the web page is loaded and parsed. - channel.createSticky("onDOMContentLoaded"); - - // Event to indicate the Cordova native side is ready. - channel.createSticky("onNativeReady"); - - // Event to indicate that all Cordova JavaScript objects have been created - // and it's time to run plugin constructors. - channel.createSticky("onCordovaReady"); - - // Event to indicate that all automatically loaded JS plugins are loaded and ready. - // FIXME remove this - channel.createSticky("onPluginsReady"); - - // Event to indicate that Cordova is ready - channel.createSticky("onDeviceReady"); - - // Event to indicate a resume lifecycle event - channel.create("onResume"); - - // Event to indicate a pause lifecycle event - channel.create("onPause"); - - // Channels that must fire before "deviceready" is fired. - channel.waitForInitialization("onCordovaReady"); - channel.waitForInitialization("onDOMContentLoaded"); - - module.exports = channel; - }); - - // file: ../../cordova-js-src/exec.js - define("cordova/exec", function (require, exports, module) { - /** - * Execute a cordova command. It is up to the native side whether this action - * is synchronous or asynchronous. The native side can return: - * Synchronous: PluginResult object as a JSON string - * Asynchronous: Empty string "" - * If async, the native side will cordova.callbackSuccess or cordova.callbackError, - * depending upon the result of the action. - * - * @param {Function} success The success callback - * @param {Function} fail The fail callback - * @param {String} service The name of the service to use - * @param {String} action Action to be run in cordova - * @param {String[]} [args] Zero or more arguments to pass to the method - */ - var cordova = require("cordova"); - var nativeApiProvider = require("cordova/android/nativeapiprovider"); - var utils = require("cordova/utils"); - var base64 = require("cordova/base64"); - var channel = require("cordova/channel"); - var jsToNativeModes = { - PROMPT: 0, - JS_OBJECT: 1, - }; - var nativeToJsModes = { - // Polls for messages using the JS->Native bridge. - POLLING: 0, - // For LOAD_URL to be viable, it would need to have a work-around for - // the bug where the soft-keyboard gets dismissed when a message is sent. - LOAD_URL: 1, - // For the ONLINE_EVENT to be viable, it would need to intercept all event - // listeners (both through addEventListener and window.ononline) as well - // as set the navigator property itself. - ONLINE_EVENT: 2, - EVAL_BRIDGE: 3, - }; - var jsToNativeBridgeMode; // Set lazily. - var nativeToJsBridgeMode = nativeToJsModes.EVAL_BRIDGE; - var pollEnabled = false; - var bridgeSecret = -1; - - var messagesFromNative = []; - var isProcessing = false; - var resolvedPromise = - typeof Promise === "undefined" ? null : Promise.resolve(); - var nextTick = resolvedPromise - ? function (fn) { - resolvedPromise.then(fn); - } - : function (fn) { - setTimeout(fn); - }; - - function androidExec(success, fail, service, action, args) { - if (bridgeSecret < 0) { - // If we ever catch this firing, we'll need to queue up exec()s - // and fire them once we get a secret. For now, I don't think - // it's possible for exec() to be called since plugins are parsed but - // not run until until after onNativeReady. - throw new Error("exec() called without bridgeSecret"); - } - // Set default bridge modes if they have not already been set. - // By default, we use the failsafe, since addJavascriptInterface breaks too often - if (jsToNativeBridgeMode === undefined) { - androidExec.setJsToNativeBridgeMode(jsToNativeModes.JS_OBJECT); - } - - // If args is not provided, default to an empty array - args = args || []; - - // Process any ArrayBuffers in the args into a string. - for (var i = 0; i < args.length; i++) { - if (utils.typeName(args[i]) === "ArrayBuffer") { - args[i] = base64.fromArrayBuffer(args[i]); - } - } - - var callbackId = service + cordova.callbackId++; - var argsJson = JSON.stringify(args); - if (success || fail) { - cordova.callbacks[callbackId] = { success: success, fail: fail }; - } - - var msgs = nativeApiProvider - .get() - .exec(bridgeSecret, service, action, callbackId, argsJson); - // If argsJson was received by Java as null, try again with the PROMPT bridge mode. - // This happens in rare circumstances, such as when certain Unicode characters are passed over the bridge on a Galaxy S2. See CB-2666. - if ( - jsToNativeBridgeMode === jsToNativeModes.JS_OBJECT && - msgs === "@Null arguments." - ) { - androidExec.setJsToNativeBridgeMode(jsToNativeModes.PROMPT); - androidExec(success, fail, service, action, args); - androidExec.setJsToNativeBridgeMode(jsToNativeModes.JS_OBJECT); - } else if (msgs) { - messagesFromNative.push(msgs); - // Always process async to avoid exceptions messing up stack. - nextTick(processMessages); - } - } - - androidExec.init = function () { - bridgeSecret = +prompt("", "gap_init:" + nativeToJsBridgeMode); - channel.onNativeReady.fire(); - }; - - function pollOnceFromOnlineEvent() { - pollOnce(true); - } - - function pollOnce(opt_fromOnlineEvent) { - if (bridgeSecret < 0) { - // This can happen when the NativeToJsMessageQueue resets the online state on page transitions. - // We know there's nothing to retrieve, so no need to poll. - return; - } - var msgs = nativeApiProvider - .get() - .retrieveJsMessages(bridgeSecret, !!opt_fromOnlineEvent); - if (msgs) { - messagesFromNative.push(msgs); - // Process sync since we know we're already top-of-stack. - processMessages(); - } - } - - function pollingTimerFunc() { - if (pollEnabled) { - pollOnce(); - setTimeout(pollingTimerFunc, 50); - } - } - - function hookOnlineApis() { - function proxyEvent(e) { - cordova.fireWindowEvent(e.type); - } - // The network module takes care of firing online and offline events. - // It currently fires them only on document though, so we bridge them - // to window here (while first listening for exec()-releated online/offline - // events). - window.addEventListener("online", pollOnceFromOnlineEvent, false); - window.addEventListener("offline", pollOnceFromOnlineEvent, false); - cordova.addWindowEventHandler("online"); - cordova.addWindowEventHandler("offline"); - document.addEventListener("online", proxyEvent, false); - document.addEventListener("offline", proxyEvent, false); - } - - hookOnlineApis(); - - androidExec.jsToNativeModes = jsToNativeModes; - androidExec.nativeToJsModes = nativeToJsModes; - - androidExec.setJsToNativeBridgeMode = function (mode) { - if (mode === jsToNativeModes.JS_OBJECT && !window._cordovaNative) { - mode = jsToNativeModes.PROMPT; - } - nativeApiProvider.setPreferPrompt(mode === jsToNativeModes.PROMPT); - jsToNativeBridgeMode = mode; - }; - - androidExec.setNativeToJsBridgeMode = function (mode) { - if (mode === nativeToJsBridgeMode) { - return; - } - if (nativeToJsBridgeMode === nativeToJsModes.POLLING) { - pollEnabled = false; - } - - nativeToJsBridgeMode = mode; - // Tell the native side to switch modes. - // Otherwise, it will be set by androidExec.init() - if (bridgeSecret >= 0) { - nativeApiProvider.get().setNativeToJsBridgeMode(bridgeSecret, mode); - } - - if (mode === nativeToJsModes.POLLING) { - pollEnabled = true; - setTimeout(pollingTimerFunc, 1); - } - }; - - function buildPayload(payload, message) { - var payloadKind = message.charAt(0); - if (payloadKind === "s") { - payload.push(message.slice(1)); - } else if (payloadKind === "t") { - payload.push(true); - } else if (payloadKind === "f") { - payload.push(false); - } else if (payloadKind === "N") { - payload.push(null); - } else if (payloadKind === "n") { - payload.push(+message.slice(1)); - } else if (payloadKind === "A") { - var data = message.slice(1); - payload.push(base64.toArrayBuffer(data)); - } else if (payloadKind === "S") { - payload.push(window.atob(message.slice(1))); - } else if (payloadKind === "M") { - var multipartMessages = message.slice(1); - while (multipartMessages !== "") { - var spaceIdx = multipartMessages.indexOf(" "); - var msgLen = +multipartMessages.slice(0, spaceIdx); - var multipartMessage = multipartMessages.substr( - spaceIdx + 1, - msgLen - ); - multipartMessages = multipartMessages.slice(spaceIdx + msgLen + 1); - buildPayload(payload, multipartMessage); - } - } else { - payload.push(JSON.parse(message)); - } - } - - // Processes a single message, as encoded by NativeToJsMessageQueue.java. - function processMessage(message) { - var firstChar = message.charAt(0); - if (firstChar === "J") { - // This is deprecated on the .java side. It doesn't work with CSP enabled. - // eslint-disable-next-line no-eval - eval(message.slice(1)); - } else if (firstChar === "S" || firstChar === "F") { - var success = firstChar === "S"; - var keepCallback = message.charAt(1) === "1"; - var spaceIdx = message.indexOf(" ", 2); - var status = +message.slice(2, spaceIdx); - var nextSpaceIdx = message.indexOf(" ", spaceIdx + 1); - var callbackId = message.slice(spaceIdx + 1, nextSpaceIdx); - var payloadMessage = message.slice(nextSpaceIdx + 1); - var payload = []; - buildPayload(payload, payloadMessage); - cordova.callbackFromNative( - callbackId, - success, - status, - payload, - keepCallback - ); - } else { - console.log( - "processMessage failed: invalid message: " + JSON.stringify(message) - ); - } - } - - function processMessages() { - // Check for the reentrant case. - if (isProcessing) { - return; - } - if (messagesFromNative.length === 0) { - return; - } - isProcessing = true; - try { - var msg = popMessageFromQueue(); - // The Java side can send a * message to indicate that it - // still has messages waiting to be retrieved. - if (msg === "*" && messagesFromNative.length === 0) { - nextTick(pollOnce); - return; - } - processMessage(msg); - } finally { - isProcessing = false; - if (messagesFromNative.length > 0) { - nextTick(processMessages); - } - } - } - - function popMessageFromQueue() { - var messageBatch = messagesFromNative.shift(); - if (messageBatch === "*") { - return "*"; - } - - var spaceIdx = messageBatch.indexOf(" "); - var msgLen = +messageBatch.slice(0, spaceIdx); - var message = messageBatch.substr(spaceIdx + 1, msgLen); - messageBatch = messageBatch.slice(spaceIdx + msgLen + 1); - if (messageBatch) { - messagesFromNative.unshift(messageBatch); - } - return message; - } - - module.exports = androidExec; - }); - - // file: src/common/exec/proxy.js - define("cordova/exec/proxy", function (require, exports, module) { - // internal map of proxy function - var CommandProxyMap = {}; - - module.exports = { - // example: cordova.commandProxy.add("Accelerometer",{getCurrentAcceleration: function(successCallback, errorCallback, options) {...},...); - add: function (id, proxyObj) { - console.log("adding proxy for " + id); - CommandProxyMap[id] = proxyObj; - return proxyObj; - }, - - // cordova.commandProxy.remove("Accelerometer"); - remove: function (id) { - var proxy = CommandProxyMap[id]; - delete CommandProxyMap[id]; - CommandProxyMap[id] = null; - return proxy; - }, - - get: function (service, action) { - return CommandProxyMap[service] - ? CommandProxyMap[service][action] - : null; - }, - }; - }); - - // file: src/common/init.js - define("cordova/init", function (require, exports, module) { - var channel = require("cordova/channel"); - var cordova = require("cordova"); - var modulemapper = require("cordova/modulemapper"); - var platform = require("cordova/platform"); - var pluginloader = require("cordova/pluginloader"); - - var platformInitChannelsArray = [ - channel.onNativeReady, - channel.onPluginsReady, - ]; - - function logUnfiredChannels(arr) { - for (var i = 0; i < arr.length; ++i) { - if (arr[i].state !== 2) { - console.log("Channel not fired: " + arr[i].type); - } - } - } - - window.setTimeout(function () { - if (channel.onDeviceReady.state !== 2) { - console.log("deviceready has not fired after 5 seconds."); - logUnfiredChannels(platformInitChannelsArray); - logUnfiredChannels(channel.deviceReadyChannelsArray); - } - }, 5000); - - if (!window.console) { - window.console = { - log: function () {}, - }; - } - if (!window.console.warn) { - window.console.warn = function (msg) { - this.log("warn: " + msg); - }; - } - - // Register pause, resume and deviceready channels as events on document. - channel.onPause = cordova.addDocumentEventHandler("pause"); - channel.onResume = cordova.addDocumentEventHandler("resume"); - channel.onActivated = cordova.addDocumentEventHandler("activated"); - channel.onDeviceReady = - cordova.addStickyDocumentEventHandler("deviceready"); - - // Listen for DOMContentLoaded and notify our channel subscribers. - if ( - document.readyState === "complete" || - document.readyState === "interactive" - ) { - channel.onDOMContentLoaded.fire(); - } else { - document.addEventListener( - "DOMContentLoaded", - function () { - channel.onDOMContentLoaded.fire(); - }, - false - ); - } - - // _nativeReady is global variable that the native side can set - // to signify that the native code is ready. It is a global since - // it may be called before any cordova JS is ready. - if (window._nativeReady) { - channel.onNativeReady.fire(); - } - - modulemapper.clobbers("cordova", "cordova"); - modulemapper.clobbers("cordova/exec", "cordova.exec"); - modulemapper.clobbers("cordova/exec", "Cordova.exec"); - - // Call the platform-specific initialization. - platform.bootstrap && platform.bootstrap(); - - // Wrap in a setTimeout to support the use-case of having plugin JS appended to cordova.js. - // The delay allows the attached modules to be defined before the plugin loader looks for them. - setTimeout(function () { - pluginloader.load(function () { - channel.onPluginsReady.fire(); - }); - }, 0); - - /** - * Create all cordova objects once native side is ready. - */ - channel.join(function () { - modulemapper.mapModules(window); - - platform.initialize && platform.initialize(); - - // Fire event to notify that all objects are created - channel.onCordovaReady.fire(); - - // Fire onDeviceReady event once page has fully loaded, all - // constructors have run and cordova info has been received from native - // side. - channel.join(function () { - require("cordova").fireDocumentEvent("deviceready"); - }, channel.deviceReadyChannelsArray); - }, platformInitChannelsArray); - }); - - // file: src/common/modulemapper.js - define("cordova/modulemapper", function (require, exports, module) { - var builder = require("cordova/builder"); - var moduleMap = define.moduleMap; - var symbolList; - var deprecationMap; - - exports.reset = function () { - symbolList = []; - deprecationMap = {}; - }; - - function addEntry( - strategy, - moduleName, - symbolPath, - opt_deprecationMessage - ) { - if (!(moduleName in moduleMap)) { - throw new Error("Module " + moduleName + " does not exist."); - } - symbolList.push(strategy, moduleName, symbolPath); - if (opt_deprecationMessage) { - deprecationMap[symbolPath] = opt_deprecationMessage; - } - } - - // Note: Android 2.3 does have Function.bind(). - exports.clobbers = function ( - moduleName, - symbolPath, - opt_deprecationMessage - ) { - addEntry("c", moduleName, symbolPath, opt_deprecationMessage); - }; - - exports.merges = function ( - moduleName, - symbolPath, - opt_deprecationMessage - ) { - addEntry("m", moduleName, symbolPath, opt_deprecationMessage); - }; - - exports.defaults = function ( - moduleName, - symbolPath, - opt_deprecationMessage - ) { - addEntry("d", moduleName, symbolPath, opt_deprecationMessage); - }; - - exports.runs = function (moduleName) { - addEntry("r", moduleName, null); - }; - - function prepareNamespace(symbolPath, context) { - if (!symbolPath) { - return context; - } - return symbolPath.split(".").reduce(function (cur, part) { - return (cur[part] = cur[part] || {}); - }, context); - } - - exports.mapModules = function (context) { - var origSymbols = {}; - context.CDV_origSymbols = origSymbols; - for (var i = 0, len = symbolList.length; i < len; i += 3) { - var strategy = symbolList[i]; - var moduleName = symbolList[i + 1]; - var module = require(moduleName); - // - if (strategy === "r") { - continue; - } - var symbolPath = symbolList[i + 2]; - var lastDot = symbolPath.lastIndexOf("."); - var namespace = symbolPath.substr(0, lastDot); - var lastName = symbolPath.substr(lastDot + 1); - - var deprecationMsg = - symbolPath in deprecationMap - ? "Access made to deprecated symbol: " + - symbolPath + - ". " + - deprecationMsg - : null; - var parentObj = prepareNamespace(namespace, context); - var target = parentObj[lastName]; - - if (strategy === "m" && target) { - builder.recursiveMerge(target, module); - } else if ((strategy === "d" && !target) || strategy !== "d") { - if (!(symbolPath in origSymbols)) { - origSymbols[symbolPath] = target; - } - builder.assignOrWrapInDeprecateGetter( - parentObj, - lastName, - module, - deprecationMsg - ); - } - } - }; - - exports.getOriginalSymbol = function (context, symbolPath) { - var origSymbols = context.CDV_origSymbols; - if (origSymbols && symbolPath in origSymbols) { - return origSymbols[symbolPath]; - } - var parts = symbolPath.split("."); - var obj = context; - for (var i = 0; i < parts.length; ++i) { - obj = obj && obj[parts[i]]; - } - return obj; - }; - - exports.reset(); - }); - - // file: ../../cordova-js-src/platform.js - define("cordova/platform", function (require, exports, module) { - // The last resume event that was received that had the result of a plugin call. - var lastResumeEvent = null; - - module.exports = { - id: "android", - bootstrap: function () { - var channel = require("cordova/channel"); - var cordova = require("cordova"); - var exec = require("cordova/exec"); - var modulemapper = require("cordova/modulemapper"); - - // Get the shared secret needed to use the bridge. - exec.init(); - - // TODO: Extract this as a proper plugin. - modulemapper.clobbers("cordova/plugin/android/app", "navigator.app"); - - var APP_PLUGIN_NAME = - Number(cordova.platformVersion.split(".")[0]) >= 4 - ? "CoreAndroid" - : "App"; - - // Inject a listener for the backbutton on the document. - var backButtonChannel = cordova.addDocumentEventHandler("backbutton"); - backButtonChannel.onHasSubscribersChange = function () { - // If we just attached the first handler or detached the last handler, - // let native know we need to override the back button. - exec(null, null, APP_PLUGIN_NAME, "overrideBackbutton", [ - this.numHandlers === 1, - ]); - }; - - // Add hardware MENU and SEARCH button handlers - cordova.addDocumentEventHandler("menubutton"); - cordova.addDocumentEventHandler("searchbutton"); - - function bindButtonChannel(buttonName) { - // generic button bind used for volumeup/volumedown buttons - var volumeButtonChannel = cordova.addDocumentEventHandler( - buttonName + "button" - ); - volumeButtonChannel.onHasSubscribersChange = function () { - exec(null, null, APP_PLUGIN_NAME, "overrideButton", [ - buttonName, - this.numHandlers === 1, - ]); - }; - } - // Inject a listener for the volume buttons on the document. - bindButtonChannel("volumeup"); - bindButtonChannel("volumedown"); - - // The resume event is not "sticky", but it is possible that the event - // will contain the result of a plugin call. We need to ensure that the - // plugin result is delivered even after the event is fired (CB-10498) - var cordovaAddEventListener = document.addEventListener; - - document.addEventListener = function (evt, handler, capture) { - cordovaAddEventListener(evt, handler, capture); - - if (evt === "resume" && lastResumeEvent) { - handler(lastResumeEvent); - } - }; - - // Let native code know we are all done on the JS side. - // Native code will then un-hide the WebView. - channel.onCordovaReady.subscribe(function () { - exec( - onMessageFromNative, - null, - APP_PLUGIN_NAME, - "messageChannel", - [] - ); - exec(null, null, APP_PLUGIN_NAME, "show", []); - }); - }, - }; - - function onMessageFromNative(msg) { - var cordova = require("cordova"); - var action = msg.action; - - switch (action) { - // pause and resume are Android app life cycle events - case "backbutton": - case "menubutton": - case "searchbutton": - case "pause": - case "volumedownbutton": - case "volumeupbutton": - cordova.fireDocumentEvent(action); - break; - case "resume": - if (arguments.length > 1 && msg.pendingResult) { - if (arguments.length === 2) { - msg.pendingResult.result = arguments[1]; - } else { - // The plugin returned a multipart message - var res = []; - for (var i = 1; i < arguments.length; i++) { - res.push(arguments[i]); - } - msg.pendingResult.result = res; - } - - // Save the plugin result so that it can be delivered to the js - // even if they miss the initial firing of the event - lastResumeEvent = msg; - } - cordova.fireDocumentEvent(action, msg); - break; - default: - throw new Error("Unknown event action " + action); - } - } - }); - - // file: ../../cordova-js-src/plugin/android/app.js - define("cordova/plugin/android/app", function (require, exports, module) { - var exec = require("cordova/exec"); - var APP_PLUGIN_NAME = - Number(require("cordova").platformVersion.split(".")[0]) >= 4 - ? "CoreAndroid" - : "App"; - - module.exports = { - /** - * Clear the resource cache. - */ - clearCache: function () { - exec(null, null, APP_PLUGIN_NAME, "clearCache", []); - }, - - /** - * Load the url into the webview or into new browser instance. - * - * @param url The URL to load - * @param props Properties that can be passed in to the activity: - * wait: int => wait msec before loading URL - * loadingDialog: "Title,Message" => display a native loading dialog - * loadUrlTimeoutValue: int => time in msec to wait before triggering a timeout error - * clearHistory: boolean => clear webview history (default=false) - * openExternal: boolean => open in a new browser (default=false) - * - * Example: - * navigator.app.loadUrl("http://server/myapp/index.html", {wait:2000, loadingDialog:"Wait,Loading App", loadUrlTimeoutValue: 60000}); - */ - loadUrl: function (url, props) { - exec(null, null, APP_PLUGIN_NAME, "loadUrl", [url, props]); - }, - - /** - * Cancel loadUrl that is waiting to be loaded. - */ - cancelLoadUrl: function () { - exec(null, null, APP_PLUGIN_NAME, "cancelLoadUrl", []); - }, - - /** - * Clear web history in this web view. - * Instead of BACK button loading the previous web page, it will exit the app. - */ - clearHistory: function () { - exec(null, null, APP_PLUGIN_NAME, "clearHistory", []); - }, - - /** - * Go to previous page displayed. - * This is the same as pressing the backbutton on Android device. - */ - backHistory: function () { - exec(null, null, APP_PLUGIN_NAME, "backHistory", []); - }, - - /** - * Override the default behavior of the Android back button. - * If overridden, when the back button is pressed, the "backKeyDown" JavaScript event will be fired. - * - * Note: The user should not have to call this method. Instead, when the user - * registers for the "backbutton" event, this is automatically done. - * - * @param override T=override, F=cancel override - */ - overrideBackbutton: function (override) { - exec(null, null, APP_PLUGIN_NAME, "overrideBackbutton", [override]); - }, - - /** - * Override the default behavior of the Android volume button. - * If overridden, when the volume button is pressed, the "volume[up|down]button" - * JavaScript event will be fired. - * - * Note: The user should not have to call this method. Instead, when the user - * registers for the "volume[up|down]button" event, this is automatically done. - * - * @param button volumeup, volumedown - * @param override T=override, F=cancel override - */ - overrideButton: function (button, override) { - exec(null, null, APP_PLUGIN_NAME, "overrideButton", [ - button, - override, - ]); - }, - - /** - * Exit and terminate the application. - */ - exitApp: function () { - return exec(null, null, APP_PLUGIN_NAME, "exitApp", []); - }, - }; - }); - - // file: src/common/pluginloader.js - define("cordova/pluginloader", function (require, exports, module) { - var modulemapper = require("cordova/modulemapper"); - - // Helper function to inject a