diff --git a/.github/ISSUE_TEMPLATE/bug-issue.yml b/.github/ISSUE_TEMPLATE/bug-issue.yml index 87c8e4fb67..73017f1b61 100644 --- a/.github/ISSUE_TEMPLATE/bug-issue.yml +++ b/.github/ISSUE_TEMPLATE/bug-issue.yml @@ -1,120 +1,61 @@ -name: 🐞 Bug report -description: Report a very clearly broken issue. -title: 'bug: ' -labels: [bug] -body: - - type: markdown - attributes: - value: | - # ReVanced Manager bug report - - Important to note that your issue may have already been reported before. Please check for existing issues [here](https://github.com/revanced/revanced-manager/labels/bug). - - - type: dropdown - attributes: - label: Type - options: - - Error while running the manager - - Error at runtime - - Cosmetic - - Other - validations: - required: true - - type: textarea - attributes: - label: Bug description - description: How did you find the bug? Any additional details that might help? - validations: - required: true - - type: textarea - attributes: - label: Steps to reproduce - description: Add the steps to reproduce this bug, including your environment. - placeholder: Step 1. Download some files. Step 2. ... - validations: - required: true - - type: textarea - attributes: - label: Android version - description: Android version used. - validations: - required: true - - type: textarea - attributes: - label: Manager version - description: Manager version used. - validations: - required: true - - type: textarea - attributes: - label: Target package name - description: App you tried to patch. - validations: - required: true - - type: textarea - attributes: - label: Target package version. - description: Version of the app you tried to patch. - validations: - required: true - - type: dropdown - attributes: - label: Installation type - options: - - Non-root - - Root - validations: - required: true - - type: textarea - attributes: - label: Patches selected. - description: Patches you selected for the app. - validations: - required: true - - type: textarea - attributes: - label: Device logs (exported using Manager settings). - description: Please copy and paste any relevant log output. This will be automatically formatted into code, so there is no need for backticks. - render: shell - validations: - required: true - - type: textarea - attributes: - label: Installer logs (exported using Installer menu option) [unneeded if the issue is not during patching]. - description: Please copy and paste any relevant log output. This will be automatically formatted into code, so there is no need for backticks. - render: shell - validations: - required: false - - type: textarea - attributes: - label: Screenshots or video - description: Add screenshots or videos that show the bug here. - placeholder: Drag and drop the screenshots/videos into this box. - validations: - required: false - - type: textarea - attributes: - label: Solution - description: If applicable, add a possible solution. - validations: - required: false - - type: textarea - attributes: - label: Additional context - description: Add additional context here. - validations: - required: false - - type: checkboxes - id: acknowledgments - attributes: - label: Acknowledgments - description: Your issue will be closed if you haven't done these steps. - options: - - label: I have searched the existing issues; this is new and no duplicate or related to another open issue. - required: true - - label: I have written a short but informative title. - required: true - - label: I properly filled out all of the requested information in this issue. - required: true - - label: The issue is solely related to ReVanced Manager and not caused by patches. - required: true +name: 🐞 Bug report +description: Create a new bug report. +title: 'bug: <title>' +labels: [bug] +body: + - type: markdown + attributes: + value: | + # ReVanced Manager bug report + + Please check for existing issues [here](https://github.com/revanced/revanced-manager/labels/bug) before creating a new one. + - type: textarea + attributes: + label: Bug description + description: | + - Describe your bug in detail + - Add steps to reproduce the bug if possible (Step 1. Download some files. Step 2. ...) + - Add images and videos if possible + - List selected patches if applicable + validations: + required: true + - type: textarea + attributes: + label: Version of ReVanced Manager and version & name of application you tried to patch + validations: + required: true + - type: dropdown + attributes: + label: Installation type + options: + - Non-root + - Root + validations: + required: false + - type: textarea + attributes: + label: Device logs + description: Export logs in ReVanced Manager settings. + render: shell + validations: + required: true + - type: textarea + attributes: + label: Patcher logs + description: Export logs in "Patcher" screen. + render: shell + validations: + required: false + - type: checkboxes + attributes: + label: Acknowledgements + description: Your issue will be closed if you don't follow the checklist below! + options: + - label: This request is not a duplicate of an existing issue. + required: true + - label: I have chosen an appropriate title. + required: true + - label: All requested information has been provided properly. + required: true + - label: The issue is solely related to the ReVanced Manager + required: true diff --git a/.github/ISSUE_TEMPLATE/feature-issue.yml b/.github/ISSUE_TEMPLATE/feature-issue.yml index 8df4eab2d7..ca76ef0020 100644 --- a/.github/ISSUE_TEMPLATE/feature-issue.yml +++ b/.github/ISSUE_TEMPLATE/feature-issue.yml @@ -1,52 +1,42 @@ -name: ⭐ Feature request -description: Create a detailed feature request. -title: 'feat: <title>' -labels: [feature-request] -body: - - type: dropdown - attributes: - label: Type - options: - - Functionality - - Cosmetic - - Other - validations: - required: true - - type: textarea - attributes: - label: Issue - description: What is the current problem. Why does it require a feature request? - validations: - required: true - - type: textarea - attributes: - label: Feature - description: Describe your feature in detail. How does it solve the issue? - validations: - required: true - - type: textarea - attributes: - label: Motivation - description: Why should your feature should be considered? - validations: - required: true - - type: textarea - attributes: - label: Additional context - description: Add additional context here. - validations: - required: false - - type: checkboxes - id: acknowledgements - attributes: - label: Acknowledgements - description: Your issue will be closed if you haven't done these steps. - options: - - label: I have searched the existing issues and this is a new and no duplicate or related to another open issue. - required: true - - label: I have written a short but informative title. - required: true - - label: I filled out all of the requested information in this issue properly. - required: true - - label: The issue is related solely to the ReVanced Manager - required: true +name: ⭐ Feature request +description: Create a new feature request. +title: 'feat: <title>' +labels: [feature request] +body: + - type: markdown + attributes: + value: | + # ReVanced Manager feature request + + Please check for existing feature requests [here](https://github.com/revanced/revanced-manager/labels/bug) before creating a new one. + - type: textarea + attributes: + label: Feature description + description: Describe your feature in detail. + validations: + required: true + - type: textarea + attributes: + label: Motivation + description: Explain why the lack of it is a problem. + validations: + required: true + - type: textarea + attributes: + label: Additional context + description: In case there is something else you want to add. + validations: + required: false + - type: checkboxes + attributes: + label: Acknowledgements + description: Your issue will be closed if you don't follow the checklist below! + options: + - label: This request is not a duplicate of an existing issue. + required: true + - label: I have chosen an appropriate title. + required: true + - label: All requested information has been provided properly. + required: true + - label: The issue is solely related to the ReVanced Manager + required: true diff --git a/.github/config.yaml b/.github/config.yaml index 650941e517..aaeba6e2da 100644 --- a/.github/config.yaml +++ b/.github/config.yaml @@ -1,2 +1,2 @@ firstPRMergeComment: > - Thank you for contributing to ReVanced. Join us on [Discord](https://revanced.app/discord) if you want to receive a contributor role. \ No newline at end of file + ❤️ Thank you for contributing to ReVanced Manager. Join us on [Discord](https://revanced.app/discord) if you want to receive a contributor role. diff --git a/.github/workflows/analyze.yml b/.github/workflows/analyze.yml deleted file mode 100644 index e3f0f0638b..0000000000 --- a/.github/workflows/analyze.yml +++ /dev/null @@ -1,38 +0,0 @@ -name: Analyze Code - -on: - push: - branches: [ "dev" ] - paths: - - "**.dart" - - ".github/workflows/analyze.yml" - pull_request: - branches: [ "main", "dev" ] - types: - - opened - - reopened - - synchronize - - ready_for_review - paths: - - "**.dart" - - ".github/workflows/analyze.yml" - -jobs: - build: - name: "Static analysis & format check" - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v3 - - name: Setup Flutter - uses: subosito/flutter-action@v2 - with: - channel: 'stable' - cache: true - - name: Install Flutter dependencies - run: flutter pub get - - name: Generate files with Builder - run: flutter packages pub run build_runner build --delete-conflicting-outputs - - name: Analyze code - uses: ValentinVignal/action-dart-analyze@v0.15 - with: - fail-on: warning diff --git a/.github/workflows/pr-build.yml b/.github/workflows/pr-build.yml index 0bb9bdb11f..4ca4a56580 100644 --- a/.github/workflows/pr-build.yml +++ b/.github/workflows/pr-build.yml @@ -1,45 +1,44 @@ -name: PR Build +name: Build pull request on: pull_request: - paths: + paths: - ".github/workflows/pr-build.yml" - - "android/**" - - "assets/**" - - "lib/**" - + - "app/**" + - "gradle/**" + - "*.properties" + - ".kts" + jobs: build: name: Build runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v3 - with: - # Make sure the release step uses its own credentials: - # https://github.com/cycjimmy/semantic-release-action#private-packages - persist-credentials: false - fetch-depth: 0 - - name: Setup JDK - uses: actions/setup-java@v3 - with: - java-version: '11' - distribution: 'zulu' - - name: Setup Flutter - uses: subosito/flutter-action@v2 + uses: actions/checkout@v4 + + - name: Set up JDK 17 + uses: actions/setup-java@v4 with: - channel: 'stable' - cache: true - - name: Install Flutter dependencies - run: flutter pub get - - name: Generate files with Builder - run: flutter packages pub run build_runner build --delete-conflicting-outputs - - name: Build with Flutter + java-version: '17' + distribution: 'temurin' + + - name: Set up Gradle + uses: gradle/actions/setup-gradle@v4 + + - name: Build with Gradle env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: flutter build apk --debug + run: ./gradlew assembleRelease --no-daemon -PnoProguard -PsignAsDebug + + - name: Set env + run: echo "COMMIT_HASH=$(git rev-parse --short HEAD)" >> $GITHUB_ENV + + - name: Add hash to APK + run: mv app/build/outputs/apk/release/app-release.apk revanced-manager-${{ env.COMMIT_HASH }}.apk + - name: Upload build - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: revanced-manager - path: build/app/outputs/flutter-apk/app-debug.apk + path: revanced-manager-${{ env.COMMIT_HASH }}.apk diff --git a/.github/workflows/release-build.yml b/.github/workflows/release-build.yml index 4b414eb7ef..4b01fe6308 100644 --- a/.github/workflows/release-build.yml +++ b/.github/workflows/release-build.yml @@ -1,4 +1,4 @@ -name: "Release Build" +name: Release Build on: push: @@ -6,45 +6,52 @@ on: - "v*" jobs: - release: + build: + name: Build runs-on: ubuntu-latest + permissions: + id-token: write + attestations: write steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Set env run: echo "RELEASE_VERSION=${GITHUB_REF#refs/*/}" >> $GITHUB_ENV - - name: Set up JDK 11 - uses: actions/setup-java@v3 - with: - java-version: "11" - distribution: "zulu" - - uses: subosito/flutter-action@v2 + + - name: Set up JDK 17 + uses: actions/setup-java@v4 with: - channel: "stable" - - name: Set up Flutter - run: flutter pub get - - name: Generate files with Builder - run: flutter packages pub run build_runner build --delete-conflicting-outputs - - name: Build with Flutter + java-version: '17' + distribution: 'temurin' + + - name: Set up Gradle + uses: gradle/actions/setup-gradle@v4 + + - name: Build with Gradle env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - SIGNING_KEY_ALIAS: ${{ secrets.SIGNING_KEY_ALIAS }} - SIGNING_KEY_PASSWORD: ${{ secrets.SIGNING_KEY_PASSWORD }} - SIGNING_STORE_PASSWORD: ${{ secrets.SIGNING_KEYSTORE_PASSWORD }} - run: flutter build apk + run: ./gradlew assembleRelease --no-daemon + - name: Sign APK id: sign_apk uses: ilharp/sign-android-release@v1 with: - releaseDir: build/app/outputs/apk/release + releaseDir: ./app/build/outputs/apk/release/ signingKey: ${{ secrets.SIGNING_KEYSTORE }} keyStorePassword: ${{ secrets.SIGNING_KEYSTORE_PASSWORD }} keyAlias: ${{ secrets.SIGNING_KEY_ALIAS }} keyPassword: ${{ secrets.SIGNING_KEY_PASSWORD }} + - name: Add version to APK - run: mv ${{steps.sign_apk.outputs.signedFile}} revanced-manager-${{ env.RELEASE_VERSION }}.apk + run: mv ${{ steps.sign_apk.outputs.signedFile }} revanced-manager-${{ env.RELEASE_VERSION }}.apk + + - name: Generate artifact attestation + uses: actions/attest-build-provenance@v1 + with: + subject-path: revanced-manager-${{ env.RELEASE_VERSION }}.apk + - name: Publish release APK uses: "marvinpinto/action-automatic-releases@latest" with: repo_token: "${{ secrets.GITHUB_TOKEN }}" prerelease: false - files: revanced-manager-${{ env.RELEASE_VERSION }}.apk \ No newline at end of file + files: revanced-manager-${{ env.RELEASE_VERSION }}.apk diff --git a/.github/workflows/update-documentation.yml b/.github/workflows/update-documentation.yml index 77097e2fe6..541a7aa5b5 100644 --- a/.github/workflows/update-documentation.yml +++ b/.github/workflows/update-documentation.yml @@ -11,7 +11,7 @@ jobs: name: Dispatch event to documentation repository if: github.ref == 'refs/heads/main' steps: - - uses: peter-evans/repository-dispatch@v2 + - uses: peter-evans/repository-dispatch@v3 with: token: ${{ secrets.DOCUMENTATION_REPO_ACCESS_TOKEN }} repository: revanced/revanced-documentation diff --git a/.gitignore b/.gitignore index 2f30200232..c0af92ebad 100644 --- a/.gitignore +++ b/.gitignore @@ -1,144 +1,12 @@ -# Miscellaneous -*.class -*.lock -*.log -*.pyc -*.swp -.DS_Store -.atom/ -.buildlog/ -.history -.svn/ - -# IntelliJ related *.iml -*.ipr -*.iws -.idea/ - -# Visual Studio Code related -.classpath -.project -.settings/ - -# Flutter repo-specific -/bin/cache/ -/bin/mingit/ -/dev/benchmarks/mega_gallery/ -/dev/bots/.recipe_deps -/dev/bots/android_tools/ -/dev/docs/doc/ -/dev/docs/flutter.docs.zip -/dev/docs/lib/ -/dev/docs/pubspec.yaml -/dev/integration_tests/**/xcuserdata -/dev/integration_tests/**/Pods -/packages/flutter/coverage/ -version - -# packages file containing multi-root paths -.packages.generated - -# Flutter/Dart/Pub related -**/doc/api/ -**/*.g.dart -**/*.locator.dart -**/*.router.dart -.dart_tool/ -.flutter-plugins -.flutter-plugins-dependencies -**/generated_plugin_registrant.dart -.packages -.pub-cache/ -.pub/ -build/ -flutter_*.png -linked_*.ds -unlinked.ds -unlinked_spec.ds - -# Android related -**/android/**/gradle-wrapper.jar -**/android/.gradle -**/android/captures/ -**/android/gradlew -**/android/gradlew.bat -**/android/local.properties -**/android/**/GeneratedPluginRegistrant.java -**/android/key.properties -*.jks - -# iOS/XCode related -**/ios/**/*.mode1v3 -**/ios/**/*.mode2v3 -**/ios/**/*.moved-aside -**/ios/**/*.pbxuser -**/ios/**/*.perspectivev3 -**/ios/**/*sync/ -**/ios/**/.sconsign.dblite -**/ios/**/.tags* -**/ios/**/.vagrant/ -**/ios/**/DerivedData/ -**/ios/**/Icon? -**/ios/**/Pods/ -**/ios/**/.symlinks/ -**/ios/**/profile -**/ios/**/xcuserdata -**/ios/.generated/ -**/ios/Flutter/.last_build_id -**/ios/Flutter/App.framework -**/ios/Flutter/Flutter.framework -**/ios/Flutter/Flutter.podspec -**/ios/Flutter/Generated.xcconfig -**/ios/Flutter/app.flx -**/ios/Flutter/app.zip -**/ios/Flutter/flutter_assets/ -**/ios/Flutter/flutter_export_environment.sh -**/ios/ServiceDefinitions.json -**/ios/Runner/GeneratedPluginRegistrant.* - -# macOS related -**/macos/Flutter/GeneratedPluginRegistrant.swift -**/macos/Flutter/Flutter-Debug.xcconfig -**/macos/Flutter/Flutter-Release.xcconfig -**/macos/Flutter/Flutter-Profile.xcconfig - -# Windows related -**/windows/flutter/ephemeral/ -**/windows/**/*.suo -**/windows/**/*.user -**/windows/**/*.userosscache -**/windows/**/*.sln.docstates -**/windows/x64/ -**/windows/x86/ -**/windows/**/*.[Cc]ache -**/windows/**/!*.[Cc]ache/ - -# Web related -lib/generated_plugin_registrant.dart - -# Coverage -coverage/ - -# Symbolication related -app.*.symbols - -# Obfuscation related -app.*.map.json - -# Exceptions to above rules. -!**/ios/**/default.mode1v3 -!**/ios/**/default.mode2v3 -!**/ios/**/default.pbxuser -!**/ios/**/default.perspectivev3 -!/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages -!/dev/ci/**/Gemfile.lock - -# Firebase related -.firebase - -# Dependency directories -node_modules/ +.gradle +/local.properties +/.idea +.DS_Store +/build +/captures +.externalNativeBuild +.cxx +local.properties -# FVM -.fvm \ No newline at end of file +.kotlin/ diff --git a/.metadata b/.metadata deleted file mode 100644 index e7c1001054..0000000000 --- a/.metadata +++ /dev/null @@ -1,45 +0,0 @@ -# This file tracks properties of this Flutter project. -# Used by Flutter tool to assess capabilities and perform upgrades etc. -# -# This file should be version controlled. - -version: - revision: 85684f9300908116a78138ea4c6036c35c9a1236 - channel: stable - -project_type: app - -# Tracks metadata for the flutter migrate command -migration: - platforms: - - platform: root - create_revision: 85684f9300908116a78138ea4c6036c35c9a1236 - base_revision: 85684f9300908116a78138ea4c6036c35c9a1236 - - platform: android - create_revision: 85684f9300908116a78138ea4c6036c35c9a1236 - base_revision: 85684f9300908116a78138ea4c6036c35c9a1236 - - platform: ios - create_revision: 85684f9300908116a78138ea4c6036c35c9a1236 - base_revision: 85684f9300908116a78138ea4c6036c35c9a1236 - - platform: linux - create_revision: 85684f9300908116a78138ea4c6036c35c9a1236 - base_revision: 85684f9300908116a78138ea4c6036c35c9a1236 - - platform: macos - create_revision: 85684f9300908116a78138ea4c6036c35c9a1236 - base_revision: 85684f9300908116a78138ea4c6036c35c9a1236 - - platform: web - create_revision: 85684f9300908116a78138ea4c6036c35c9a1236 - base_revision: 85684f9300908116a78138ea4c6036c35c9a1236 - - platform: windows - create_revision: 85684f9300908116a78138ea4c6036c35c9a1236 - base_revision: 85684f9300908116a78138ea4c6036c35c9a1236 - - # User provided section - - # List of Local paths (relative to this file) that should be - # ignored by the migrate tool. - # - # Files that are not part of the templates will be ignored by default. - unmanaged_files: - - 'lib/main.dart' - - 'ios/Runner.xcodeproj/project.pbxproj' diff --git a/.releaserc b/.releaserc deleted file mode 100644 index a10173d648..0000000000 --- a/.releaserc +++ /dev/null @@ -1,75 +0,0 @@ -{ - "branches": [ - "main", - { - "name": "dev", - "prerelease": true - } - ], - "plugins": [ - "semantic-release-export-data", - "@semantic-release/commit-analyzer", - [ - "@semantic-release/release-notes-generator", - { - "presetConfig": { - "types": [ - { - "type": "build", - "section": "Dependency Updates" - }, - { - "type": "chore", - "section": "Other Changes", - "hidden": false - }, - { - "type": "perf", - "section": "Performance Improvements", - "hidden": false - }, - { - "type": "refactor", - "section": "Code Improvements", - "hidden": false - } - ] - } - } - ], - "@semantic-release/changelog", - "semantic-release-flutter-plugin", - [ - "@semantic-release/git", - { - "assets": [ - "CHANGELOG.md", - "pubspec.yaml" - ] - } - ], - [ - "@semantic-release/github", - { - "assets": [ - { - "path": "build/app/outputs/apk/release/revanced-manager-*.apk" - } - ], - "successComment": false - } - ], - [ - "@saithodev/semantic-release-backmerge", - { - "backmergeBranches": [ - { - "from": "main", - "to": "dev" - } - ], - "clearWorkspace": true - } - ] - ] -} diff --git a/.run/main.dart.run.xml b/.run/main.dart.run.xml deleted file mode 100644 index 4767aff814..0000000000 --- a/.run/main.dart.run.xml +++ /dev/null @@ -1,6 +0,0 @@ -<component name="ProjectRunConfigurationManager"> - <configuration default="false" name="main.dart" type="FlutterRunConfigurationType" factoryName="Flutter"> - <option name="filePath" value="$PROJECT_DIR$/lib/main.dart" /> - <method v="2" /> - </configuration> -</component> \ No newline at end of file diff --git a/.vscode/tasks.json b/.vscode/tasks.json deleted file mode 100644 index a09c28f5e6..0000000000 --- a/.vscode/tasks.json +++ /dev/null @@ -1,91 +0,0 @@ -{ - "version": "2.0.0", - "tasks": [ - { - "label": "Generate (Builder)", - "type": "shell", - "command": "flutter packages pub run build_runner build --delete-conflicting-outputs", - "problemMatcher": [] - }, - { - "label": "Build (Android)", - "type": "shell", - "command": "flutter build apk", - "problemMatcher": [], - "group": { - "kind": "build", - "isDefault": true - } - }, - { - "label": "Install (Android)", - "type": "shell", - "command": "adb install build\\app\\outputs\\flutter-apk\\app-release.apk", - "problemMatcher": [] - }, - { - "label": "Clean (Flutter)", - "type": "shell", - "command": "flutter clean && flutter pub get", - "problemMatcher": [] - }, - { - "label": "Clean (Builder)", - "type": "shell", - "command": "flutter packages pub run build_runner clean", - "problemMatcher": [] - }, - { - "label": "Build all (Android)", - "dependsOrder": "sequence", - "dependsOn": [ - "Generate (Builder)", - "Build (Android)" - ], - "problemMatcher": [] - }, - { - "label": "Clean all", - "dependsOrder": "sequence", - "dependsOn": [ - "Clean (Flutter)", - "Clean (Builder)" - ], - "problemMatcher": [] - }, - { - "label": "Clean all & Build all (Android)", - "dependsOrder": "sequence", - "dependsOn": [ - "Clean all", - "Build all (Android)" - ], - "problemMatcher": [] - }, - { - "label": "Clean all & Install (Android)", - "dependsOrder": "sequence", - "dependsOn": [ - "Clean all", - "Build all (Android)", - "Install (Android)", - ], - "problemMatcher": [] - }, - { - "label": "Build & Install (Android)", - "dependsOrder": "sequence", - "dependsOn": [ - "Build (Android)", - "Install (Android)" - ], - "problemMatcher": [] - }, - { - "label": "Validate translations", - "type": "shell", - "command": "flutter pub run flutter_i18n diff en.json pt.json", - "problemMatcher": [] - } - ] -} diff --git a/CHANGELOG.md b/CHANGELOG.md deleted file mode 100644 index 8b13789179..0000000000 --- a/CHANGELOG.md +++ /dev/null @@ -1 +0,0 @@ - diff --git a/README.md b/README.md index 07ed4a791f..c742bcab29 100644 --- a/README.md +++ b/README.md @@ -1,31 +1,55 @@ -# 💊 ReVanced Manager +# ReVanced Manager (Compose Rewrite) -The official ReVanced Manager based on Flutter. +[![GitHub license](https://img.shields.io/github/license/revanced/revanced-manager)](../../blob/main/LICENSE) +[![GitHub last commit](https://img.shields.io/github/last-commit/revanced/revanced-manager/compose-dev)](https://github.com/ReVanced/revanced-manager/commits/compose-dev) +[![GitHub commit activity](https://img.shields.io/github/commit-activity/w/revanced/revanced-manager/compose-dev)](https://github.com/ReVanced/revanced-manager/commits/compose-dev) + +_(Yet another)_ rewrite of the ReVanced Manager using Kotlin and Jetpack Compose. + +## Design system + +In this rewrite, we are adopting the latest Material Design principles and guidelines by using Material 3 and Material You. + +Material Design is a design system developed by Google that provides a unified visual language for building beautiful and consistent user interfaces across all platforms and devices. Material You is an extension of Material Design that provides even more customization options for users, making it possible for them to personalize their device and create a unique look and feel. + +### Why Material 3? + +* **Consistent design language** +* **Improved accessibility** +* **Better user experience** + +By using Material 3 and Material You, we are ensuring that the app's user interface is consistent, customizable, accessible, and engaging for our users. This will help to improve the overall user experience and increase user satisfaction with the the manager. + +## Technology stack + +* Kotlin: Kotlin is a modern and concise programming language that is fully interoperable with Java and provides improved safety, readability, and maintainability compared to Java. +* Jetpack Compose: Jetpack Compose is a modern UI toolkit for Android development that allows developers to build beautiful and performant user interfaces using declarative programming. It provides a unified and efficient way of building UI that is well-integrated with the Android framework. + +## Why Kotlin and Compose? + +* **Improved safety:** Kotlin provides improved safety compared to Java, which reduces the likelihood of common programming mistakes that can cause security vulnerabilities or crashes. +* **Concise and readable code:** Kotlin's concise syntax and expressive type system make the code more readable, which makes it easier for developers to understand and maintain the codebase. +* **Better performance:** Jetpack Compose uses the power of the Android framework to provide smooth and fast performance, which enhances the user experience. +* **Modern and efficient UI development:** Jetpack Compose provides a modern and efficient way of building UI, which makes it easier for developers to create beautiful and performant user interfaces. ## 🔽 Download -To download latest Manager, go [here](https://github.com/revanced/revanced-manager/releases/latest) and install the provided APK file. + +You can obtain ReVanced Manager by downloading it from either [revanced.app/download](https://revanced.app/download) or [GitHub Releases](https://github.com/ReVanced/revanced-manager/releases) ## 📝 Prerequisites -1. Android 8 or higher -2. Does not work on some armv7 devices -## 🔴 Issues -For suggestions and bug reports, open an issue [here](https://github.com/revanced/revanced-manager/issues/new/choose). +For a list of prerequisites, refer to [docs/0_prerequisites.md](docs/0_prerequisites.md) -## 💭 Discussion -If you wish to discuss the Manager, a thread has been made under the [#development](https://discord.com/channels/952946952348270622/1002922226443632761) channel in the Discord server, please note that this thread may be temporary and may be removed in the future. +## 🔴 Issues +For suggestions and bug reports, open an issue [here](https://github.com/revanced/revanced-manager/issues/new/choose). ## 🌐 Translation + [![Crowdin](https://badges.crowdin.net/revanced/localized.svg)](https://crowdin.com/project/revanced) -If you wish to translate ReVanced Manager, we're accepting translations on [Crowdin](https://translate.revanced.app) +We're accepting translations on [Crowdin](https://translate.revanced.app) ## 🛠️ Building Manager from source -1. Setup flutter environment for your [platform](https://docs.flutter.dev/get-started/install) -2. Clone the repository locally -3. Add your github token in gradle.properties like [this](/docs/4_building.md) -4. Open the project in terminal -5. Run `flutter pub get` in terminal -6. Then `flutter packages pub run build_runner build --delete-conflicting-outputs` (Must be done on each git pull) -7. To build release apk run `flutter build apk` + +For instructions on how to build ReVanced Manager from source, refer to [docs/4_building.md](docs/4_building.md) \ No newline at end of file diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000000..15e3660da6 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,77 @@ +<p align="center"> + <picture> + <source + width="256px" + media="(prefers-color-scheme: dark)" + srcset="assets/revanced-headline/revanced-headline-vertical-dark.svg" + > + <img + width="256px" + src="assets/revanced-headline/revanced-headline-vertical-light.svg" + > + </picture> + <br> + <a href="https://revanced.app/"> + <picture> + <source height="24px" media="(prefers-color-scheme: dark)" srcset="assets/revanced-logo/revanced-logo.svg" /> + <img height="24px" src="assets/revanced-logo/revanced-logo.svg" /> + </picture> + </a>    + <a href="https://github.com/ReVanced"> + <picture> + <source height="24px" media="(prefers-color-scheme: dark)" srcset="https://i.ibb.co/dMMmCrW/Git-Hub-Mark.png" /> + <img height="24px" src="https://i.ibb.co/9wV3HGF/Git-Hub-Mark-Light.png" /> + </picture> + </a>    + <a href="http://revanced.app/discord"> + <picture> + <source height="24px" media="(prefers-color-scheme: dark)" srcset="https://user-images.githubusercontent.com/13122796/178032563-d4e084b7-244e-4358-af50-26bde6dd4996.png" /> + <img height="24px" src="https://user-images.githubusercontent.com/13122796/178032563-d4e084b7-244e-4358-af50-26bde6dd4996.png" /> + </picture> + </a>    + <a href="https://reddit.com/r/revancedapp"> + <picture> + <source height="24px" media="(prefers-color-scheme: dark)" srcset="https://user-images.githubusercontent.com/13122796/178032351-9d9d5619-8ef7-470a-9eec-2744ece54553.png" /> + <img height="24px" src="https://user-images.githubusercontent.com/13122796/178032351-9d9d5619-8ef7-470a-9eec-2744ece54553.png" /> + </picture> + </a>    + <a href="https://t.me/app_revanced"> + <picture> + <source height="24px" media="(prefers-color-scheme: dark)" srcset="https://user-images.githubusercontent.com/13122796/178032213-faf25ab8-0bc3-4a94-a730-b524c96df124.png" /> + <img height="24px" src="https://user-images.githubusercontent.com/13122796/178032213-faf25ab8-0bc3-4a94-a730-b524c96df124.png" /> + </picture> + </a>    + <a href="https://x.com/revancedapp"> + <picture> + <source media="(prefers-color-scheme: dark)" srcset="https://user-images.githubusercontent.com/93124920/270180600-7c1b38bf-889b-4d68-bd5e-b9d86f91421a.png"> + <img height="24px" src="https://user-images.githubusercontent.com/93124920/270108715-d80743fa-b330-4809-b1e6-79fbdc60d09c.png" /> + </picture> + </a>    + <a href="https://www.youtube.com/@ReVanced"> + <picture> + <source height="24px" media="(prefers-color-scheme: dark)" srcset="https://user-images.githubusercontent.com/13122796/178032714-c51c7492-0666-44ac-99c2-f003a695ab50.png" /> + <img height="24px" src="https://user-images.githubusercontent.com/13122796/178032714-c51c7492-0666-44ac-99c2-f003a695ab50.png" /> + </picture> + </a> + <br> + <br> + Continuing the legacy of Vanced +</p> + +# 🔒 Security Policy + +This document describes how to report security vulnerabilities for ReVanced Manager. + +## 🚨 Reporting a Vulnerability + +Please open an issue in our [advisory tracker](https://github.com/ReVanced/revanced-manager/security/advisories/new) or reach out privately to us on [Discord](https://discord.gg/revanced). + +If a vulnerability is confirmed and accepted, you can join our [Discord](https://discord.gg/revanced) server to receive a special contributor role. + +### ⏳ Supported Versions + +| Version | Branch | Supported | +| --------------------------------------------------------------------------------------------------------------------------------------- | ----------- | ------------------ | +| ![Latest stable release](https://img.shields.io/github/v/release/ReVanced/revanced-manager?style=for-the-badge "Latest stable release") | main | :white_check_mark: | +| ![Latest version](https://img.shields.io/badge/version-latest-brightgreen?style=for-the-badge "Latest version") | dev | :white_check_mark: | +| ![Latest version](https://img.shields.io/badge/version-latest-brightgreen?style=for-the-badge "Latest version") | compose-dev | :white_check_mark: | diff --git a/analysis_options.yaml b/analysis_options.yaml deleted file mode 100644 index 1c02c24c3a..0000000000 --- a/analysis_options.yaml +++ /dev/null @@ -1,163 +0,0 @@ -# This file configures the analyzer, which statically analyzes Dart code to -# check for errors, warnings, and lints. -# -# The issues identified by the analyzer are surfaced in the UI of Dart-enabled -# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be -# invoked from the command line by running `flutter analyze`. - -# The following line activates a set of recommended lints for Flutter apps, -# packages, and plugins designed to encourage good coding practices. -include: package:flutter_lints/flutter.yaml - -analyzer: - exclude: - - lib/app/app.locator.dart - - lib/app/app.router.dart - - lib/models/patch.g.dart - - lib/models/patched_application.g.dart - -linter: - rules: - - always_declare_return_types - - require_trailing_commas - - always_put_control_body_on_new_line - - always_require_non_null_named_parameters - - always_use_package_imports # we do this commonly - - annotate_overrides - - avoid_bool_literals_in_conditional_expressions - - avoid_double_and_int_checks - - avoid_empty_else - - avoid_equals_and_hash_code_on_mutable_classes - - avoid_escaping_inner_quotes - - avoid_field_initializers_in_const_classes - - avoid_function_literals_in_foreach_calls - - avoid_implementing_value_types - - avoid_init_to_null - - avoid_js_rounded_ints - - avoid_null_checks_in_equality_operators - - avoid_print - - avoid_redundant_argument_values - - avoid_relative_lib_imports - - avoid_renaming_method_parameters - - avoid_return_types_on_setters - - avoid_returning_null - - avoid_returning_null_for_future - - avoid_returning_null_for_void - - avoid_setters_without_getters - - avoid_shadowing_type_parameters - - avoid_single_cascade_in_expression_statements - - avoid_type_to_string - - avoid_types_as_parameter_names - - avoid_unnecessary_containers - - avoid_void_async - - avoid_web_libraries_in_flutter # we use web libraries in web-specific code, and our tests prevent us from using them elsewhere - - await_only_futures - - camel_case_extensions - - camel_case_types - - cancel_subscriptions - - cast_nullable_to_non_nullable - - close_sinks # not reliable enough - - control_flow_in_finally - - curly_braces_in_flow_control_structures - - depend_on_referenced_packages - - deprecated_consistency - - directives_ordering - - empty_catches - - empty_constructor_bodies - - empty_statements - - eol_at_end_of_file - - exhaustive_cases - - file_names - - flutter_style_todos - - hash_and_equals - - implementation_imports - - collection_methods_unrelated_type - - leading_newlines_in_multiline_strings - - library_names - - library_prefixes - - library_private_types_in_public_api - - missing_whitespace_between_adjacent_strings - - no_adjacent_strings_in_list - - no_duplicate_case_values - - no_logic_in_create_state - - non_constant_identifier_names - - noop_primitive_operations - - null_check_on_nullable_type_parameter - - null_closures - - overridden_fields - - package_api_docs - - package_names - - package_prefixed_library_names - - prefer_adjacent_string_concatenation - - prefer_asserts_in_initializer_lists - - prefer_collection_literals - - prefer_conditional_assignment - - prefer_const_constructors - - prefer_const_constructors_in_immutables - - prefer_const_declarations - - prefer_const_literals_to_create_immutables - - prefer_contains - - prefer_final_fields - - prefer_final_in_for_each - - prefer_final_locals - - prefer_for_elements_to_map_fromIterable - - prefer_foreach - - prefer_function_declarations_over_variables - - prefer_generic_function_type_aliases - - prefer_if_elements_to_conditional_expressions - - prefer_if_null_operators - - prefer_initializing_formals - - prefer_inlined_adds - - prefer_interpolation_to_compose_strings - - prefer_is_empty - - prefer_is_not_empty - - prefer_is_not_operator - - prefer_iterable_whereType - - prefer_mixin # Has false positives, see https://github.com/dart-lang/linter/issues/3018 - - prefer_null_aware_method_calls # "call()" is confusing to people new to the language since it's not documented anywhere - - prefer_null_aware_operators - - prefer_single_quotes - - prefer_spread_collections - - prefer_typing_uninitialized_variables - - prefer_void_to_null - - provide_deprecation_message - - recursive_getters - - sized_box_for_whitespace - - slash_for_doc_comments - - sort_child_properties_last - - sort_constructors_first - - sort_unnamed_constructors_first - - test_types_in_equals - - throw_in_finally - - tighten_type_of_initializing_formals - - type_init_formals - - unnecessary_brace_in_string_interps - - unnecessary_const - - unnecessary_getters_setters - - unnecessary_new - - unnecessary_null_aware_assignments - - unnecessary_null_checks - - unnecessary_null_in_if_null_operators - - unnecessary_nullable_for_final_variable_declarations - - unnecessary_overrides - - unnecessary_parenthesis - - unnecessary_statements - - unnecessary_string_escapes - - unnecessary_string_interpolations - - unnecessary_this - - unrelated_type_equality_checks - - unsafe_html - - use_build_context_synchronously - - use_full_hex_values_for_flutter_colors - - use_function_type_syntax_for_parameters - - use_if_null_to_convert_nulls_to_bools - - use_is_even_rather_than_modulo - - use_key_in_widget_constructors - - use_late_for_private_fields_and_variables - - use_named_constants - - use_raw_strings - - use_rethrow_when_possible - - use_setters_to_change_properties - - use_test_throws_matchers - - valid_regexps - - void_checks diff --git a/android/.gitignore b/android/.gitignore deleted file mode 100644 index 6f568019d3..0000000000 --- a/android/.gitignore +++ /dev/null @@ -1,13 +0,0 @@ -gradle-wrapper.jar -/.gradle -/captures/ -/gradlew -/gradlew.bat -/local.properties -GeneratedPluginRegistrant.java - -# Remember to never publicly share your keystore. -# See https://flutter.dev/docs/deployment/android#reference-the-keystore-from-the-app -key.properties -**/*.keystore -**/*.jks diff --git a/android/Gemfile b/android/Gemfile deleted file mode 100644 index 7a118b49be..0000000000 --- a/android/Gemfile +++ /dev/null @@ -1,3 +0,0 @@ -source "https://rubygems.org" - -gem "fastlane" diff --git a/android/app/build.gradle b/android/app/build.gradle deleted file mode 100644 index 8663736a80..0000000000 --- a/android/app/build.gradle +++ /dev/null @@ -1,92 +0,0 @@ -def localProperties = new Properties() -def localPropertiesFile = rootProject.file('local.properties') -if (localPropertiesFile.exists()) { - localPropertiesFile.withReader('UTF-8') { reader -> - localProperties.load(reader) - } -} - -def flutterRoot = localProperties.getProperty('flutter.sdk') -if (flutterRoot == null) { - throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.") -} - -def flutterVersionCode = localProperties.getProperty('flutter.versionCode') -if (flutterVersionCode == null) { - flutterVersionCode = '1' -} - -def flutterVersionName = localProperties.getProperty('flutter.versionName') -if (flutterVersionName == null) { - flutterVersionName = '1.0' -} - -apply plugin: 'com.android.application' -apply plugin: 'kotlin-android' -apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" - -android { - compileSdkVersion flutter.compileSdkVersion - ndkVersion flutter.ndkVersion - - compileOptions { - sourceCompatibility JavaVersion.VERSION_11 - targetCompatibility JavaVersion.VERSION_11 - } - - kotlinOptions { - jvmTarget = '11' - } - - sourceSets { - main.java.srcDirs += 'src/main/kotlin' - } - - defaultConfig { - applicationId "app.revanced.manager.flutter" - minSdkVersion 26 - targetSdkVersion 33 - versionCode flutterVersionCode.toInteger() - versionName flutterVersionName - } - - buildTypes { - release { - resValue "string", "app_name", "ReVanced Manager" - signingConfig signingConfigs.debug - ndk { - abiFilters 'arm64-v8a', 'armeabi-v7a', 'x86_64' - } - } - debug { - shrinkResources false - minifyEnabled false - resValue "string", "app_name", "ReVanced Manager Debug" - applicationIdSuffix ".debug" - signingConfig signingConfigs.debug - ndk { - abiFilters 'arm64-v8a', 'armeabi-v7a', 'x86_64' - } - } - } - - packagingOptions { - exclude '/prebuilt/**' - } -} - -flutter { - source '../..' -} - -dependencies { - implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" - - // ReVanced - implementation "app.revanced:revanced-patcher:11.0.4" - - // Signing & aligning - implementation("org.bouncycastle:bcpkix-jdk15on:1.70") - implementation("com.android.tools.build:apksig:7.2.2") - -} diff --git a/android/app/src/debug/AndroidManifest.xml b/android/app/src/debug/AndroidManifest.xml deleted file mode 100644 index 2abfc8e943..0000000000 --- a/android/app/src/debug/AndroidManifest.xml +++ /dev/null @@ -1,4 +0,0 @@ -<manifest xmlns:android="http://schemas.android.com/apk/res/android" - package="app.revanced.manager.flutter"> - <uses-permission android:name="android.permission.INTERNET"/> -</manifest> diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml deleted file mode 100644 index 8548324123..0000000000 --- a/android/app/src/main/AndroidManifest.xml +++ /dev/null @@ -1,59 +0,0 @@ -<manifest xmlns:android="http://schemas.android.com/apk/res/android" - package="app.revanced.manager.flutter"> - <uses-permission android:name="android.permission.FOREGROUND_SERVICE"/> - <uses-permission android:name="android.permission.INTERNET" /> - - <uses-permission android:name="android.permission.READ_MEDIA_IMAGES" /> - <uses-permission android:name="android.permission.READ_MEDIA_VIDEO" /> - <uses-permission android:name="android.permission.READ_MEDIA_AUDIO" /> - - <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" - android:maxSdkVersion="32" /> - <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" - android:maxSdkVersion="32" /> - <uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" /> - - <uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" /> - <uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" /> - <uses-permission android:name="android.permission.REQUEST_DELETE_PACKAGES" /> - <uses-permission android:name="android.permission.WAKE_LOCK" /> - - <uses-permission android:name="android.permission.QUERY_ALL_PACKAGES" /> - <application - android:label="@string/app_name" - android:name="${applicationName}" - android:icon="@mipmap/ic_launcher" - android:largeHeap="true" - android:requestLegacyExternalStorage="true" - android:extractNativeLibs="true" - android:enableOnBackInvokedCallback="true"> - <activity - android:name=".MainActivity" - android:exported="true" - android:launchMode="singleTop" - android:theme="@style/LaunchTheme" - android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode" - android:hardwareAccelerated="true" - android:windowSoftInputMode="adjustResize"> - <meta-data - android:name="io.flutter.embedding.android.NormalTheme" - android:resource="@style/NormalTheme"/> - <intent-filter> - <action android:name="android.intent.action.MAIN"/> - <category android:name="android.intent.category.LAUNCHER"/> - </intent-filter> - </activity> - <meta-data - android:name="flutterEmbedding" - android:value="2" /> - <provider - android:name="androidx.core.content.FileProvider" - android:authorities="${applicationId}.fileProvider" - android:exported="false" - android:grantUriPermissions="true"> - <meta-data - android:name="android.support.FILE_PROVIDER_PATHS" - android:resource="@xml/file_paths" /> - </provider> - </application> -</manifest> diff --git a/android/app/src/main/kotlin/app/revanced/manager/flutter/MainActivity.kt b/android/app/src/main/kotlin/app/revanced/manager/flutter/MainActivity.kt deleted file mode 100644 index 2c8d77169e..0000000000 --- a/android/app/src/main/kotlin/app/revanced/manager/flutter/MainActivity.kt +++ /dev/null @@ -1,385 +0,0 @@ -package app.revanced.manager.flutter - -import android.os.Build -import android.os.Handler -import android.os.Looper -import androidx.annotation.NonNull -import app.revanced.manager.flutter.utils.Aapt -import app.revanced.manager.flutter.utils.aligning.ZipAligner -import app.revanced.manager.flutter.utils.signing.Signer -import app.revanced.manager.flutter.utils.zip.ZipFile -import app.revanced.manager.flutter.utils.zip.structures.ZipEntry -import app.revanced.patcher.Patcher -import app.revanced.patcher.PatcherOptions -import app.revanced.patcher.extensions.PatchExtensions.compatiblePackages -import app.revanced.patcher.extensions.PatchExtensions.patchName -import app.revanced.patcher.logging.Logger -import app.revanced.patcher.util.patch.PatchBundle -import dalvik.system.DexClassLoader -import io.flutter.embedding.android.FlutterActivity -import io.flutter.embedding.engine.FlutterEngine -import io.flutter.plugin.common.MethodChannel -import java.io.File - -private const val PATCHER_CHANNEL = "app.revanced.manager.flutter/patcher" -private const val INSTALLER_CHANNEL = "app.revanced.manager.flutter/installer" - -class MainActivity : FlutterActivity() { - private val handler = Handler(Looper.getMainLooper()) - private lateinit var installerChannel: MethodChannel - private var cancel: Boolean = false - private var stopResult: MethodChannel.Result? = null - - override fun configureFlutterEngine(@NonNull flutterEngine: FlutterEngine) { - super.configureFlutterEngine(flutterEngine) - val mainChannel = MethodChannel(flutterEngine.dartExecutor.binaryMessenger, PATCHER_CHANNEL) - installerChannel = MethodChannel(flutterEngine.dartExecutor.binaryMessenger, INSTALLER_CHANNEL) - mainChannel.setMethodCallHandler { call, result -> - when (call.method) { - "runPatcher" -> { - val patchBundleFilePath = call.argument<String>("patchBundleFilePath") - val originalFilePath = call.argument<String>("originalFilePath") - val inputFilePath = call.argument<String>("inputFilePath") - val patchedFilePath = call.argument<String>("patchedFilePath") - val outFilePath = call.argument<String>("outFilePath") - val integrationsPath = call.argument<String>("integrationsPath") - val selectedPatches = call.argument<List<String>>("selectedPatches") - val cacheDirPath = call.argument<String>("cacheDirPath") - val keyStoreFilePath = call.argument<String>("keyStoreFilePath") - val keystorePassword = call.argument<String>("keystorePassword") - - if (patchBundleFilePath != null && - originalFilePath != null && - inputFilePath != null && - patchedFilePath != null && - outFilePath != null && - integrationsPath != null && - selectedPatches != null && - cacheDirPath != null && - keyStoreFilePath != null && - keystorePassword != null - ) { - cancel = false - runPatcher( - result, - patchBundleFilePath, - originalFilePath, - inputFilePath, - patchedFilePath, - outFilePath, - integrationsPath, - selectedPatches, - cacheDirPath, - keyStoreFilePath, - keystorePassword - ) - } else { - result.notImplemented() - } - } - "stopPatcher" -> { - cancel = true - stopResult = result - } - else -> result.notImplemented() - } - } - } - - private fun runPatcher( - result: MethodChannel.Result, - patchBundleFilePath: String, - originalFilePath: String, - inputFilePath: String, - patchedFilePath: String, - outFilePath: String, - integrationsPath: String, - selectedPatches: List<String>, - cacheDirPath: String, - keyStoreFilePath: String, - keystorePassword: String - ) { - val originalFile = File(originalFilePath) - val inputFile = File(inputFilePath) - val patchedFile = File(patchedFilePath) - val outFile = File(outFilePath) - val integrations = File(integrationsPath) - val keyStoreFile = File(keyStoreFilePath) - - Thread { - try { - handler.post { - installerChannel.invokeMethod( - "update", - mapOf( - "progress" to 0.1, - "header" to "", - "log" to "Copying original apk" - ) - ) - } - - if(cancel) { - handler.post { stopResult!!.success(null) } - return@Thread - } - - originalFile.copyTo(inputFile, true) - - handler.post { - installerChannel.invokeMethod( - "update", - mapOf( - "progress" to 0.2, - "header" to "Unpacking apk...", - "log" to "Unpacking input apk" - ) - ) - } - - if(cancel) { - handler.post { stopResult!!.success(null) } - return@Thread - } - - val patcher = - Patcher( - PatcherOptions( - inputFile, - cacheDirPath, - Aapt.binary(applicationContext).absolutePath, - cacheDirPath, - logger = ManagerLogger() - ) - ) - - if(cancel) { - handler.post { stopResult!!.success(null) } - return@Thread - } - - handler.post { - installerChannel.invokeMethod( - "update", - mapOf("progress" to 0.3, "header" to "", "log" to "") - ) - } - handler.post { - installerChannel.invokeMethod( - "update", - mapOf( - "progress" to 0.4, - "header" to "Merging integrations...", - "log" to "Merging integrations" - ) - ) - } - - if(cancel) { - handler.post { stopResult!!.success(null) } - return@Thread - } - - patcher.addIntegrations(listOf(integrations)) {} - - if(cancel) { - handler.post { stopResult!!.success(null) } - return@Thread - } - - handler.post { - installerChannel.invokeMethod( - "update", - mapOf( - "progress" to 0.5, - "header" to "Applying patches...", - "log" to "" - ) - ) - } - - if(cancel) { - handler.post { stopResult!!.success(null) } - return@Thread - } - - val patches = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.CUPCAKE) { - PatchBundle.Dex( - patchBundleFilePath, - DexClassLoader( - patchBundleFilePath, - cacheDirPath, - null, - javaClass.classLoader - ) - ).loadPatches().filter { patch -> - (patch.compatiblePackages?.any { it.name == patcher.context.packageMetadata.packageName } == true || patch.compatiblePackages.isNullOrEmpty()) && - selectedPatches.any { it == patch.patchName } - } - } else { - TODO("VERSION.SDK_INT < CUPCAKE") - } - - if(cancel) { - handler.post { stopResult!!.success(null) } - return@Thread - } - - patcher.addPatches(patches) - patcher.executePatches().forEach { (patch, res) -> - if (res.isSuccess) { - val msg = "Applied $patch" - handler.post { - installerChannel.invokeMethod( - "update", - mapOf( - "progress" to 0.5, - "header" to "", - "log" to msg - ) - ) - } - if(cancel) { - handler.post { stopResult!!.success(null) } - return@Thread - } - return@forEach - } - val msg = - "Failed to apply $patch: " + "${res.exceptionOrNull()!!.message ?: res.exceptionOrNull()!!.cause!!::class.simpleName}" - handler.post { - installerChannel.invokeMethod( - "update", - mapOf("progress" to 0.5, "header" to "", "log" to msg) - ) - } - if(cancel) { - handler.post { stopResult!!.success(null) } - return@Thread - } - } - - handler.post { - installerChannel.invokeMethod( - "update", - mapOf( - "progress" to 0.7, - "header" to "Repacking apk...", - "log" to "Repacking patched apk" - ) - ) - } - if(cancel) { - handler.post { stopResult!!.success(null) } - return@Thread - } - val res = patcher.save() - ZipFile(patchedFile).use { file -> - res.dexFiles.forEach { - if(cancel) { - handler.post { stopResult!!.success(null) } - return@Thread - } - file.addEntryCompressData( - ZipEntry.createWithName(it.name), - it.stream.readBytes() - ) - } - res.resourceFile?.let { - file.copyEntriesFromFileAligned( - ZipFile(it), - ZipAligner::getEntryAlignment - ) - } - file.copyEntriesFromFileAligned( - ZipFile(inputFile), - ZipAligner::getEntryAlignment - ) - } - if(cancel) { - handler.post { stopResult!!.success(null) } - return@Thread - } - handler.post { - installerChannel.invokeMethod( - "update", - mapOf( - "progress" to 0.9, - "header" to "Signing apk...", - "log" to "" - ) - ) - } - - try { - Signer("ReVanced", keystorePassword).signApk( - patchedFile, - outFile, - keyStoreFile - ) - } catch (e: Exception) { - //log to console - print("Error signing apk: ${e.message}") - e.printStackTrace() - } - - handler.post { - installerChannel.invokeMethod( - "update", - mapOf( - "progress" to 1.0, - "header" to "Finished!", - "log" to "Finished!" - ) - ) - } - } catch (ex: Throwable) { - val stack = ex.stackTraceToString() - handler.post { - installerChannel.invokeMethod( - "update", - mapOf( - "progress" to -100.0, - "header" to "Aborted...", - "log" to "An error occurred! Aborted\nError:\n$stack" - ) - ) - } - } - handler.post { result.success(null) } - }.start() - } - - inner class ManagerLogger : Logger { - override fun error(msg: String) { - handler.post { - installerChannel - .invokeMethod( - "update", - mapOf("progress" to -1.0, "header" to "", "log" to msg) - ) - } - } - - override fun warn(msg: String) { - handler.post { - installerChannel.invokeMethod( - "update", - mapOf("progress" to -1.0, "header" to "", "log" to msg) - ) - } - } - - override fun info(msg: String) { - handler.post { - installerChannel.invokeMethod( - "update", - mapOf("progress" to -1.0, "header" to "", "log" to msg) - ) - } - } - - override fun trace(_msg: String) { /* unused */ - } - } -} diff --git a/android/app/src/main/kotlin/app/revanced/manager/flutter/utils/Aapt.kt b/android/app/src/main/kotlin/app/revanced/manager/flutter/utils/Aapt.kt deleted file mode 100644 index 72198e58fa..0000000000 --- a/android/app/src/main/kotlin/app/revanced/manager/flutter/utils/Aapt.kt +++ /dev/null @@ -1,12 +0,0 @@ -package app.revanced.manager.flutter.utils - -import android.content.Context -import java.io.File - -object Aapt { - fun binary(context: Context): File { - return File(context.applicationInfo.nativeLibraryDir).resolveAapt() - } -} - -private fun File.resolveAapt() = resolve(list { _, f -> !File(f).isDirectory && f.contains("aapt") }!!.first()) \ No newline at end of file diff --git a/android/app/src/main/kotlin/app/revanced/manager/flutter/utils/aligning/ZipAligner.kt b/android/app/src/main/kotlin/app/revanced/manager/flutter/utils/aligning/ZipAligner.kt deleted file mode 100644 index 088aad5993..0000000000 --- a/android/app/src/main/kotlin/app/revanced/manager/flutter/utils/aligning/ZipAligner.kt +++ /dev/null @@ -1,11 +0,0 @@ -package app.revanced.manager.flutter.utils.aligning - -import app.revanced.manager.flutter.utils.zip.structures.ZipEntry - -internal object ZipAligner { - private const val DEFAULT_ALIGNMENT = 4 - private const val LIBRARY_ALIGNMENT = 4096 - - fun getEntryAlignment(entry: ZipEntry): Int? = - if (entry.compression.toUInt() != 0u) null else if (entry.fileName.endsWith(".so")) LIBRARY_ALIGNMENT else DEFAULT_ALIGNMENT -} diff --git a/android/app/src/main/kotlin/app/revanced/manager/flutter/utils/signing/Signer.kt b/android/app/src/main/kotlin/app/revanced/manager/flutter/utils/signing/Signer.kt deleted file mode 100644 index 1e1a08a21c..0000000000 --- a/android/app/src/main/kotlin/app/revanced/manager/flutter/utils/signing/Signer.kt +++ /dev/null @@ -1,74 +0,0 @@ -package app.revanced.manager.flutter.utils.signing - -import com.android.apksig.ApkSigner -import org.bouncycastle.asn1.x500.X500Name -import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo -import org.bouncycastle.cert.X509v3CertificateBuilder -import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter -import org.bouncycastle.jce.provider.BouncyCastleProvider -import org.bouncycastle.operator.ContentSigner -import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder -import java.io.File -import java.io.FileInputStream -import java.io.FileOutputStream -import java.math.BigInteger -import java.security.* -import java.security.cert.X509Certificate -import java.util.* - -internal class Signer( - private val cn: String, password: String -) { - private val passwordCharArray = password.toCharArray() - private fun newKeystore(out: File) { - val (publicKey, privateKey) = createKey() - val privateKS = KeyStore.getInstance("BKS", "BC") - privateKS.load(null, passwordCharArray) - privateKS.setKeyEntry("alias", privateKey, passwordCharArray, arrayOf(publicKey)) - privateKS.store(FileOutputStream(out), passwordCharArray) - } - - private fun createKey(): Pair<X509Certificate, PrivateKey> { - val gen = KeyPairGenerator.getInstance("RSA") - gen.initialize(2048) - val pair = gen.generateKeyPair() - var serialNumber: BigInteger - do serialNumber = - BigInteger.valueOf(SecureRandom().nextLong()) while (serialNumber < BigInteger.ZERO) - val x500Name = X500Name("CN=$cn") - val builder = X509v3CertificateBuilder( - x500Name, - serialNumber, - Date(System.currentTimeMillis() - 1000L * 60L * 60L * 24L * 30L), - Date(System.currentTimeMillis() + 1000L * 60L * 60L * 24L * 366L * 30L), - Locale.ENGLISH, - x500Name, - SubjectPublicKeyInfo.getInstance(pair.public.encoded) - ) - val signer: ContentSigner = JcaContentSignerBuilder("SHA256withRSA").build(pair.private) - return JcaX509CertificateConverter().getCertificate(builder.build(signer)) to pair.private - } - - fun signApk(input: File, output: File, ks: File) { - Security.addProvider(BouncyCastleProvider()) - - if (!ks.exists()) newKeystore(ks) - - val keyStore = KeyStore.getInstance("BKS", "BC") - FileInputStream(ks).use { fis -> keyStore.load(fis, null) } - val alias = keyStore.aliases().nextElement() - - val config = ApkSigner.SignerConfig.Builder( - cn, - keyStore.getKey(alias, passwordCharArray) as PrivateKey, - listOf(keyStore.getCertificate(alias) as X509Certificate) - ).build() - - val signer = ApkSigner.Builder(listOf(config)) - signer.setCreatedBy(cn) - signer.setInputApk(input) - signer.setOutputApk(output) - - signer.build().sign() - } -} diff --git a/android/app/src/main/kotlin/app/revanced/manager/flutter/utils/zip/Extensions.kt b/android/app/src/main/kotlin/app/revanced/manager/flutter/utils/zip/Extensions.kt deleted file mode 100644 index 3ff0516de5..0000000000 --- a/android/app/src/main/kotlin/app/revanced/manager/flutter/utils/zip/Extensions.kt +++ /dev/null @@ -1,35 +0,0 @@ -@file:Suppress("unused") - -package app.revanced.manager.flutter.utils.zip - -import java.io.DataInput -import java.io.DataOutput -import java.nio.ByteBuffer - -fun UInt.toLittleEndian() = - (((this.toInt() and 0xff000000.toInt()) shr 24) or ((this.toInt() and 0x00ff0000) shr 8) or ((this.toInt() and 0x0000ff00) shl 8) or (this.toInt() shl 24)).toUInt() - -fun UShort.toLittleEndian() = (this.toUInt() shl 16).toLittleEndian().toUShort() - -fun UInt.toBigEndian() = (((this.toInt() and 0xff) shl 24) or ((this.toInt() and 0xff00) shl 8) - or ((this.toInt() and 0x00ff0000) ushr 8) or (this.toInt() ushr 24)).toUInt() - -fun UShort.toBigEndian() = (this.toUInt() shl 16).toBigEndian().toUShort() - -fun ByteBuffer.getUShort() = this.short.toUShort() -fun ByteBuffer.getUInt() = this.int.toUInt() - -fun ByteBuffer.putUShort(ushort: UShort): ByteBuffer = this.putShort(ushort.toShort()) -fun ByteBuffer.putUInt(uint: UInt): ByteBuffer = this.putInt(uint.toInt()) - -fun DataInput.readUShort() = this.readShort().toUShort() -fun DataInput.readUInt() = this.readInt().toUInt() - -fun DataOutput.writeUShort(ushort: UShort) = this.writeShort(ushort.toInt()) -fun DataOutput.writeUInt(uint: UInt) = this.writeInt(uint.toInt()) - -fun DataInput.readUShortLE() = this.readUShort().toBigEndian() -fun DataInput.readUIntLE() = this.readUInt().toBigEndian() - -fun DataOutput.writeUShortLE(ushort: UShort) = this.writeUShort(ushort.toLittleEndian()) -fun DataOutput.writeUIntLE(uint: UInt) = this.writeUInt(uint.toLittleEndian()) diff --git a/android/app/src/main/kotlin/app/revanced/manager/flutter/utils/zip/ZipFile.kt b/android/app/src/main/kotlin/app/revanced/manager/flutter/utils/zip/ZipFile.kt deleted file mode 100644 index 2330938b3b..0000000000 --- a/android/app/src/main/kotlin/app/revanced/manager/flutter/utils/zip/ZipFile.kt +++ /dev/null @@ -1,176 +0,0 @@ -package app.revanced.manager.flutter.utils.zip - -import app.revanced.manager.flutter.utils.zip.structures.ZipEndRecord -import app.revanced.manager.flutter.utils.zip.structures.ZipEntry -import java.io.Closeable -import java.io.File -import java.io.RandomAccessFile -import java.nio.ByteBuffer -import java.nio.channels.FileChannel -import java.util.zip.CRC32 -import java.util.zip.Deflater - -class ZipFile(file: File) : Closeable { - var entries: MutableList<ZipEntry> = mutableListOf() - - private val filePointer: RandomAccessFile = RandomAccessFile(file, "rw") - private var CDNeedsRewrite = false - - private val compressionLevel = 5 - - init { - //if file isn't empty try to load entries - if (file.length() > 0) { - val endRecord = findEndRecord() - - if (endRecord.diskNumber > 0u || endRecord.totalEntries != endRecord.diskEntries) - throw IllegalArgumentException("Multi-file archives are not supported") - - entries = readEntries(endRecord).toMutableList() - } - - //seek back to start for writing - filePointer.seek(0) - } - - private fun findEndRecord(): ZipEndRecord { - //look from end to start since end record is at the end - for (i in filePointer.length() - 1 downTo 0) { - filePointer.seek(i) - //possible beginning of signature - if (filePointer.readByte() == 0x50.toByte()) { - //seek back to get the full int - filePointer.seek(i) - val possibleSignature = filePointer.readUIntLE() - if (possibleSignature == ZipEndRecord.ECD_SIGNATURE) { - filePointer.seek(i) - return ZipEndRecord.fromECD(filePointer) - } - } - } - - throw Exception("Couldn't find end record") - } - - private fun readEntries(endRecord: ZipEndRecord): List<ZipEntry> { - filePointer.seek(endRecord.centralDirectoryStartOffset.toLong()) - - val numberOfEntries = endRecord.diskEntries.toInt() - - return buildList(numberOfEntries) { - for (i in 1..numberOfEntries) { - add( - ZipEntry.fromCDE(filePointer).also - { - //for some reason the local extra field can be different from the central one - it.readLocalExtra( - filePointer.channel.map( - FileChannel.MapMode.READ_ONLY, - it.localHeaderOffset.toLong() + 28, - 2 - ) - ) - }) - } - } - } - - private fun writeCD() { - val CDStart = filePointer.channel.position().toUInt() - - entries.forEach { - filePointer.channel.write(it.toCDE()) - } - - val entriesCount = entries.size.toUShort() - - val endRecord = ZipEndRecord( - 0u, - 0u, - entriesCount, - entriesCount, - filePointer.channel.position().toUInt() - CDStart, - CDStart, - "" - ) - - filePointer.channel.write(endRecord.toECD()) - } - - private fun addEntry(entry: ZipEntry, data: ByteBuffer) { - CDNeedsRewrite = true - - entry.localHeaderOffset = filePointer.channel.position().toUInt() - - filePointer.channel.write(entry.toLFH()) - filePointer.channel.write(data) - - entries.add(entry) - } - - fun addEntryCompressData(entry: ZipEntry, data: ByteArray) { - val compressor = Deflater(compressionLevel, true) - compressor.setInput(data) - compressor.finish() - - val uncompressedSize = data.size - val compressedData = - ByteArray(uncompressedSize) //i'm guessing compression won't make the data bigger - - val compressedDataLength = compressor.deflate(compressedData) - val compressedBuffer = - ByteBuffer.wrap(compressedData.take(compressedDataLength).toByteArray()) - - compressor.end() - - val crc = CRC32() - crc.update(data) - - entry.compression = 8u //deflate compression - entry.uncompressedSize = uncompressedSize.toUInt() - entry.compressedSize = compressedDataLength.toUInt() - entry.crc32 = crc.value.toUInt() - - addEntry(entry, compressedBuffer) - } - - private fun addEntryCopyData(entry: ZipEntry, data: ByteBuffer, alignment: Int? = null) { - alignment?.let { - //calculate where data would end up - val dataOffset = filePointer.filePointer + entry.LFHSize - - val mod = dataOffset % alignment - - //wrong alignment - if (mod != 0L) { - //add padding at end of extra field - entry.localExtraField = - entry.localExtraField.copyOf((entry.localExtraField.size + (alignment - mod)).toInt()) - } - } - - addEntry(entry, data) - } - - fun getDataForEntry(entry: ZipEntry): ByteBuffer { - return filePointer.channel.map( - FileChannel.MapMode.READ_ONLY, - entry.dataOffset.toLong(), - entry.compressedSize.toLong() - ) - } - - fun copyEntriesFromFileAligned(file: ZipFile, entryAlignment: (entry: ZipEntry) -> Int?) { - for (entry in file.entries) { - if (entries.any { it.fileName == entry.fileName }) continue //don't add duplicates - - val data = file.getDataForEntry(entry) - addEntryCopyData(entry, data, entryAlignment(entry)) - } - } - - override fun close() { - if (CDNeedsRewrite) writeCD() - filePointer.close() - } -} diff --git a/android/app/src/main/kotlin/app/revanced/manager/flutter/utils/zip/structures/ZipEndRecord.kt b/android/app/src/main/kotlin/app/revanced/manager/flutter/utils/zip/structures/ZipEndRecord.kt deleted file mode 100644 index e7b9b58e26..0000000000 --- a/android/app/src/main/kotlin/app/revanced/manager/flutter/utils/zip/structures/ZipEndRecord.kt +++ /dev/null @@ -1,78 +0,0 @@ -package app.revanced.manager.flutter.utils.zip.structures - -import app.revanced.manager.flutter.utils.zip.putUInt -import app.revanced.manager.flutter.utils.zip.putUShort -import app.revanced.manager.flutter.utils.zip.readUIntLE -import app.revanced.manager.flutter.utils.zip.readUShortLE -import java.io.DataInput -import java.nio.ByteBuffer -import java.nio.ByteOrder - -data class ZipEndRecord( - val diskNumber: UShort, - val startingDiskNumber: UShort, - val diskEntries: UShort, - val totalEntries: UShort, - val centralDirectorySize: UInt, - val centralDirectoryStartOffset: UInt, - val fileComment: String, -) { - - companion object { - const val ECD_HEADER_SIZE = 22 - const val ECD_SIGNATURE = 0x06054b50u - - fun fromECD(input: DataInput): ZipEndRecord { - val signature = input.readUIntLE() - - if (signature != ECD_SIGNATURE) - throw IllegalArgumentException("Input doesn't start with end record signature") - - val diskNumber = input.readUShortLE() - val startingDiskNumber = input.readUShortLE() - val diskEntries = input.readUShortLE() - val totalEntries = input.readUShortLE() - val centralDirectorySize = input.readUIntLE() - val centralDirectoryStartOffset = input.readUIntLE() - val fileCommentLength = input.readUShortLE() - var fileComment = "" - - if (fileCommentLength > 0u) { - val fileCommentBytes = ByteArray(fileCommentLength.toInt()) - input.readFully(fileCommentBytes) - fileComment = fileCommentBytes.toString(Charsets.UTF_8) - } - - return ZipEndRecord( - diskNumber, - startingDiskNumber, - diskEntries, - totalEntries, - centralDirectorySize, - centralDirectoryStartOffset, - fileComment - ) - } - } - - fun toECD(): ByteBuffer { - val commentBytes = fileComment.toByteArray(Charsets.UTF_8) - - val buffer = ByteBuffer.allocate(ECD_HEADER_SIZE + commentBytes.size) - .also { it.order(ByteOrder.LITTLE_ENDIAN) } - - buffer.putUInt(ECD_SIGNATURE) - buffer.putUShort(diskNumber) - buffer.putUShort(startingDiskNumber) - buffer.putUShort(diskEntries) - buffer.putUShort(totalEntries) - buffer.putUInt(centralDirectorySize) - buffer.putUInt(centralDirectoryStartOffset) - buffer.putUShort(commentBytes.size.toUShort()) - - buffer.put(commentBytes) - - buffer.flip() - return buffer - } -} diff --git a/android/app/src/main/kotlin/app/revanced/manager/flutter/utils/zip/structures/ZipEntry.kt b/android/app/src/main/kotlin/app/revanced/manager/flutter/utils/zip/structures/ZipEntry.kt deleted file mode 100644 index bda1398e7c..0000000000 --- a/android/app/src/main/kotlin/app/revanced/manager/flutter/utils/zip/structures/ZipEntry.kt +++ /dev/null @@ -1,190 +0,0 @@ -package app.revanced.manager.flutter.utils.zip.structures - -import app.revanced.manager.flutter.utils.zip.* -import java.io.DataInput -import java.nio.ByteBuffer -import java.nio.ByteOrder - -data class ZipEntry( - val version: UShort, - val versionNeeded: UShort, - val flags: UShort, - var compression: UShort, - val modificationTime: UShort, - val modificationDate: UShort, - var crc32: UInt, - var compressedSize: UInt, - var uncompressedSize: UInt, - val diskNumber: UShort, - val internalAttributes: UShort, - val externalAttributes: UInt, - var localHeaderOffset: UInt, - val fileName: String, - val extraField: ByteArray, - val fileComment: String, - var localExtraField: ByteArray = ByteArray(0), //separate for alignment -) { - val LFHSize: Int - get() = LFH_HEADER_SIZE + fileName.toByteArray(Charsets.UTF_8).size + localExtraField.size - - val dataOffset: UInt - get() = localHeaderOffset + LFHSize.toUInt() - - companion object { - const val CDE_HEADER_SIZE = 46 - const val CDE_SIGNATURE = 0x02014b50u - - const val LFH_HEADER_SIZE = 30 - const val LFH_SIGNATURE = 0x04034b50u - - fun createWithName(fileName: String): ZipEntry { - return ZipEntry( - 0x1403u, //made by unix, version 20 - 0u, - 0u, - 0u, - 0x0821u, //seems to be static time google uses, no idea - 0x0221u, //same as above - 0u, - 0u, - 0u, - 0u, - 0u, - 0u, - 0u, - fileName, - ByteArray(0), - "" - ) - } - - fun fromCDE(input: DataInput): ZipEntry { - val signature = input.readUIntLE() - - if (signature != CDE_SIGNATURE) - throw IllegalArgumentException("Input doesn't start with central directory entry signature") - - val version = input.readUShortLE() - val versionNeeded = input.readUShortLE() - var flags = input.readUShortLE() - val compression = input.readUShortLE() - val modificationTime = input.readUShortLE() - val modificationDate = input.readUShortLE() - val crc32 = input.readUIntLE() - val compressedSize = input.readUIntLE() - val uncompressedSize = input.readUIntLE() - val fileNameLength = input.readUShortLE() - var fileName = "" - val extraFieldLength = input.readUShortLE() - val extraField = ByteArray(extraFieldLength.toInt()) - val fileCommentLength = input.readUShortLE() - var fileComment = "" - val diskNumber = input.readUShortLE() - val internalAttributes = input.readUShortLE() - val externalAttributes = input.readUIntLE() - val localHeaderOffset = input.readUIntLE() - - val variableFieldsLength = - fileNameLength.toInt() + extraFieldLength.toInt() + fileCommentLength.toInt() - - if (variableFieldsLength > 0) { - val fileNameBytes = ByteArray(fileNameLength.toInt()) - input.readFully(fileNameBytes) - fileName = fileNameBytes.toString(Charsets.UTF_8) - - input.readFully(extraField) - - val fileCommentBytes = ByteArray(fileCommentLength.toInt()) - input.readFully(fileCommentBytes) - fileComment = fileCommentBytes.toString(Charsets.UTF_8) - } - - flags = (flags and 0b1000u.inv() - .toUShort()) //disable data descriptor flag as they are not used - - return ZipEntry( - version, - versionNeeded, - flags, - compression, - modificationTime, - modificationDate, - crc32, - compressedSize, - uncompressedSize, - diskNumber, - internalAttributes, - externalAttributes, - localHeaderOffset, - fileName, - extraField, - fileComment, - ) - } - } - - fun readLocalExtra(buffer: ByteBuffer) { - buffer.order(ByteOrder.LITTLE_ENDIAN) - localExtraField = ByteArray(buffer.getUShort().toInt()) - } - - fun toLFH(): ByteBuffer { - val nameBytes = fileName.toByteArray(Charsets.UTF_8) - - val buffer = ByteBuffer.allocate(LFH_HEADER_SIZE + nameBytes.size + localExtraField.size) - .also { it.order(ByteOrder.LITTLE_ENDIAN) } - - buffer.putUInt(LFH_SIGNATURE) - buffer.putUShort(versionNeeded) - buffer.putUShort(flags) - buffer.putUShort(compression) - buffer.putUShort(modificationTime) - buffer.putUShort(modificationDate) - buffer.putUInt(crc32) - buffer.putUInt(compressedSize) - buffer.putUInt(uncompressedSize) - buffer.putUShort(nameBytes.size.toUShort()) - buffer.putUShort(localExtraField.size.toUShort()) - - buffer.put(nameBytes) - buffer.put(localExtraField) - - buffer.flip() - return buffer - } - - fun toCDE(): ByteBuffer { - val nameBytes = fileName.toByteArray(Charsets.UTF_8) - val commentBytes = fileComment.toByteArray(Charsets.UTF_8) - - val buffer = - ByteBuffer.allocate(CDE_HEADER_SIZE + nameBytes.size + extraField.size + commentBytes.size) - .also { it.order(ByteOrder.LITTLE_ENDIAN) } - - buffer.putUInt(CDE_SIGNATURE) - buffer.putUShort(version) - buffer.putUShort(versionNeeded) - buffer.putUShort(flags) - buffer.putUShort(compression) - buffer.putUShort(modificationTime) - buffer.putUShort(modificationDate) - buffer.putUInt(crc32) - buffer.putUInt(compressedSize) - buffer.putUInt(uncompressedSize) - buffer.putUShort(nameBytes.size.toUShort()) - buffer.putUShort(extraField.size.toUShort()) - buffer.putUShort(commentBytes.size.toUShort()) - buffer.putUShort(diskNumber) - buffer.putUShort(internalAttributes) - buffer.putUInt(externalAttributes) - buffer.putUInt(localHeaderOffset) - - buffer.put(nameBytes) - buffer.put(extraField) - buffer.put(commentBytes) - - buffer.flip() - return buffer - } -} - diff --git a/android/app/src/main/res/drawable-v21/launch_background.xml b/android/app/src/main/res/drawable-v21/launch_background.xml deleted file mode 100644 index f74085f3f6..0000000000 --- a/android/app/src/main/res/drawable-v21/launch_background.xml +++ /dev/null @@ -1,12 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<!-- Modify this file to customize your launch splash screen --> -<layer-list xmlns:android="http://schemas.android.com/apk/res/android"> - <item android:drawable="?android:colorBackground" /> - - <!-- You can insert your own image assets here --> - <!-- <item> - <bitmap - android:gravity="center" - android:src="@mipmap/launch_image" /> - </item> --> -</layer-list> diff --git a/android/app/src/main/res/drawable/launch_background.xml b/android/app/src/main/res/drawable/launch_background.xml deleted file mode 100644 index 304732f884..0000000000 --- a/android/app/src/main/res/drawable/launch_background.xml +++ /dev/null @@ -1,12 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<!-- Modify this file to customize your launch splash screen --> -<layer-list xmlns:android="http://schemas.android.com/apk/res/android"> - <item android:drawable="@android:color/white" /> - - <!-- You can insert your own image assets here --> - <!-- <item> - <bitmap - android:gravity="center" - android:src="@mipmap/launch_image" /> - </item> --> -</layer-list> diff --git a/android/app/src/main/res/values-night-v31/styles.xml b/android/app/src/main/res/values-night-v31/styles.xml deleted file mode 100644 index 581c5fca71..0000000000 --- a/android/app/src/main/res/values-night-v31/styles.xml +++ /dev/null @@ -1,19 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<resources> - <!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is on --> - <style name="LaunchTheme" parent="@android:style/Theme.Black.NoTitleBar"> - <!-- Show a splash screen on the activity. Automatically removed when - the Flutter engine draws its first frame --> - <item name="android:windowBackground">@drawable/launch_background</item> - <item name="android:windowSplashScreenAnimatedIcon">@drawable/ic_launcher_round</item> - </style> - <!-- Theme applied to the Android Window as soon as the process has started. - This theme determines the color of the Android Window while your - Flutter UI initializes, as well as behind your Flutter UI while its - running. - - This Theme is only used starting with V2 of Flutter's Android embedding. --> - <style name="NormalTheme" parent="@android:style/Theme.Black.NoTitleBar"> - <item name="android:windowBackground">?android:colorBackground</item> - </style> -</resources> diff --git a/android/app/src/main/res/values-night/styles.xml b/android/app/src/main/res/values-night/styles.xml deleted file mode 100644 index 06952be745..0000000000 --- a/android/app/src/main/res/values-night/styles.xml +++ /dev/null @@ -1,18 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<resources> - <!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is on --> - <style name="LaunchTheme" parent="@android:style/Theme.Black.NoTitleBar"> - <!-- Show a splash screen on the activity. Automatically removed when - the Flutter engine draws its first frame --> - <item name="android:windowBackground">@drawable/launch_background</item> - </style> - <!-- Theme applied to the Android Window as soon as the process has started. - This theme determines the color of the Android Window while your - Flutter UI initializes, as well as behind your Flutter UI while its - running. - - This Theme is only used starting with V2 of Flutter's Android embedding. --> - <style name="NormalTheme" parent="@android:style/Theme.Black.NoTitleBar"> - <item name="android:windowBackground">?android:colorBackground</item> - </style> -</resources> diff --git a/android/app/src/main/res/values-v31/styles.xml b/android/app/src/main/res/values-v31/styles.xml deleted file mode 100644 index 41f95cf120..0000000000 --- a/android/app/src/main/res/values-v31/styles.xml +++ /dev/null @@ -1,20 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<resources> - <!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is off --> - <style name="LaunchTheme" parent="@android:style/Theme.Light.NoTitleBar"> - <!-- Show a splash screen on the activity. Automatically removed when - the Flutter engine draws its first frame --> - <item name="android:windowBackground">@drawable/launch_background</item> - <item name="android:windowSplashScreenAnimatedIcon">@drawable/ic_launcher_round</item> - </style> - <!-- Theme applied to the Android Window as soon as the process has started. - This theme determines the color of the Android Window while your - Flutter UI initializes, as well as behind your Flutter UI while its - running. - - This Theme is only used starting with V2 of Flutter's Android embedding. --> - <style name="NormalTheme" parent="@android:style/Theme.Light.NoTitleBar"> - <item name="android:windowBackground">?android:colorBackground</item> - </style> -</resources> - diff --git a/android/app/src/main/res/values/ic_launcher_background.xml b/android/app/src/main/res/values/ic_launcher_background.xml deleted file mode 100644 index 74008be87f..0000000000 --- a/android/app/src/main/res/values/ic_launcher_background.xml +++ /dev/null @@ -1,4 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<resources> - <color name="ic_launcher_background">#1B1B1B</color> -</resources> \ No newline at end of file diff --git a/android/app/src/main/res/values/styles.xml b/android/app/src/main/res/values/styles.xml deleted file mode 100644 index cb1ef88056..0000000000 --- a/android/app/src/main/res/values/styles.xml +++ /dev/null @@ -1,18 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<resources> - <!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is off --> - <style name="LaunchTheme" parent="@android:style/Theme.Light.NoTitleBar"> - <!-- Show a splash screen on the activity. Automatically removed when - the Flutter engine draws its first frame --> - <item name="android:windowBackground">@drawable/launch_background</item> - </style> - <!-- Theme applied to the Android Window as soon as the process has started. - This theme determines the color of the Android Window while your - Flutter UI initializes, as well as behind your Flutter UI while its - running. - - This Theme is only used starting with V2 of Flutter's Android embedding. --> - <style name="NormalTheme" parent="@android:style/Theme.Light.NoTitleBar"> - <item name="android:windowBackground">?android:colorBackground</item> - </style> -</resources> diff --git a/android/app/src/main/res/xml/file_paths.xml b/android/app/src/main/res/xml/file_paths.xml deleted file mode 100644 index d2e2c8deaf..0000000000 --- a/android/app/src/main/res/xml/file_paths.xml +++ /dev/null @@ -1,4 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<paths> - <cache-path name="cache" path="." /> -</paths> diff --git a/android/app/src/profile/AndroidManifest.xml b/android/app/src/profile/AndroidManifest.xml deleted file mode 100644 index 2abfc8e943..0000000000 --- a/android/app/src/profile/AndroidManifest.xml +++ /dev/null @@ -1,4 +0,0 @@ -<manifest xmlns:android="http://schemas.android.com/apk/res/android" - package="app.revanced.manager.flutter"> - <uses-permission android:name="android.permission.INTERNET"/> -</manifest> diff --git a/android/build.gradle b/android/build.gradle deleted file mode 100644 index 88ac491ced..0000000000 --- a/android/build.gradle +++ /dev/null @@ -1,37 +0,0 @@ -buildscript { - ext.kotlin_version = '1.7.10' - repositories { - google() - mavenCentral() - } - - dependencies { - classpath 'com.android.tools.build:gradle:7.1.3' - classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" - } -} - -allprojects { - repositories { - google() - mavenCentral() - maven { - url = uri("https://maven.pkg.github.com/revanced/revanced-patcher") - credentials { - username = (project.findProperty("gpr.user") ?: System.getenv("GITHUB_ACTOR")) as String - password = (project.findProperty("gpr.key") ?: System.getenv("GITHUB_TOKEN")) as String - } - } - mavenLocal() - } -} - -rootProject.buildDir = '../build' -subprojects { - project.buildDir = "${rootProject.buildDir}/${project.name}" - project.evaluationDependsOn(':app') -} - -tasks.register("clean", Delete) { - delete rootProject.buildDir -} diff --git a/android/gradle.properties b/android/gradle.properties deleted file mode 100644 index 4b11638cfe..0000000000 --- a/android/gradle.properties +++ /dev/null @@ -1,6 +0,0 @@ -org.gradle.jvmargs=-Xmx1536M -XX:+UseParallelGC -org.gradle.parallel=true -org.gradle.daemon=true -org.gradle.caching=true -android.useAndroidX=true -android.enableJetifier=true diff --git a/android/settings.gradle b/android/settings.gradle deleted file mode 100644 index 44e62bcf06..0000000000 --- a/android/settings.gradle +++ /dev/null @@ -1,11 +0,0 @@ -include ':app' - -def localPropertiesFile = new File(rootProject.projectDir, "local.properties") -def properties = new Properties() - -assert localPropertiesFile.exists() -localPropertiesFile.withReader("UTF-8") { reader -> properties.load(reader) } - -def flutterSdkPath = properties.getProperty("flutter.sdk") -assert flutterSdkPath != null, "flutter.sdk not set in local.properties" -apply from: "$flutterSdkPath/packages/flutter_tools/gradle/app_plugin_loader.gradle" diff --git a/app/.gitignore b/app/.gitignore new file mode 100644 index 0000000000..42afabfd2a --- /dev/null +++ b/app/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/app/build.gradle.kts b/app/build.gradle.kts new file mode 100644 index 0000000000..211d1ca531 --- /dev/null +++ b/app/build.gradle.kts @@ -0,0 +1,211 @@ +import kotlin.random.Random + +plugins { + alias(libs.plugins.android.application) + alias(libs.plugins.kotlin.android) + alias(libs.plugins.kotlin.serialization) + alias(libs.plugins.kotlin.parcelize) + alias(libs.plugins.compose.compiler) + alias(libs.plugins.devtools) + alias(libs.plugins.about.libraries) +} + +android { + namespace = "app.revanced.manager" + compileSdk = 35 + buildToolsVersion = "35.0.1" + + defaultConfig { + applicationId = "app.revanced.manager" + minSdk = 26 + targetSdk = 35 + versionCode = 1 + versionName = "0.0.1" + vectorDrawables.useSupportLibrary = true + } + + buildTypes { + debug { + applicationIdSuffix = ".debug" + resValue("string", "app_name", "ReVanced Manager (dev)") + isPseudoLocalesEnabled = true + + buildConfigField("long", "BUILD_ID", "${Random.nextLong()}L") + } + + release { + if (!project.hasProperty("noProguard")) { + isMinifyEnabled = true + isShrinkResources = true + proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro") + } + + if (project.hasProperty("signAsDebug")) { + applicationIdSuffix = ".debug" + resValue("string", "app_name", "ReVanced Manager Debug") + signingConfig = signingConfigs.getByName("debug") + + isPseudoLocalesEnabled = true + } + + buildConfigField("long", "BUILD_ID", "0L") + } + } + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 + } + + dependenciesInfo { + includeInApk = false + includeInBundle = false + } + + packaging { + resources.excludes.addAll(listOf( + "/prebuilt/**", + "META-INF/DEPENDENCIES", + "META-INF/**.version", + "DebugProbesKt.bin", + "kotlin-tooling-metadata.json", + "org/bouncycastle/pqc/**.properties", + "org/bouncycastle/x509/**.properties", + )) + jniLibs { + useLegacyPackaging = true + } + } + + ksp { + arg("room.schemaLocation", "$projectDir/schemas") + } + + kotlinOptions { + jvmTarget = "17" + } + + buildFeatures { + compose = true + aidl = true + buildConfig = true + } + + android { + androidResources { + generateLocaleConfig = true + } + } + + externalNativeBuild { + cmake { + path = file("src/main/cpp/CMakeLists.txt") + version = "3.22.1" + } + } +} + +kotlin { + jvmToolchain(17) +} + +dependencies { + + // AndroidX Core + implementation(libs.androidx.ktx) + implementation(libs.runtime.ktx) + implementation(libs.runtime.compose) + implementation(libs.splash.screen) + implementation(libs.activity.compose) + implementation(libs.work.runtime.ktx) + implementation(libs.preferences.datastore) + implementation(libs.appcompat) + + // Compose + implementation(platform(libs.compose.bom)) + implementation(libs.compose.ui) + implementation(libs.compose.ui.preview) + implementation(libs.compose.ui.tooling) + implementation(libs.compose.livedata) + implementation(libs.compose.material.icons.extended) + implementation(libs.compose.material3) + implementation(libs.navigation.compose) + + // Accompanist + implementation(libs.accompanist.drawablepainter) + + // Placeholder + implementation(libs.placeholder.material3) + + // HTML Scraper + implementation(libs.skrapeit.dsl) + implementation(libs.skrapeit.parser) + + // Coil (async image loading, network image) + implementation(libs.coil.compose) + implementation(libs.coil.appiconloader) + + // KotlinX + implementation(libs.kotlinx.serialization.json) + implementation(libs.kotlinx.collection.immutable) + implementation(libs.kotlinx.datetime) + + // Room + implementation(libs.room.runtime) + implementation(libs.room.ktx) + annotationProcessor(libs.room.compiler) + ksp(libs.room.compiler) + + // ReVanced + implementation(libs.revanced.patcher) + implementation(libs.revanced.library) + + // Downloader plugins + implementation(project(":downloader-plugin")) + + // Native processes + implementation(libs.kotlin.process) + + // HiddenAPI + compileOnly(libs.hidden.api.stub) + + // LibSU + implementation(libs.libsu.core) + implementation(libs.libsu.service) + implementation(libs.libsu.nio) + + // Koin + implementation(libs.koin.android) + implementation(libs.koin.compose) + implementation(libs.koin.compose.navigation) + implementation(libs.koin.workmanager) + + // Licenses + implementation(libs.about.libraries) + + // Ktor + implementation(libs.ktor.core) + implementation(libs.ktor.logging) + implementation(libs.ktor.okhttp) + implementation(libs.ktor.content.negotiation) + implementation(libs.ktor.serialization) + + // Markdown + implementation(libs.markdown.renderer) + + // Fading Edges + implementation(libs.fading.edges) + + // Scrollbars + implementation(libs.scrollbars) + + // EnumUtil + implementation(libs.enumutil) + ksp(libs.enumutil.ksp) + + // Reorderable lists + implementation(libs.reorderable) + + // Compose Icons + implementation(libs.compose.icons.fontawesome) +} diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro new file mode 100644 index 0000000000..b9b9c1aff0 --- /dev/null +++ b/app/proguard-rules.pro @@ -0,0 +1,63 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle.kts.kts. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +-dontobfuscate + +# Required for serialization to work properly +-if @kotlinx.serialization.Serializable class ** +-keepclassmembers class <1> { + static <1>$Companion Companion; +} +-if @kotlinx.serialization.Serializable class ** { + static **$* *; +} +-keepclassmembers class <2>$<3> { + kotlinx.serialization.KSerializer serializer(...); +} +-if @kotlinx.serialization.Serializable class ** { + public static ** INSTANCE; +} +-keepclassmembers class <1> { + public static <1> INSTANCE; + kotlinx.serialization.KSerializer serializer(...); +} + +# This required for the process runtime. +-keep class app.revanced.manager.patcher.runtime.process.* { + *; +} +# Required for the patcher to function correctly +-keep class app.revanced.patcher.** { + *; +} +-keep class brut.** { + *; +} +-keep class org.xmlpull.** { + *; +} +-keep class kotlin.** { + *; +} +-keep class org.jf.** { + *; +} +-keep class com.android.** { + *; +} +-keep class app.revanced.manager.plugin.** { + *; +} + +-dontwarn com.google.auto.value.** +-dontwarn java.awt.** +-dontwarn javax.** +-dontwarn org.slf4j.** +-dontwarn it.skrape.fetcher.* +-dontwarn com.google.j2objc.annotations.* + +-keepattributes RuntimeVisibleAnnotations,AnnotationDefault \ No newline at end of file diff --git a/app/schemas/app.revanced.manager.data.room.AppDatabase/1.json b/app/schemas/app.revanced.manager.data.room.AppDatabase/1.json new file mode 100644 index 0000000000..fd83a51ed1 --- /dev/null +++ b/app/schemas/app.revanced.manager.data.room.AppDatabase/1.json @@ -0,0 +1,429 @@ +{ + "formatVersion": 1, + "database": { + "version": 1, + "identityHash": "d0119047505da435972c5247181de675", + "entities": [ + { + "tableName": "patch_bundles", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uid` INTEGER NOT NULL, `name` TEXT NOT NULL, `version` TEXT, `source` TEXT NOT NULL, `auto_update` INTEGER NOT NULL, PRIMARY KEY(`uid`))", + "fields": [ + { + "fieldPath": "uid", + "columnName": "uid", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "version", + "columnName": "version", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "source", + "columnName": "source", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "autoUpdate", + "columnName": "auto_update", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "uid" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "patch_selections", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uid` INTEGER NOT NULL, `patch_bundle` INTEGER NOT NULL, `package_name` TEXT NOT NULL, PRIMARY KEY(`uid`), FOREIGN KEY(`patch_bundle`) REFERENCES `patch_bundles`(`uid`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "uid", + "columnName": "uid", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "patchBundle", + "columnName": "patch_bundle", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "packageName", + "columnName": "package_name", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "uid" + ] + }, + "indices": [ + { + "name": "index_patch_selections_patch_bundle_package_name", + "unique": true, + "columnNames": [ + "patch_bundle", + "package_name" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_patch_selections_patch_bundle_package_name` ON `${TABLE_NAME}` (`patch_bundle`, `package_name`)" + } + ], + "foreignKeys": [ + { + "table": "patch_bundles", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "patch_bundle" + ], + "referencedColumns": [ + "uid" + ] + } + ] + }, + { + "tableName": "selected_patches", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`selection` INTEGER NOT NULL, `patch_name` TEXT NOT NULL, PRIMARY KEY(`selection`, `patch_name`), FOREIGN KEY(`selection`) REFERENCES `patch_selections`(`uid`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "selection", + "columnName": "selection", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "patchName", + "columnName": "patch_name", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "selection", + "patch_name" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "patch_selections", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "selection" + ], + "referencedColumns": [ + "uid" + ] + } + ] + }, + { + "tableName": "downloaded_app", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`package_name` TEXT NOT NULL, `version` TEXT NOT NULL, `directory` TEXT NOT NULL, `last_used` INTEGER NOT NULL, PRIMARY KEY(`package_name`, `version`))", + "fields": [ + { + "fieldPath": "packageName", + "columnName": "package_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "version", + "columnName": "version", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "directory", + "columnName": "directory", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastUsed", + "columnName": "last_used", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "package_name", + "version" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "installed_app", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`current_package_name` TEXT NOT NULL, `original_package_name` TEXT NOT NULL, `version` TEXT NOT NULL, `install_type` TEXT NOT NULL, PRIMARY KEY(`current_package_name`))", + "fields": [ + { + "fieldPath": "currentPackageName", + "columnName": "current_package_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "originalPackageName", + "columnName": "original_package_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "version", + "columnName": "version", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "installType", + "columnName": "install_type", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "current_package_name" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "applied_patch", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`package_name` TEXT NOT NULL, `bundle` INTEGER NOT NULL, `patch_name` TEXT NOT NULL, PRIMARY KEY(`package_name`, `bundle`, `patch_name`), FOREIGN KEY(`package_name`) REFERENCES `installed_app`(`current_package_name`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`bundle`) REFERENCES `patch_bundles`(`uid`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "packageName", + "columnName": "package_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bundle", + "columnName": "bundle", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "patchName", + "columnName": "patch_name", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "package_name", + "bundle", + "patch_name" + ] + }, + "indices": [ + { + "name": "index_applied_patch_bundle", + "unique": false, + "columnNames": [ + "bundle" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_applied_patch_bundle` ON `${TABLE_NAME}` (`bundle`)" + } + ], + "foreignKeys": [ + { + "table": "installed_app", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "package_name" + ], + "referencedColumns": [ + "current_package_name" + ] + }, + { + "table": "patch_bundles", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "bundle" + ], + "referencedColumns": [ + "uid" + ] + } + ] + }, + { + "tableName": "option_groups", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uid` INTEGER NOT NULL, `patch_bundle` INTEGER NOT NULL, `package_name` TEXT NOT NULL, PRIMARY KEY(`uid`), FOREIGN KEY(`patch_bundle`) REFERENCES `patch_bundles`(`uid`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "uid", + "columnName": "uid", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "patchBundle", + "columnName": "patch_bundle", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "packageName", + "columnName": "package_name", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "uid" + ] + }, + "indices": [ + { + "name": "index_option_groups_patch_bundle_package_name", + "unique": true, + "columnNames": [ + "patch_bundle", + "package_name" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_option_groups_patch_bundle_package_name` ON `${TABLE_NAME}` (`patch_bundle`, `package_name`)" + } + ], + "foreignKeys": [ + { + "table": "patch_bundles", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "patch_bundle" + ], + "referencedColumns": [ + "uid" + ] + } + ] + }, + { + "tableName": "options", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`group` INTEGER NOT NULL, `patch_name` TEXT NOT NULL, `key` TEXT NOT NULL, `value` TEXT NOT NULL, PRIMARY KEY(`group`, `patch_name`, `key`), FOREIGN KEY(`group`) REFERENCES `option_groups`(`uid`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "group", + "columnName": "group", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "patchName", + "columnName": "patch_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "key", + "columnName": "key", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "value", + "columnName": "value", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "group", + "patch_name", + "key" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "option_groups", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "group" + ], + "referencedColumns": [ + "uid" + ] + } + ] + }, + { + "tableName": "trusted_downloader_plugins", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`package_name` TEXT NOT NULL, `signature` BLOB NOT NULL, PRIMARY KEY(`package_name`))", + "fields": [ + { + "fieldPath": "packageName", + "columnName": "package_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "signature", + "columnName": "signature", + "affinity": "BLOB", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "package_name" + ] + }, + "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, 'd0119047505da435972c5247181de675')" + ] + } +} \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000000..0404d045d8 --- /dev/null +++ b/app/src/main/AndroidManifest.xml @@ -0,0 +1,78 @@ +<?xml version="1.0" encoding="utf-8"?> +<manifest xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:tools="http://schemas.android.com/tools"> + + <permission + android:name="app.revanced.manager.permission.PLUGIN_HOST" + android:protectionLevel="signature" + android:label="@string/plugin_host_permission_label" + android:description="@string/plugin_host_permission_description" + /> + + <uses-permission android:name="app.revanced.manager.permission.PLUGIN_HOST" /> + <uses-permission android:name="android.permission.QUERY_ALL_PACKAGES" + tools:ignore="QueryAllPackagesPermission" /> + <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" /> + <uses-permission android:name="android.permission.INTERNET" /> + <uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" /> + <uses-permission android:name="android.permission.REQUEST_DELETE_PACKAGES" /> + <uses-permission android:name="android.permission.FOREGROUND_SERVICE" /> + <uses-permission android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE" /> + <uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" /> + <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" android:maxSdkVersion="29" /> + <uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" + tools:ignore="ScopedStorage" /> + <uses-permission android:name="android.permission.WAKE_LOCK" /> + + <application + android:name=".ManagerApplication" + android:allowBackup="true" + android:dataExtractionRules="@xml/data_extraction_rules" + android:largeHeap="true" + android:fullBackupContent="@xml/backup_rules" + android:icon="@mipmap/ic_launcher" + android:label="@string/app_name" + android:supportsRtl="true" + android:theme="@style/Theme.ReVancedManager" + android:enableOnBackInvokedCallback="true" + tools:targetApi="34"> + + <activity + android:name=".MainActivity" + android:exported="true" + android:theme="@style/Theme.ReVancedManager"> + + <intent-filter> + <action android:name="android.intent.action.MAIN" /> + <category android:name="android.intent.category.LAUNCHER" /> + </intent-filter> + </activity> + + <activity android:name=".plugin.downloader.webview.WebViewActivity" android:exported="false" android:theme="@style/Theme.WebViewActivity" /> + + <service android:name=".service.InstallService" /> + <service android:name=".service.UninstallService" /> + + <service + android:name="androidx.work.impl.foreground.SystemForegroundService" + android:foregroundServiceType="specialUse" + android:exported="false" + tools:node="merge"> + <property + android:name="android.app.PROPERTY_SPECIAL_USE_FGS_SUBTYPE" + android:value="patching" + /> + </service> + + <provider + android:name="androidx.startup.InitializationProvider" + android:authorities="${applicationId}.androidx-startup" + android:exported="false" + tools:node="merge"> + <meta-data + android:name="androidx.work.WorkManagerInitializer" + android:value="androidx.startup" + tools:node="remove" /> + </provider> + </application> +</manifest> \ No newline at end of file diff --git a/app/src/main/aidl/app/revanced/manager/IRootSystemService.aidl b/app/src/main/aidl/app/revanced/manager/IRootSystemService.aidl new file mode 100644 index 0000000000..5dbb41c6d8 --- /dev/null +++ b/app/src/main/aidl/app/revanced/manager/IRootSystemService.aidl @@ -0,0 +1,8 @@ +// IRootService.aidl +package app.revanced.manager; + +// Declare any non-default types here with import statements + +interface IRootSystemService { + IBinder getFileSystemService(); +} \ No newline at end of file diff --git a/app/src/main/aidl/app/revanced/manager/patcher/runtime/process/IPatcherEvents.aidl b/app/src/main/aidl/app/revanced/manager/patcher/runtime/process/IPatcherEvents.aidl new file mode 100644 index 0000000000..27a4f61b2a --- /dev/null +++ b/app/src/main/aidl/app/revanced/manager/patcher/runtime/process/IPatcherEvents.aidl @@ -0,0 +1,11 @@ +// IPatcherEvents.aidl +package app.revanced.manager.patcher.runtime.process; + +// Interface for sending events back to the main app process. +oneway interface IPatcherEvents { + void log(String level, String msg); + void patchSucceeded(); + void progress(String name, String state, String msg); + // The patching process has ended. The exceptionStackTrace is null if it finished successfully. + void finished(String exceptionStackTrace); +} \ No newline at end of file diff --git a/app/src/main/aidl/app/revanced/manager/patcher/runtime/process/IPatcherProcess.aidl b/app/src/main/aidl/app/revanced/manager/patcher/runtime/process/IPatcherProcess.aidl new file mode 100644 index 0000000000..f938ca6235 --- /dev/null +++ b/app/src/main/aidl/app/revanced/manager/patcher/runtime/process/IPatcherProcess.aidl @@ -0,0 +1,14 @@ +// IPatcherProcess.aidl +package app.revanced.manager.patcher.runtime.process; + +import app.revanced.manager.patcher.runtime.process.Parameters; +import app.revanced.manager.patcher.runtime.process.IPatcherEvents; + +interface IPatcherProcess { + // Returns BuildConfig.BUILD_ID, which is used to ensure the main app and runner process are running the same code. + long buildId(); + // Makes the patcher process exit with code 0 + oneway void exit(); + // Starts patching. + oneway void start(in Parameters parameters, IPatcherEvents events); +} \ No newline at end of file diff --git a/app/src/main/aidl/app/revanced/manager/patcher/runtime/process/Parameters.aidl b/app/src/main/aidl/app/revanced/manager/patcher/runtime/process/Parameters.aidl new file mode 100644 index 0000000000..a1e8bee78d --- /dev/null +++ b/app/src/main/aidl/app/revanced/manager/patcher/runtime/process/Parameters.aidl @@ -0,0 +1,4 @@ +// Parameters.aidl +package app.revanced.manager.patcher.runtime.process; + +parcelable Parameters; \ No newline at end of file diff --git a/app/src/main/assets/root/module.prop b/app/src/main/assets/root/module.prop new file mode 100644 index 0000000000..05a5a159dd --- /dev/null +++ b/app/src/main/assets/root/module.prop @@ -0,0 +1,6 @@ +id=__PKG_NAME__-ReVanced +name=__LABEL__ ReVanced +version=__VERSION__ +versionCode=0 +author=ReVanced +description=Mounts the patched APK on top of the original one \ No newline at end of file diff --git a/app/src/main/assets/root/service.sh b/app/src/main/assets/root/service.sh new file mode 100644 index 0000000000..dc3bcb5f45 --- /dev/null +++ b/app/src/main/assets/root/service.sh @@ -0,0 +1,40 @@ +#!/system/bin/sh +DIR=${0%/*} + +package_name="__PKG_NAME__" +version="__VERSION__" + +rm "$DIR/log" + +{ + +until [ "$(getprop sys.boot_completed)" = 1 ]; do sleep 5; done +sleep 5 + +base_path="$DIR/$package_name.apk" +stock_path="$(pm path "$package_name" | grep base | sed 's/package://g')" +stock_version="$(dumpsys package "$package_name" | grep versionName | cut -d "=" -f2)" + +echo "base_path: $base_path" +echo "stock_path: $stock_path" +echo "base_version: $version" +echo "stock_version: $stock_version" + +if mount | grep -q "$stock_path" ; then + echo "Not mounting as stock path is already mounted" + exit 1 +fi + +if [ "$version" != "$stock_version" ]; then + echo "Not mounting as versions don't match" + exit 1 +fi + +if [ -z "$stock_path" ]; then + echo "Not mounting as app info could not be loaded" + exit 1 +fi + +mount -o bind "$base_path" "$stock_path" + +} >> "$DIR/log" diff --git a/app/src/main/cpp/CMakeLists.txt b/app/src/main/cpp/CMakeLists.txt new file mode 100644 index 0000000000..64793f8fe5 --- /dev/null +++ b/app/src/main/cpp/CMakeLists.txt @@ -0,0 +1,38 @@ + +# For more information about using CMake with Android Studio, read the +# documentation: https://d.android.com/studio/projects/add-native-code.html. +# For more examples on how to use CMake, see https://github.com/android/ndk-samples. + +# Sets the minimum CMake version required for this project. +cmake_minimum_required(VERSION 3.22.1) + +# Declares the project name. The project name can be accessed via ${ PROJECT_NAME}, +# Since this is the top level CMakeLists.txt, the project name is also accessible +# with ${CMAKE_PROJECT_NAME} (both CMake variables are in-sync within the top level +# build script scope). +project("prop_override") + +# Creates and names a library, sets it as either STATIC +# or SHARED, and provides the relative paths to its source code. +# You can define multiple libraries, and CMake builds them for you. +# Gradle automatically packages shared libraries with your APK. +# +# In this top level CMakeLists.txt, ${CMAKE_PROJECT_NAME} is used to define +# the target library name; in the sub-module's CMakeLists.txt, ${PROJECT_NAME} +# is preferred for the same purpose. +# +# In order to load a library into your app from Java/Kotlin, you must call +# System.loadLibrary() and pass the name of the library defined here; +# for GameActivity/NativeActivity derived applications, the same library name must be +# used in the AndroidManifest.xml file. +add_library(${CMAKE_PROJECT_NAME} SHARED + # List C/C++ source files with relative paths to this CMakeLists.txt. + prop_override.cpp) + +# Specifies libraries CMake should link to your target library. You +# can link libraries from various origins, such as libraries defined in this +# build script, prebuilt third-party libraries, or Android system libraries. +target_link_libraries(${CMAKE_PROJECT_NAME} + # List libraries link to the target library + android + log) diff --git a/app/src/main/cpp/prop_override.cpp b/app/src/main/cpp/prop_override.cpp new file mode 100644 index 0000000000..b314ccd117 --- /dev/null +++ b/app/src/main/cpp/prop_override.cpp @@ -0,0 +1,62 @@ +// Library for overriding Android system properties via environment variables. +// +// Usage: LD_PRELOAD=prop_override.so PROP_dalvik.vm.heapsize=123M getprop dalvik.vm.heapsize +// Output: 123M +#include <string> +#include <cstring> +#include <cstdlib> +#include <dlfcn.h> + +// Source: https://android.googlesource.com/platform/system/core/+/100b08a848d018eeb1caa5d5e7c7c2aaac65da15/libcutils/include/cutils/properties.h +#define PROP_VALUE_MAX 92 +// This is the mangled name of "android::base::GetProperty". +#define GET_PROPERTY_MANGLED_NAME "_ZN7android4base11GetPropertyERKNSt3__112basic_stringIcNS1_11char_traitsIcEENS1_9allocatorIcEEEES9_" + +extern "C" typedef int (*property_get_ptr)(const char *, char *, const char *); +typedef std::string (*GetProperty_ptr)(const std::string &, const std::string &); + +char *GetPropOverride(const std::string &key) { + auto envKey = "PROP_" + key; + + return getenv(envKey.c_str()); +} + +// See: https://android.googlesource.com/platform/system/core/+/100b08a848d018eeb1caa5d5e7c7c2aaac65da15/libcutils/properties.cpp +extern "C" int property_get(const char *key, char *value, const char *default_value) { + auto replacement = GetPropOverride(std::string(key)); + if (replacement) { + int len = strnlen(replacement, PROP_VALUE_MAX); + + strncpy(value, replacement, len); + return len; + } + + static property_get_ptr original = NULL; + if (!original) { + // Get the address of the original function. + original = reinterpret_cast<property_get_ptr>(dlsym(RTLD_NEXT, "property_get")); + } + + return original(key, value, default_value); +} + +// Defining android::base::GetProperty ourselves won't work because std::string has a slightly different "path" in the NDK version of the C++ standard library. +// We can get around this by forcing the function to adopt a specific name using the asm keyword. +std::string GetProperty(const std::string &, const std::string &) asm(GET_PROPERTY_MANGLED_NAME); + + +// See: https://android.googlesource.com/platform/system/libbase/+/1a34bb67c4f3ba0a1ea6f4f20ac9fe117ba4fe64/properties.cpp +// This isn't used for the properties we want to override, but property_get is deprecated so that could change in the future. +std::string GetProperty(const std::string &key, const std::string &default_value) { + auto replacement = GetPropOverride(key); + if (replacement) { + return std::string(replacement); + } + + static GetProperty_ptr original = NULL; + if (!original) { + original = reinterpret_cast<GetProperty_ptr>(dlsym(RTLD_NEXT, GET_PROPERTY_MANGLED_NAME)); + } + + return original(key, default_value); +} diff --git a/app/src/main/java/app/revanced/manager/MainActivity.kt b/app/src/main/java/app/revanced/manager/MainActivity.kt new file mode 100644 index 0000000000..60399f10f1 --- /dev/null +++ b/app/src/main/java/app/revanced/manager/MainActivity.kt @@ -0,0 +1,335 @@ +package app.revanced.manager + +import android.content.ActivityNotFoundException +import android.os.Bundle +import android.os.Parcelable +import androidx.activity.ComponentActivity +import androidx.activity.compose.rememberLauncherForActivityResult +import androidx.activity.compose.setContent +import androidx.activity.enableEdgeToEdge +import androidx.activity.result.contract.ActivityResultContracts +import androidx.compose.animation.ExperimentalAnimationApi +import androidx.compose.animation.slideInHorizontally +import androidx.compose.animation.slideOutHorizontally +import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen +import androidx.core.view.WindowCompat +import androidx.lifecycle.lifecycleScope +import androidx.navigation.NavBackStackEntry +import androidx.navigation.NavController +import androidx.navigation.compose.NavHost +import androidx.navigation.compose.composable +import androidx.navigation.compose.navigation +import androidx.navigation.compose.rememberNavController +import androidx.navigation.toRoute +import app.revanced.manager.ui.model.navigation.AppSelector +import app.revanced.manager.ui.model.navigation.ComplexParameter +import app.revanced.manager.ui.model.navigation.Dashboard +import app.revanced.manager.ui.model.navigation.InstalledApplicationInfo +import app.revanced.manager.ui.model.navigation.Patcher +import app.revanced.manager.ui.model.navigation.SelectedApplicationInfo +import app.revanced.manager.ui.model.navigation.Settings +import app.revanced.manager.ui.model.navigation.Update +import app.revanced.manager.ui.screen.AppSelectorScreen +import app.revanced.manager.ui.screen.DashboardScreen +import app.revanced.manager.ui.screen.InstalledAppInfoScreen +import app.revanced.manager.ui.screen.PatcherScreen +import app.revanced.manager.ui.screen.PatchesSelectorScreen +import app.revanced.manager.ui.screen.RequiredOptionsScreen +import app.revanced.manager.ui.screen.SelectedAppInfoScreen +import app.revanced.manager.ui.screen.SettingsScreen +import app.revanced.manager.ui.screen.UpdateScreen +import app.revanced.manager.ui.screen.settings.AboutSettingsScreen +import app.revanced.manager.ui.screen.settings.AdvancedSettingsScreen +import app.revanced.manager.ui.screen.settings.ContributorScreen +import app.revanced.manager.ui.screen.settings.DeveloperOptionsScreen +import app.revanced.manager.ui.screen.settings.DownloadsSettingsScreen +import app.revanced.manager.ui.screen.settings.GeneralSettingsScreen +import app.revanced.manager.ui.screen.settings.ImportExportSettingsScreen +import app.revanced.manager.ui.screen.settings.LicensesScreen +import app.revanced.manager.ui.screen.settings.update.ChangelogsScreen +import app.revanced.manager.ui.screen.settings.update.UpdatesSettingsScreen +import app.revanced.manager.ui.theme.ReVancedManagerTheme +import app.revanced.manager.ui.theme.Theme +import app.revanced.manager.ui.viewmodel.MainViewModel +import app.revanced.manager.ui.viewmodel.SelectedAppInfoViewModel +import app.revanced.manager.util.EventEffect +import kotlinx.coroutines.launch +import org.koin.androidx.compose.koinViewModel +import org.koin.androidx.compose.navigation.koinNavViewModel +import org.koin.core.parameter.parametersOf +import org.koin.androidx.viewmodel.ext.android.getViewModel as getActivityViewModel + +class MainActivity : ComponentActivity() { + @ExperimentalAnimationApi + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + WindowCompat.setDecorFitsSystemWindows(window, false) + enableEdgeToEdge() + installSplashScreen() + + val vm: MainViewModel = getActivityViewModel() + + setContent { + val launcher = rememberLauncherForActivityResult( + ActivityResultContracts.StartActivityForResult(), + onResult = vm::applyLegacySettings + ) + val theme by vm.prefs.theme.getAsState() + val dynamicColor by vm.prefs.dynamicColor.getAsState() + + EventEffect(vm.legacyImportActivityFlow) { + try { + launcher.launch(it) + } catch (_: ActivityNotFoundException) { + } + } + + ReVancedManagerTheme( + darkTheme = theme == Theme.SYSTEM && isSystemInDarkTheme() || theme == Theme.DARK, + dynamicColor = dynamicColor + ) { + ReVancedManager(vm) + } + } + } +} + +@Composable +private fun ReVancedManager(vm: MainViewModel) { + val navController = rememberNavController() + + EventEffect(vm.appSelectFlow) { app -> + navController.navigateComplex( + SelectedApplicationInfo, + SelectedApplicationInfo.ViewModelParams(app) + ) + } + + NavHost( + navController = navController, + startDestination = Dashboard, + enterTransition = { slideInHorizontally(initialOffsetX = { it }) }, + exitTransition = { slideOutHorizontally(targetOffsetX = { -it / 3 }) }, + popEnterTransition = { slideInHorizontally(initialOffsetX = { -it / 3 }) }, + popExitTransition = { slideOutHorizontally(targetOffsetX = { it }) }, + ) { + composable<Dashboard> { + DashboardScreen( + onSettingsClick = { navController.navigate(Settings) }, + onAppSelectorClick = { + navController.navigate(AppSelector) + }, + onUpdateClick = { + navController.navigate(Update()) + }, + onDownloaderPluginClick = { + navController.navigate(Settings.Downloads) + }, + onAppClick = { packageName -> + navController.navigate(InstalledApplicationInfo(packageName)) + } + ) + } + + composable<InstalledApplicationInfo> { + val data = it.toRoute<InstalledApplicationInfo>() + + InstalledAppInfoScreen( + onPatchClick = vm::selectApp, + onBackClick = navController::popBackStack, + viewModel = koinViewModel { parametersOf(data.packageName) } + ) + } + + composable<AppSelector> { + AppSelectorScreen( + onSelect = vm::selectApp, + onStorageSelect = vm::selectApp, + onBackClick = navController::popBackStack + ) + } + + composable<Patcher> { + PatcherScreen( + onBackClick = { + navController.navigate(route = Dashboard) { + launchSingleTop = true + popUpTo<Dashboard> { + inclusive = false + } + } + }, + vm = koinViewModel { parametersOf(it.getComplexArg<Patcher.ViewModelParams>()) } + ) + } + + composable<Update> { + val data = it.toRoute<Update>() + + UpdateScreen( + onBackClick = navController::popBackStack, + vm = koinViewModel { parametersOf(data.downloadOnScreenEntry) } + ) + } + + navigation<SelectedApplicationInfo>(startDestination = SelectedApplicationInfo.Main) { + composable<SelectedApplicationInfo.Main> { + val parentBackStackEntry = navController.navGraphEntry(it) + val data = + parentBackStackEntry.getComplexArg<SelectedApplicationInfo.ViewModelParams>() + val viewModel = + koinNavViewModel<SelectedAppInfoViewModel>(viewModelStoreOwner = parentBackStackEntry) { + parametersOf(data) + } + + SelectedAppInfoScreen( + onBackClick = navController::popBackStack, + onPatchClick = { + it.lifecycleScope.launch { + navController.navigateComplex( + Patcher, + viewModel.getPatcherParams() + ) + } + }, + onPatchSelectorClick = { app, patches, options -> + navController.navigateComplex( + SelectedApplicationInfo.PatchesSelector, + SelectedApplicationInfo.PatchesSelector.ViewModelParams( + app, + patches, + options + ) + ) + }, + onRequiredOptions = { app, patches, options -> + navController.navigateComplex( + SelectedApplicationInfo.RequiredOptions, + SelectedApplicationInfo.PatchesSelector.ViewModelParams( + app, + patches, + options + ) + ) + }, + vm = viewModel + ) + } + + composable<SelectedApplicationInfo.PatchesSelector> { + val data = + it.getComplexArg<SelectedApplicationInfo.PatchesSelector.ViewModelParams>() + val selectedAppInfoVm = koinNavViewModel<SelectedAppInfoViewModel>( + viewModelStoreOwner = navController.navGraphEntry(it) + ) + + PatchesSelectorScreen( + onBackClick = navController::popBackStack, + onSave = { patches, options -> + selectedAppInfoVm.updateConfiguration(patches, options) + navController.popBackStack() + }, + vm = koinViewModel { parametersOf(data) } + ) + } + + composable<SelectedApplicationInfo.RequiredOptions> { + val data = + it.getComplexArg<SelectedApplicationInfo.PatchesSelector.ViewModelParams>() + val selectedAppInfoVm = koinNavViewModel<SelectedAppInfoViewModel>( + viewModelStoreOwner = navController.navGraphEntry(it) + ) + + RequiredOptionsScreen( + onBackClick = navController::popBackStack, + onContinue = { patches, options -> + selectedAppInfoVm.updateConfiguration(patches, options) + it.lifecycleScope.launch { + navController.navigateComplex( + Patcher, + selectedAppInfoVm.getPatcherParams() + ) + } + }, + vm = koinViewModel { parametersOf(data) } + ) + } + } + + navigation<Settings>(startDestination = Settings.Main) { + composable<Settings.Main> { + SettingsScreen( + onBackClick = navController::popBackStack, + navigate = navController::navigate + ) + } + + composable<Settings.General> { + GeneralSettingsScreen(onBackClick = navController::popBackStack) + } + + composable<Settings.Advanced> { + AdvancedSettingsScreen(onBackClick = navController::popBackStack) + } + + composable<Settings.Updates> { + UpdatesSettingsScreen( + onBackClick = navController::popBackStack, + onChangelogClick = { navController.navigate(Settings.Changelogs) }, + onUpdateClick = { navController.navigate(Update()) } + ) + } + + composable<Settings.Downloads> { + DownloadsSettingsScreen(onBackClick = navController::popBackStack) + } + + composable<Settings.ImportExport> { + ImportExportSettingsScreen(onBackClick = navController::popBackStack) + } + + composable<Settings.About> { + AboutSettingsScreen( + onBackClick = navController::popBackStack, + navigate = navController::navigate + ) + } + + composable<Settings.Changelogs> { + ChangelogsScreen(onBackClick = navController::popBackStack) + } + + composable<Settings.Contributors> { + ContributorScreen(onBackClick = navController::popBackStack) + } + + composable<Settings.Licenses> { + LicensesScreen(onBackClick = navController::popBackStack) + } + + composable<Settings.DeveloperOptions> { + DeveloperOptionsScreen(onBackClick = navController::popBackStack) + } + } + } +} + +@Composable +private fun NavController.navGraphEntry(entry: NavBackStackEntry) = + remember(entry) { getBackStackEntry(entry.destination.parent!!.id) } + +// Androidx Navigation does not support storing complex types in route objects, so we have to store them inside the saved state handle of the back stack entry instead. +private fun <T : Parcelable, R : ComplexParameter<T>> NavController.navigateComplex( + route: R, + data: T +) { + navigate(route) + getBackStackEntry(route).savedStateHandle["args"] = data +} + +private fun <T : Parcelable> NavBackStackEntry.getComplexArg() = savedStateHandle.get<T>("args")!! \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/ManagerApplication.kt b/app/src/main/java/app/revanced/manager/ManagerApplication.kt new file mode 100644 index 0000000000..1d17e5ef61 --- /dev/null +++ b/app/src/main/java/app/revanced/manager/ManagerApplication.kt @@ -0,0 +1,110 @@ +package app.revanced.manager + +import android.app.Activity +import android.app.Application +import android.os.Bundle +import android.util.Log +import app.revanced.manager.data.platform.Filesystem +import app.revanced.manager.di.* +import app.revanced.manager.domain.manager.PreferencesManager +import app.revanced.manager.domain.repository.DownloaderPluginRepository +import app.revanced.manager.domain.repository.PatchBundleRepository +import app.revanced.manager.util.tag +import kotlinx.coroutines.Dispatchers +import coil.Coil +import coil.ImageLoader +import com.topjohnwu.superuser.Shell +import com.topjohnwu.superuser.internal.BuilderImpl +import kotlinx.coroutines.MainScope +import kotlinx.coroutines.launch +import me.zhanghai.android.appiconloader.coil.AppIconFetcher +import me.zhanghai.android.appiconloader.coil.AppIconKeyer +import org.koin.android.ext.android.inject +import org.koin.android.ext.koin.androidContext +import org.koin.android.ext.koin.androidLogger +import org.koin.androidx.workmanager.koin.workManagerFactory +import org.koin.core.context.startKoin + +class ManagerApplication : Application() { + private val scope = MainScope() + private val prefs: PreferencesManager by inject() + private val patchBundleRepository: PatchBundleRepository by inject() + private val downloaderPluginRepository: DownloaderPluginRepository by inject() + private val fs: Filesystem by inject() + + override fun onCreate() { + super.onCreate() + + startKoin { + androidContext(this@ManagerApplication) + androidLogger() + workManagerFactory() + modules( + httpModule, + preferencesModule, + repositoryModule, + serviceModule, + managerModule, + workerModule, + viewModelModule, + databaseModule, + rootModule + ) + } + + val pixels = 512 + Coil.setImageLoader( + ImageLoader.Builder(this) + .components { + add(AppIconKeyer()) + add(AppIconFetcher.Factory(pixels, true, this@ManagerApplication)) + } + .build() + ) + + val shellBuilder = BuilderImpl.create().setFlags(Shell.FLAG_MOUNT_MASTER) + Shell.setDefaultBuilder(shellBuilder) + + scope.launch { + prefs.preload() + } + scope.launch(Dispatchers.Default) { + downloaderPluginRepository.reload() + } + scope.launch(Dispatchers.Default) { + with(patchBundleRepository) { + reload() + updateCheck() + } + } + registerActivityLifecycleCallbacks(object : ActivityLifecycleCallbacks { + private var firstActivityCreated = false + + override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) { + if (firstActivityCreated) return + firstActivityCreated = true + + // We do not want to call onFreshProcessStart() if there is state to restore. + // This can happen on system-initiated process death. + if (savedInstanceState == null) { + Log.d(tag, "Fresh process created") + onFreshProcessStart() + } else Log.d(tag, "System-initiated process death detected") + } + + override fun onActivityStarted(activity: Activity) {} + override fun onActivityResumed(activity: Activity) {} + override fun onActivityPaused(activity: Activity) {} + override fun onActivityStopped(activity: Activity) {} + override fun onActivitySaveInstanceState(activity: Activity, outState: Bundle) {} + override fun onActivityDestroyed(activity: Activity) {} + }) + } + + private fun onFreshProcessStart() { + fs.uiTempDir.apply { + deleteRecursively() + mkdirs() + } + } +} \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/data/platform/Filesystem.kt b/app/src/main/java/app/revanced/manager/data/platform/Filesystem.kt new file mode 100644 index 0000000000..7bad2debc3 --- /dev/null +++ b/app/src/main/java/app/revanced/manager/data/platform/Filesystem.kt @@ -0,0 +1,51 @@ +package app.revanced.manager.data.platform + +import android.Manifest +import android.app.Application +import android.content.Context +import android.content.pm.PackageManager +import android.os.Build +import android.os.Environment +import androidx.activity.result.contract.ActivityResultContract +import androidx.activity.result.contract.ActivityResultContracts +import app.revanced.manager.util.RequestManageStorageContract +import java.io.File +import java.nio.file.Path + +class Filesystem(private val app: Application) { + val contentResolver = app.contentResolver // TODO: move Content Resolver operations to here. + + /** + * A directory that gets cleared when the app restarts. + * Do not store paths to this directory in a parcel. + */ + val tempDir: File = app.getDir("ephemeral", Context.MODE_PRIVATE).apply { + deleteRecursively() + mkdirs() + } + + /** + * A directory for storing temporary files related to UI. + * This is the same as [tempDir], but does not get cleared on system-initiated process death. + * Paths to this directory can be safely stored in parcels. + */ + val uiTempDir: File = app.getDir("ui_ephemeral", Context.MODE_PRIVATE) + + fun externalFilesDir(): Path = Environment.getExternalStorageDirectory().toPath() + + private fun usesManagePermission() = Build.VERSION.SDK_INT >= Build.VERSION_CODES.R + + private val storagePermissionName = + if (usesManagePermission()) Manifest.permission.MANAGE_EXTERNAL_STORAGE else Manifest.permission.READ_EXTERNAL_STORAGE + + fun permissionContract(): Pair<ActivityResultContract<String, Boolean>, String> { + val contract = + if (usesManagePermission()) RequestManageStorageContract() else ActivityResultContracts.RequestPermission() + return contract to storagePermissionName + } + + fun hasStoragePermission() = + if (usesManagePermission()) Environment.isExternalStorageManager() else app.checkSelfPermission( + storagePermissionName + ) == PackageManager.PERMISSION_GRANTED +} \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/data/platform/NetworkInfo.kt b/app/src/main/java/app/revanced/manager/data/platform/NetworkInfo.kt new file mode 100644 index 0000000000..f5d3dd89bb --- /dev/null +++ b/app/src/main/java/app/revanced/manager/data/platform/NetworkInfo.kt @@ -0,0 +1,19 @@ +package app.revanced.manager.data.platform + +import android.app.Application +import android.net.ConnectivityManager +import android.net.NetworkCapabilities +import androidx.core.content.getSystemService + +class NetworkInfo(app: Application) { + private val connectivityManager = app.getSystemService<ConnectivityManager>()!! + + private fun getCapabilities() = connectivityManager.activeNetwork?.let { connectivityManager.getNetworkCapabilities(it) } + fun isConnected() = connectivityManager.activeNetwork != null + fun isUnmetered() = getCapabilities()?.hasCapability(NetworkCapabilities.NET_CAPABILITY_NOT_METERED) ?: true + + /** + * Returns true if it is safe to download large files. + */ + fun isSafe() = isConnected() && isUnmetered() +} \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/data/room/AppDatabase.kt b/app/src/main/java/app/revanced/manager/data/room/AppDatabase.kt new file mode 100644 index 0000000000..403bd1cf71 --- /dev/null +++ b/app/src/main/java/app/revanced/manager/data/room/AppDatabase.kt @@ -0,0 +1,39 @@ +package app.revanced.manager.data.room + +import androidx.room.Database +import androidx.room.RoomDatabase +import androidx.room.TypeConverters +import app.revanced.manager.data.room.apps.downloaded.DownloadedAppDao +import app.revanced.manager.data.room.apps.downloaded.DownloadedApp +import app.revanced.manager.data.room.apps.installed.AppliedPatch +import app.revanced.manager.data.room.apps.installed.InstalledApp +import app.revanced.manager.data.room.apps.installed.InstalledAppDao +import app.revanced.manager.data.room.selection.PatchSelection +import app.revanced.manager.data.room.selection.SelectedPatch +import app.revanced.manager.data.room.selection.SelectionDao +import app.revanced.manager.data.room.bundles.PatchBundleDao +import app.revanced.manager.data.room.bundles.PatchBundleEntity +import app.revanced.manager.data.room.options.Option +import app.revanced.manager.data.room.options.OptionDao +import app.revanced.manager.data.room.options.OptionGroup +import app.revanced.manager.data.room.plugins.TrustedDownloaderPlugin +import app.revanced.manager.data.room.plugins.TrustedDownloaderPluginDao +import kotlin.random.Random + +@Database( + entities = [PatchBundleEntity::class, PatchSelection::class, SelectedPatch::class, DownloadedApp::class, InstalledApp::class, AppliedPatch::class, OptionGroup::class, Option::class, TrustedDownloaderPlugin::class], + version = 1 +) +@TypeConverters(Converters::class) +abstract class AppDatabase : RoomDatabase() { + abstract fun patchBundleDao(): PatchBundleDao + abstract fun selectionDao(): SelectionDao + abstract fun downloadedAppDao(): DownloadedAppDao + abstract fun installedAppDao(): InstalledAppDao + abstract fun optionDao(): OptionDao + abstract fun trustedDownloaderPluginDao(): TrustedDownloaderPluginDao + + companion object { + fun generateUid() = Random.Default.nextInt() + } +} \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/data/room/Converters.kt b/app/src/main/java/app/revanced/manager/data/room/Converters.kt new file mode 100644 index 0000000000..a9437f86e2 --- /dev/null +++ b/app/src/main/java/app/revanced/manager/data/room/Converters.kt @@ -0,0 +1,26 @@ +package app.revanced.manager.data.room + +import androidx.room.TypeConverter +import app.revanced.manager.data.room.bundles.Source +import app.revanced.manager.data.room.options.Option.SerializedValue +import java.io.File + +class Converters { + @TypeConverter + fun sourceFromString(value: String) = Source.from(value) + + @TypeConverter + fun sourceToString(value: Source) = value.toString() + + @TypeConverter + fun fileFromString(value: String) = File(value) + + @TypeConverter + fun fileToString(file: File): String = file.path + + @TypeConverter + fun serializedOptionFromString(value: String) = SerializedValue.fromJsonString(value) + + @TypeConverter + fun serializedOptionToString(value: SerializedValue) = value.toJsonString() +} \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/data/room/apps/downloaded/DownloadedApp.kt b/app/src/main/java/app/revanced/manager/data/room/apps/downloaded/DownloadedApp.kt new file mode 100644 index 0000000000..f170331448 --- /dev/null +++ b/app/src/main/java/app/revanced/manager/data/room/apps/downloaded/DownloadedApp.kt @@ -0,0 +1,16 @@ +package app.revanced.manager.data.room.apps.downloaded + +import androidx.room.ColumnInfo +import androidx.room.Entity +import java.io.File + +@Entity( + tableName = "downloaded_app", + primaryKeys = ["package_name", "version"] +) +data class DownloadedApp( + @ColumnInfo(name = "package_name") val packageName: String, + @ColumnInfo(name = "version") val version: String, + @ColumnInfo(name = "directory") val directory: File, + @ColumnInfo(name = "last_used") val lastUsed: Long = System.currentTimeMillis() +) \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/data/room/apps/downloaded/DownloadedAppDao.kt b/app/src/main/java/app/revanced/manager/data/room/apps/downloaded/DownloadedAppDao.kt new file mode 100644 index 0000000000..492dbde16c --- /dev/null +++ b/app/src/main/java/app/revanced/manager/data/room/apps/downloaded/DownloadedAppDao.kt @@ -0,0 +1,26 @@ +package app.revanced.manager.data.room.apps.downloaded + +import androidx.room.Dao +import androidx.room.Delete +import androidx.room.Insert +import androidx.room.Query +import androidx.room.Upsert +import kotlinx.coroutines.flow.Flow + +@Dao +interface DownloadedAppDao { + @Query("SELECT * FROM downloaded_app") + fun getAllApps(): Flow<List<DownloadedApp>> + + @Query("SELECT * FROM downloaded_app WHERE package_name = :packageName AND version = :version") + suspend fun get(packageName: String, version: String): DownloadedApp? + + @Upsert + suspend fun upsert(downloadedApp: DownloadedApp) + + @Query("UPDATE downloaded_app SET last_used = :newValue WHERE package_name = :packageName AND version = :version") + suspend fun markUsed(packageName: String, version: String, newValue: Long = System.currentTimeMillis()) + + @Delete + suspend fun delete(downloadedApps: Collection<DownloadedApp>) +} \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/data/room/apps/installed/AppliedPatch.kt b/app/src/main/java/app/revanced/manager/data/room/apps/installed/AppliedPatch.kt new file mode 100644 index 0000000000..d2a498a3a0 --- /dev/null +++ b/app/src/main/java/app/revanced/manager/data/room/apps/installed/AppliedPatch.kt @@ -0,0 +1,35 @@ +package app.revanced.manager.data.room.apps.installed + +import android.os.Parcelable +import androidx.room.ColumnInfo +import androidx.room.Entity +import androidx.room.ForeignKey +import androidx.room.Index +import app.revanced.manager.data.room.bundles.PatchBundleEntity +import kotlinx.parcelize.Parcelize + +@Parcelize +@Entity( + tableName = "applied_patch", + primaryKeys = ["package_name", "bundle", "patch_name"], + foreignKeys = [ + ForeignKey( + InstalledApp::class, + parentColumns = ["current_package_name"], + childColumns = ["package_name"], + onDelete = ForeignKey.CASCADE + ), + ForeignKey( + PatchBundleEntity::class, + parentColumns = ["uid"], + childColumns = ["bundle"], + onDelete = ForeignKey.CASCADE + ) + ], + indices = [Index(value = ["bundle"], unique = false)] +) +data class AppliedPatch( + @ColumnInfo(name = "package_name") val packageName: String, + @ColumnInfo(name = "bundle") val bundle: Int, + @ColumnInfo(name = "patch_name") val patchName: String +) : Parcelable \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/data/room/apps/installed/InstalledApp.kt b/app/src/main/java/app/revanced/manager/data/room/apps/installed/InstalledApp.kt new file mode 100644 index 0000000000..c0986dfd10 --- /dev/null +++ b/app/src/main/java/app/revanced/manager/data/room/apps/installed/InstalledApp.kt @@ -0,0 +1,20 @@ +package app.revanced.manager.data.room.apps.installed + +import androidx.room.ColumnInfo +import androidx.room.Entity +import androidx.room.PrimaryKey +import app.revanced.manager.R + +enum class InstallType(val stringResource: Int) { + DEFAULT(R.string.default_install), + MOUNT(R.string.mount_install) +} + +@Entity(tableName = "installed_app") +data class InstalledApp( + @PrimaryKey + @ColumnInfo(name = "current_package_name") val currentPackageName: String, + @ColumnInfo(name = "original_package_name") val originalPackageName: String, + @ColumnInfo(name = "version") val version: String, + @ColumnInfo(name = "install_type") val installType: InstallType +) \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/data/room/apps/installed/InstalledAppDao.kt b/app/src/main/java/app/revanced/manager/data/room/apps/installed/InstalledAppDao.kt new file mode 100644 index 0000000000..c290cc5e9e --- /dev/null +++ b/app/src/main/java/app/revanced/manager/data/room/apps/installed/InstalledAppDao.kt @@ -0,0 +1,46 @@ +package app.revanced.manager.data.room.apps.installed + +import androidx.room.Dao +import androidx.room.Delete +import androidx.room.Insert +import androidx.room.MapColumn +import androidx.room.Query +import androidx.room.Transaction +import androidx.room.Upsert +import kotlinx.coroutines.flow.Flow + +@Dao +interface InstalledAppDao { + @Query("SELECT * FROM installed_app") + fun getAll(): Flow<List<InstalledApp>> + + @Query("SELECT * FROM installed_app WHERE current_package_name = :packageName") + suspend fun get(packageName: String): InstalledApp? + + @Query( + "SELECT bundle, patch_name FROM applied_patch" + + " WHERE package_name = :packageName" + ) + suspend fun getPatchesSelection(packageName: String): Map<@MapColumn("bundle") Int, List<@MapColumn( + "patch_name" + ) String>> + + @Transaction + suspend fun upsertApp(installedApp: InstalledApp, appliedPatches: List<AppliedPatch>) { + upsertApp(installedApp) + deleteAppliedPatches(installedApp.currentPackageName) + insertAppliedPatches(appliedPatches) + } + + @Upsert + suspend fun upsertApp(installedApp: InstalledApp) + + @Insert + suspend fun insertAppliedPatches(appliedPatches: List<AppliedPatch>) + + @Query("DELETE FROM applied_patch WHERE package_name = :packageName") + suspend fun deleteAppliedPatches(packageName: String) + + @Delete + suspend fun delete(installedApp: InstalledApp) +} \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/data/room/bundles/PatchBundleDao.kt b/app/src/main/java/app/revanced/manager/data/room/bundles/PatchBundleDao.kt new file mode 100644 index 0000000000..d9955a702b --- /dev/null +++ b/app/src/main/java/app/revanced/manager/data/room/bundles/PatchBundleDao.kt @@ -0,0 +1,37 @@ +package app.revanced.manager.data.room.bundles + +import androidx.room.* +import kotlinx.coroutines.flow.Flow + +@Dao +interface PatchBundleDao { + @Query("SELECT * FROM patch_bundles") + suspend fun all(): List<PatchBundleEntity> + + @Query("SELECT version, auto_update FROM patch_bundles WHERE uid = :uid") + fun getPropsById(uid: Int): Flow<BundleProperties?> + + @Query("UPDATE patch_bundles SET version = :patches WHERE uid = :uid") + suspend fun updateVersion(uid: Int, patches: String?) + + @Query("UPDATE patch_bundles SET auto_update = :value WHERE uid = :uid") + suspend fun setAutoUpdate(uid: Int, value: Boolean) + + @Query("UPDATE patch_bundles SET name = :value WHERE uid = :uid") + suspend fun setName(uid: Int, value: String) + + @Query("DELETE FROM patch_bundles WHERE uid != 0") + suspend fun purgeCustomBundles() + + @Transaction + suspend fun reset() { + purgeCustomBundles() + updateVersion(0, null) // Reset the main source + } + + @Query("DELETE FROM patch_bundles WHERE uid = :uid") + suspend fun remove(uid: Int) + + @Insert + suspend fun add(source: PatchBundleEntity) +} \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/data/room/bundles/PatchBundleEntity.kt b/app/src/main/java/app/revanced/manager/data/room/bundles/PatchBundleEntity.kt new file mode 100644 index 0000000000..8ba5f64a96 --- /dev/null +++ b/app/src/main/java/app/revanced/manager/data/room/bundles/PatchBundleEntity.kt @@ -0,0 +1,44 @@ +package app.revanced.manager.data.room.bundles + +import androidx.room.* +import io.ktor.http.* + +sealed class Source { + object Local : Source() { + const val SENTINEL = "local" + + override fun toString() = SENTINEL + } + + object API : Source() { + const val SENTINEL = "api" + + override fun toString() = SENTINEL + } + + data class Remote(val url: Url) : Source() { + override fun toString() = url.toString() + } + + companion object { + fun from(value: String) = when (value) { + Local.SENTINEL -> Local + API.SENTINEL -> API + else -> Remote(Url(value)) + } + } +} + +@Entity(tableName = "patch_bundles") +data class PatchBundleEntity( + @PrimaryKey val uid: Int, + @ColumnInfo(name = "name") val name: String, + @ColumnInfo(name = "version") val version: String? = null, + @ColumnInfo(name = "source") val source: Source, + @ColumnInfo(name = "auto_update") val autoUpdate: Boolean +) + +data class BundleProperties( + @ColumnInfo(name = "version") val version: String? = null, + @ColumnInfo(name = "auto_update") val autoUpdate: Boolean +) \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/data/room/options/Option.kt b/app/src/main/java/app/revanced/manager/data/room/options/Option.kt new file mode 100644 index 0000000000..44bc3d40ab --- /dev/null +++ b/app/src/main/java/app/revanced/manager/data/room/options/Option.kt @@ -0,0 +1,116 @@ +package app.revanced.manager.data.room.options + +import androidx.room.ColumnInfo +import androidx.room.Entity +import androidx.room.ForeignKey +import app.revanced.manager.patcher.patch.Option +import kotlinx.serialization.Serializable +import kotlinx.serialization.SerializationException +import kotlinx.serialization.encodeToString +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.JsonElement +import kotlinx.serialization.json.JsonNull +import kotlinx.serialization.json.JsonPrimitive +import kotlinx.serialization.json.add +import kotlinx.serialization.json.boolean +import kotlinx.serialization.json.buildJsonArray +import kotlinx.serialization.json.float +import kotlinx.serialization.json.int +import kotlinx.serialization.json.jsonArray +import kotlinx.serialization.json.jsonPrimitive +import kotlinx.serialization.json.long +import kotlin.reflect.KClass +import kotlin.reflect.KType +import kotlin.reflect.typeOf + +@Entity( + tableName = "options", + primaryKeys = ["group", "patch_name", "key"], + foreignKeys = [ForeignKey( + OptionGroup::class, + parentColumns = ["uid"], + childColumns = ["group"], + onDelete = ForeignKey.CASCADE + )] +) +data class Option( + @ColumnInfo(name = "group") val group: Int, + @ColumnInfo(name = "patch_name") val patchName: String, + @ColumnInfo(name = "key") val key: String, + // Encoded as Json. + @ColumnInfo(name = "value") val value: SerializedValue, +) { + @Serializable + data class SerializedValue(val raw: JsonElement) { + fun toJsonString() = json.encodeToString(raw) + fun deserializeFor(option: Option<*>): Any? { + if (raw is JsonNull) return null + + val errorMessage = "Cannot deserialize value as ${option.type}" + try { + if (option.type.classifier == List::class) { + val elementType = option.type.arguments.first().type!! + return raw.jsonArray.map { deserializeBasicType(elementType, it.jsonPrimitive) } + } + + return deserializeBasicType(option.type, raw.jsonPrimitive) + } catch (e: IllegalArgumentException) { + throw SerializationException(errorMessage, e) + } catch (e: IllegalStateException) { + throw SerializationException(errorMessage, e) + } catch (e: kotlinx.serialization.SerializationException) { + throw SerializationException(errorMessage, e) + } + } + + companion object { + private val json = Json { + // Patcher does not forbid the use of these values, so we should support them. + allowSpecialFloatingPointValues = true + } + + private fun deserializeBasicType(type: KType, value: JsonPrimitive) = when (type) { + typeOf<Boolean>() -> value.boolean + typeOf<Int>() -> value.int + typeOf<Long>() -> value.long + typeOf<Float>() -> value.float + typeOf<String>() -> value.content.also { + if (!value.isString) throw SerializationException( + "Expected value to be a string: $value" + ) + } + + else -> throw SerializationException("Unknown type: $type") + } + + fun fromJsonString(value: String) = SerializedValue(json.decodeFromString(value)) + fun fromValue(value: Any?) = SerializedValue(when (value) { + null -> JsonNull + is Number -> JsonPrimitive(value) + is Boolean -> JsonPrimitive(value) + is String -> JsonPrimitive(value) + is List<*> -> buildJsonArray { + var elementClass: KClass<out Any>? = null + + value.forEach { + when (it) { + null -> throw SerializationException("List elements must not be null") + is Number -> add(it) + is Boolean -> add(it) + is String -> add(it) + else -> throw SerializationException("Unknown element type: ${it::class.simpleName}") + } + + if (elementClass == null) elementClass = it::class + else if (elementClass != it::class) throw SerializationException("List elements must have the same type") + } + } + + else -> throw SerializationException("Unknown type: ${value::class.simpleName}") + }) + } + } + + class SerializationException(message: String, cause: Throwable? = null) : + Exception(message, cause) +} diff --git a/app/src/main/java/app/revanced/manager/data/room/options/OptionDao.kt b/app/src/main/java/app/revanced/manager/data/room/options/OptionDao.kt new file mode 100644 index 0000000000..5a147f6f3b --- /dev/null +++ b/app/src/main/java/app/revanced/manager/data/room/options/OptionDao.kt @@ -0,0 +1,50 @@ +package app.revanced.manager.data.room.options + +import androidx.room.Dao +import androidx.room.Insert +import androidx.room.MapColumn +import androidx.room.Query +import androidx.room.Transaction +import kotlinx.coroutines.flow.Flow + +@Dao +abstract class OptionDao { + @Transaction + @Query( + "SELECT patch_bundle, `group`, patch_name, `key`, value FROM option_groups" + + " LEFT JOIN options ON uid = options.`group`" + + " WHERE package_name = :packageName" + ) + abstract suspend fun getOptions(packageName: String): Map<@MapColumn("patch_bundle") Int, List<Option>> + + @Query("SELECT uid FROM option_groups WHERE patch_bundle = :bundleUid AND package_name = :packageName") + abstract suspend fun getGroupId(bundleUid: Int, packageName: String): Int? + + @Query("SELECT package_name FROM option_groups") + abstract fun getPackagesWithOptions(): Flow<List<String>> + + @Insert + abstract suspend fun createOptionGroup(group: OptionGroup) + + @Query("DELETE FROM option_groups WHERE patch_bundle = :uid") + abstract suspend fun clearForPatchBundle(uid: Int) + + @Query("DELETE FROM option_groups WHERE package_name = :packageName") + abstract suspend fun clearForPackage(packageName: String) + + @Query("DELETE FROM option_groups") + abstract suspend fun reset() + + @Insert + protected abstract suspend fun insertOptions(patches: List<Option>) + + @Query("DELETE FROM options WHERE `group` = :groupId") + protected abstract suspend fun clearGroup(groupId: Int) + + @Transaction + open suspend fun updateOptions(options: Map<Int, List<Option>>) = + options.forEach { (groupId, options) -> + clearGroup(groupId) + insertOptions(options) + } +} \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/data/room/options/OptionGroup.kt b/app/src/main/java/app/revanced/manager/data/room/options/OptionGroup.kt new file mode 100644 index 0000000000..df35dc993e --- /dev/null +++ b/app/src/main/java/app/revanced/manager/data/room/options/OptionGroup.kt @@ -0,0 +1,24 @@ +package app.revanced.manager.data.room.options + +import androidx.room.ColumnInfo +import androidx.room.Entity +import androidx.room.ForeignKey +import androidx.room.Index +import androidx.room.PrimaryKey +import app.revanced.manager.data.room.bundles.PatchBundleEntity + +@Entity( + tableName = "option_groups", + foreignKeys = [ForeignKey( + PatchBundleEntity::class, + parentColumns = ["uid"], + childColumns = ["patch_bundle"], + onDelete = ForeignKey.CASCADE + )], + indices = [Index(value = ["patch_bundle", "package_name"], unique = true)] +) +data class OptionGroup( + @PrimaryKey val uid: Int, + @ColumnInfo(name = "patch_bundle") val patchBundle: Int, + @ColumnInfo(name = "package_name") val packageName: String +) \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/data/room/plugins/TrustedDownloaderPlugin.kt b/app/src/main/java/app/revanced/manager/data/room/plugins/TrustedDownloaderPlugin.kt new file mode 100644 index 0000000000..8e1b9c39bd --- /dev/null +++ b/app/src/main/java/app/revanced/manager/data/room/plugins/TrustedDownloaderPlugin.kt @@ -0,0 +1,11 @@ +package app.revanced.manager.data.room.plugins + +import androidx.room.ColumnInfo +import androidx.room.Entity +import androidx.room.PrimaryKey + +@Entity(tableName = "trusted_downloader_plugins") +class TrustedDownloaderPlugin( + @PrimaryKey @ColumnInfo(name = "package_name") val packageName: String, + @ColumnInfo(name = "signature") val signature: ByteArray +) \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/data/room/plugins/TrustedDownloaderPluginDao.kt b/app/src/main/java/app/revanced/manager/data/room/plugins/TrustedDownloaderPluginDao.kt new file mode 100644 index 0000000000..ad1845f73d --- /dev/null +++ b/app/src/main/java/app/revanced/manager/data/room/plugins/TrustedDownloaderPluginDao.kt @@ -0,0 +1,22 @@ +package app.revanced.manager.data.room.plugins + +import androidx.room.Dao +import androidx.room.Query +import androidx.room.Transaction +import androidx.room.Upsert + +@Dao +interface TrustedDownloaderPluginDao { + @Query("SELECT signature FROM trusted_downloader_plugins WHERE package_name = :packageName") + suspend fun getTrustedSignature(packageName: String): ByteArray? + + @Upsert + suspend fun upsertTrust(plugin: TrustedDownloaderPlugin) + + @Query("DELETE FROM trusted_downloader_plugins WHERE package_name = :packageName") + suspend fun remove(packageName: String) + + @Transaction + @Query("DELETE FROM trusted_downloader_plugins WHERE package_name IN (:packageNames)") + suspend fun removeAll(packageNames: Set<String>) +} \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/data/room/selection/PatchSelection.kt b/app/src/main/java/app/revanced/manager/data/room/selection/PatchSelection.kt new file mode 100644 index 0000000000..02f5ab94d3 --- /dev/null +++ b/app/src/main/java/app/revanced/manager/data/room/selection/PatchSelection.kt @@ -0,0 +1,24 @@ +package app.revanced.manager.data.room.selection + +import androidx.room.ColumnInfo +import androidx.room.Entity +import androidx.room.ForeignKey +import androidx.room.Index +import androidx.room.PrimaryKey +import app.revanced.manager.data.room.bundles.PatchBundleEntity + +@Entity( + tableName = "patch_selections", + foreignKeys = [ForeignKey( + PatchBundleEntity::class, + parentColumns = ["uid"], + childColumns = ["patch_bundle"], + onDelete = ForeignKey.CASCADE + )], + indices = [Index(value = ["patch_bundle", "package_name"], unique = true)] +) +data class PatchSelection( + @PrimaryKey val uid: Int, + @ColumnInfo(name = "patch_bundle") val patchBundle: Int, + @ColumnInfo(name = "package_name") val packageName: String +) \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/data/room/selection/SelectedPatch.kt b/app/src/main/java/app/revanced/manager/data/room/selection/SelectedPatch.kt new file mode 100644 index 0000000000..c190364cf5 --- /dev/null +++ b/app/src/main/java/app/revanced/manager/data/room/selection/SelectedPatch.kt @@ -0,0 +1,20 @@ +package app.revanced.manager.data.room.selection + +import androidx.room.ColumnInfo +import androidx.room.Entity +import androidx.room.ForeignKey + +@Entity( + tableName = "selected_patches", + primaryKeys = ["selection", "patch_name"], + foreignKeys = [ForeignKey( + PatchSelection::class, + parentColumns = ["uid"], + childColumns = ["selection"], + onDelete = ForeignKey.CASCADE + )] +) +data class SelectedPatch( + @ColumnInfo(name = "selection") val selection: Int, + @ColumnInfo(name = "patch_name") val patchName: String +) \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/data/room/selection/SelectionDao.kt b/app/src/main/java/app/revanced/manager/data/room/selection/SelectionDao.kt new file mode 100644 index 0000000000..14ad1d871f --- /dev/null +++ b/app/src/main/java/app/revanced/manager/data/room/selection/SelectionDao.kt @@ -0,0 +1,58 @@ +package app.revanced.manager.data.room.selection + +import androidx.room.Dao +import androidx.room.Insert +import androidx.room.MapColumn +import androidx.room.Query +import androidx.room.Transaction + +@Dao +abstract class SelectionDao { + @Transaction + @Query( + "SELECT patch_bundle, patch_name FROM patch_selections" + + " LEFT JOIN selected_patches ON uid = selected_patches.selection" + + " WHERE package_name = :packageName" + ) + abstract suspend fun getSelectedPatches(packageName: String): Map<@MapColumn("patch_bundle") Int, List<@MapColumn( + "patch_name" + ) String>> + + @Transaction + @Query( + "SELECT package_name, patch_name FROM patch_selections" + + " LEFT JOIN selected_patches ON uid = selected_patches.selection" + + " WHERE patch_bundle = :bundleUid" + ) + abstract suspend fun exportSelection(bundleUid: Int): Map<@MapColumn("package_name") String, List<@MapColumn( + "patch_name" + ) String>> + + @Query("SELECT uid FROM patch_selections WHERE patch_bundle = :bundleUid AND package_name = :packageName") + abstract suspend fun getSelectionId(bundleUid: Int, packageName: String): Int? + + @Insert + abstract suspend fun createSelection(selection: PatchSelection) + + @Query("DELETE FROM patch_selections WHERE patch_bundle = :uid") + abstract suspend fun clearForPatchBundle(uid: Int) + + @Query("DELETE FROM patch_selections WHERE package_name = :packageName") + abstract suspend fun clearForPackage(packageName: String) + + @Query("DELETE FROM patch_selections") + abstract suspend fun reset() + + @Insert + protected abstract suspend fun selectPatches(patches: List<SelectedPatch>) + + @Query("DELETE FROM selected_patches WHERE selection = :selectionId") + protected abstract suspend fun clearSelection(selectionId: Int) + + @Transaction + open suspend fun updateSelections(selections: Map<Int, Set<String>>) = + selections.forEach { (selectionUid, patches) -> + clearSelection(selectionUid) + selectPatches(patches.map { SelectedPatch(selectionUid, it) }) + } +} \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/di/DatabaseModule.kt b/app/src/main/java/app/revanced/manager/di/DatabaseModule.kt new file mode 100644 index 0000000000..37d8c05dd6 --- /dev/null +++ b/app/src/main/java/app/revanced/manager/di/DatabaseModule.kt @@ -0,0 +1,15 @@ +package app.revanced.manager.di + +import android.content.Context +import androidx.room.Room +import app.revanced.manager.data.room.AppDatabase +import org.koin.android.ext.koin.androidContext +import org.koin.dsl.module + +val databaseModule = module { + fun provideAppDatabase(context: Context) = Room.databaseBuilder(context, AppDatabase::class.java, "manager").build() + + single { + provideAppDatabase(androidContext()) + } +} \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/di/HttpModule.kt b/app/src/main/java/app/revanced/manager/di/HttpModule.kt new file mode 100644 index 0000000000..1d827ce633 --- /dev/null +++ b/app/src/main/java/app/revanced/manager/di/HttpModule.kt @@ -0,0 +1,60 @@ +package app.revanced.manager.di + +import android.content.Context +import app.revanced.manager.BuildConfig +import io.ktor.client.* +import io.ktor.client.engine.okhttp.* +import io.ktor.client.plugins.HttpTimeout +import io.ktor.client.plugins.UserAgent +import io.ktor.client.plugins.contentnegotiation.* +import io.ktor.serialization.kotlinx.json.* +import kotlinx.serialization.json.Json +import okhttp3.Cache +import okhttp3.Dns +import org.koin.android.ext.koin.androidContext +import org.koin.core.module.dsl.singleOf +import org.koin.dsl.module +import java.net.Inet4Address +import java.net.InetAddress + +val httpModule = module { + fun provideHttpClient(context: Context, json: Json) = HttpClient(OkHttp) { + engine { + config { + dns(object : Dns { + override fun lookup(hostname: String): List<InetAddress> { + val addresses = Dns.SYSTEM.lookup(hostname) + return if (hostname == "raw.githubusercontent.com") { + addresses.filterIsInstance<Inet4Address>() + } else { + addresses + } + } + }) + cache(Cache(context.cacheDir.resolve("cache").also { it.mkdirs() }, 1024 * 1024 * 100)) + followRedirects(true) + followSslRedirects(true) + } + } + install(ContentNegotiation) { + json(json) + } + install(HttpTimeout) { + socketTimeoutMillis = 10000 + } + install(UserAgent) { + agent = "ReVanced-Manager/${BuildConfig.VERSION_CODE}" + } + } + + fun provideJson() = Json { + encodeDefaults = true + isLenient = true + ignoreUnknownKeys = true + } + + single { + provideHttpClient(androidContext(), get()) + } + singleOf(::provideJson) +} \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/di/ManagerModule.kt b/app/src/main/java/app/revanced/manager/di/ManagerModule.kt new file mode 100644 index 0000000000..0aae1cd67f --- /dev/null +++ b/app/src/main/java/app/revanced/manager/di/ManagerModule.kt @@ -0,0 +1,11 @@ +package app.revanced.manager.di + +import app.revanced.manager.domain.manager.KeystoreManager +import app.revanced.manager.util.PM +import org.koin.core.module.dsl.singleOf +import org.koin.dsl.module + +val managerModule = module { + singleOf(::KeystoreManager) + singleOf(::PM) +} \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/di/PreferencesModule.kt b/app/src/main/java/app/revanced/manager/di/PreferencesModule.kt new file mode 100644 index 0000000000..029ef4ed0a --- /dev/null +++ b/app/src/main/java/app/revanced/manager/di/PreferencesModule.kt @@ -0,0 +1,9 @@ +package app.revanced.manager.di + +import app.revanced.manager.domain.manager.PreferencesManager +import org.koin.core.module.dsl.singleOf +import org.koin.dsl.module + +val preferencesModule = module { + singleOf(::PreferencesManager) +} \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/di/RepositoryModule.kt b/app/src/main/java/app/revanced/manager/di/RepositoryModule.kt new file mode 100644 index 0000000000..159436d407 --- /dev/null +++ b/app/src/main/java/app/revanced/manager/di/RepositoryModule.kt @@ -0,0 +1,29 @@ +package app.revanced.manager.di + +import app.revanced.manager.data.platform.Filesystem +import app.revanced.manager.data.platform.NetworkInfo +import app.revanced.manager.domain.repository.* +import app.revanced.manager.domain.worker.WorkerRepository +import app.revanced.manager.network.api.ReVancedAPI +import org.koin.core.module.dsl.createdAtStart +import org.koin.core.module.dsl.singleOf +import org.koin.dsl.module + +val repositoryModule = module { + singleOf(::ReVancedAPI) + singleOf(::Filesystem) { + createdAtStart() + } + singleOf(::NetworkInfo) + singleOf(::PatchBundlePersistenceRepository) + singleOf(::PatchSelectionRepository) + singleOf(::PatchOptionsRepository) + singleOf(::PatchBundleRepository) { + // It is best to load patch bundles ASAP + createdAtStart() + } + singleOf(::DownloaderPluginRepository) + singleOf(::WorkerRepository) + singleOf(::DownloadedAppRepository) + singleOf(::InstalledAppRepository) +} \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/di/RootModule.kt b/app/src/main/java/app/revanced/manager/di/RootModule.kt new file mode 100644 index 0000000000..1e27555b0c --- /dev/null +++ b/app/src/main/java/app/revanced/manager/di/RootModule.kt @@ -0,0 +1,9 @@ +package app.revanced.manager.di + +import app.revanced.manager.domain.installer.RootInstaller +import org.koin.core.module.dsl.singleOf +import org.koin.dsl.module + +val rootModule = module { + singleOf(::RootInstaller) +} \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/di/ServiceModule.kt b/app/src/main/java/app/revanced/manager/di/ServiceModule.kt new file mode 100644 index 0000000000..cfda5030db --- /dev/null +++ b/app/src/main/java/app/revanced/manager/di/ServiceModule.kt @@ -0,0 +1,9 @@ +package app.revanced.manager.di + +import app.revanced.manager.network.service.HttpService +import org.koin.core.module.dsl.singleOf +import org.koin.dsl.module + +val serviceModule = module { + singleOf(::HttpService) +} \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/di/ViewModelModule.kt b/app/src/main/java/app/revanced/manager/di/ViewModelModule.kt new file mode 100644 index 0000000000..4846510f7d --- /dev/null +++ b/app/src/main/java/app/revanced/manager/di/ViewModelModule.kt @@ -0,0 +1,26 @@ +package app.revanced.manager.di + +import app.revanced.manager.ui.viewmodel.* +import org.koin.androidx.viewmodel.dsl.viewModelOf +import org.koin.dsl.module + +val viewModelModule = module { + viewModelOf(::MainViewModel) + viewModelOf(::DashboardViewModel) + viewModelOf(::SelectedAppInfoViewModel) + viewModelOf(::PatchesSelectorViewModel) + viewModelOf(::GeneralSettingsViewModel) + viewModelOf(::AdvancedSettingsViewModel) + viewModelOf(::AppSelectorViewModel) + viewModelOf(::PatcherViewModel) + viewModelOf(::UpdateViewModel) + viewModelOf(::ChangelogsViewModel) + viewModelOf(::ImportExportViewModel) + viewModelOf(::AboutViewModel) + viewModelOf(::DeveloperOptionsViewModel) + viewModelOf(::ContributorViewModel) + viewModelOf(::DownloadsViewModel) + viewModelOf(::InstalledAppsViewModel) + viewModelOf(::InstalledAppInfoViewModel) + viewModelOf(::UpdatesSettingsViewModel) +} diff --git a/app/src/main/java/app/revanced/manager/di/WorkerModule.kt b/app/src/main/java/app/revanced/manager/di/WorkerModule.kt new file mode 100644 index 0000000000..d5d9112e9b --- /dev/null +++ b/app/src/main/java/app/revanced/manager/di/WorkerModule.kt @@ -0,0 +1,9 @@ +package app.revanced.manager.di + +import app.revanced.manager.patcher.worker.PatcherWorker +import org.koin.androidx.workmanager.dsl.workerOf +import org.koin.dsl.module + +val workerModule = module { + workerOf(::PatcherWorker) +} \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/domain/bundles/LocalPatchBundle.kt b/app/src/main/java/app/revanced/manager/domain/bundles/LocalPatchBundle.kt new file mode 100644 index 0000000000..bcbc59cf83 --- /dev/null +++ b/app/src/main/java/app/revanced/manager/domain/bundles/LocalPatchBundle.kt @@ -0,0 +1,21 @@ +package app.revanced.manager.domain.bundles + +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import java.io.File +import java.io.InputStream + +class LocalPatchBundle(name: String, id: Int, directory: File) : + PatchBundleSource(name, id, directory) { + suspend fun replace(patches: InputStream) { + withContext(Dispatchers.IO) { + patchBundleOutputStream().use { outputStream -> + patches.copyTo(outputStream) + } + } + + reload()?.also { + saveVersion(it.readManifestAttribute("Version")) + } + } +} diff --git a/app/src/main/java/app/revanced/manager/domain/bundles/PatchBundleSource.kt b/app/src/main/java/app/revanced/manager/domain/bundles/PatchBundleSource.kt new file mode 100644 index 0000000000..308e2a56dd --- /dev/null +++ b/app/src/main/java/app/revanced/manager/domain/bundles/PatchBundleSource.kt @@ -0,0 +1,114 @@ +package app.revanced.manager.domain.bundles + +import android.app.Application +import android.util.Log +import androidx.compose.runtime.Composable +import androidx.compose.runtime.Stable +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import app.revanced.manager.R +import app.revanced.manager.domain.repository.PatchBundlePersistenceRepository +import app.revanced.manager.patcher.patch.PatchBundle +import app.revanced.manager.util.tag +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.map +import org.koin.core.component.KoinComponent +import org.koin.core.component.inject +import java.io.File +import java.io.OutputStream + +/** + * A [PatchBundle] source. + */ +@Stable +sealed class PatchBundleSource(initialName: String, val uid: Int, directory: File) : KoinComponent { + protected val configRepository: PatchBundlePersistenceRepository by inject() + private val app: Application by inject() + protected val patchesFile = directory.resolve("patches.jar") + + private val _state = MutableStateFlow(load()) + val state = _state.asStateFlow() + + private val _nameFlow = MutableStateFlow(initialName) + val nameFlow = + _nameFlow.map { it.ifEmpty { app.getString(if (isDefault) R.string.bundle_name_default else R.string.bundle_name_fallback) } } + + suspend fun getName() = nameFlow.first() + + /** + * Returns true if the bundle has been downloaded to local storage. + */ + fun hasInstalled() = patchesFile.exists() + + protected fun patchBundleOutputStream(): OutputStream = with(patchesFile) { + // Android 14+ requires dex containers to be readonly. + try { + setWritable(true, true) + outputStream() + } finally { + setReadOnly() + } + } + + private fun load(): State { + if (!hasInstalled()) return State.Missing + + return try { + State.Loaded(PatchBundle(patchesFile)) + } catch (t: Throwable) { + Log.e(tag, "Failed to load patch bundle with UID $uid", t) + State.Failed(t) + } + } + + suspend fun reload(): PatchBundle? { + val newState = load() + _state.value = newState + + val bundle = newState.patchBundleOrNull() + // Try to read the name from the patch bundle manifest if the bundle does not have a name. + if (bundle != null && _nameFlow.value.isEmpty()) { + bundle.readManifestAttribute("Name")?.let { setName(it) } + } + + return bundle + } + + /** + * Create a flow that emits the [app.revanced.manager.data.room.bundles.BundleProperties] of this [PatchBundleSource]. + * The flow will emit null if the associated [PatchBundleSource] is deleted. + */ + fun propsFlow() = configRepository.getProps(uid).flowOn(Dispatchers.Default) + suspend fun getProps() = propsFlow().first()!! + + suspend fun currentVersion() = getProps().version + protected suspend fun saveVersion(version: String?) = + configRepository.updateVersion(uid, version) + + suspend fun setName(name: String) { + configRepository.setName(uid, name) + _nameFlow.value = name + } + + sealed interface State { + fun patchBundleOrNull(): PatchBundle? = null + + data object Missing : State + data class Failed(val throwable: Throwable) : State + data class Loaded(val bundle: PatchBundle) : State { + override fun patchBundleOrNull() = bundle + } + } + + companion object Extensions { + val PatchBundleSource.isDefault inline get() = uid == 0 + val PatchBundleSource.asRemoteOrNull inline get() = this as? RemotePatchBundle + val PatchBundleSource.nameState + @Composable inline get() = nameFlow.collectAsStateWithLifecycle( + "" + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/domain/bundles/RemotePatchBundle.kt b/app/src/main/java/app/revanced/manager/domain/bundles/RemotePatchBundle.kt new file mode 100644 index 0000000000..9deb7bbe22 --- /dev/null +++ b/app/src/main/java/app/revanced/manager/domain/bundles/RemotePatchBundle.kt @@ -0,0 +1,71 @@ +package app.revanced.manager.domain.bundles + +import androidx.compose.runtime.Stable +import app.revanced.manager.network.api.ReVancedAPI +import app.revanced.manager.network.dto.ReVancedAsset +import app.revanced.manager.network.service.HttpService +import app.revanced.manager.network.utils.getOrThrow +import io.ktor.client.request.url +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import org.koin.core.component.inject +import java.io.File + +@Stable +sealed class RemotePatchBundle(name: String, id: Int, directory: File, val endpoint: String) : + PatchBundleSource(name, id, directory) { + protected val http: HttpService by inject() + + protected abstract suspend fun getLatestInfo(): ReVancedAsset + + private suspend fun download(info: ReVancedAsset) = withContext(Dispatchers.IO) { + patchBundleOutputStream().use { + http.streamTo(it) { + url(info.downloadUrl) + } + } + + saveVersion(info.version) + reload() + } + + suspend fun downloadLatest() { + download(getLatestInfo()) + } + + suspend fun update(): Boolean = withContext(Dispatchers.IO) { + val info = getLatestInfo() + if (hasInstalled() && info.version == currentVersion()) + return@withContext false + + download(info) + true + } + + suspend fun deleteLocalFiles() = withContext(Dispatchers.Default) { + patchesFile.delete() + reload() + } + + suspend fun setAutoUpdate(value: Boolean) = configRepository.setAutoUpdate(uid, value) + + companion object { + const val updateFailMsg = "Failed to update patch bundle(s)" + } +} + +class JsonPatchBundle(name: String, id: Int, directory: File, endpoint: String) : + RemotePatchBundle(name, id, directory, endpoint) { + override suspend fun getLatestInfo() = withContext(Dispatchers.IO) { + http.request<ReVancedAsset> { + url(endpoint) + }.getOrThrow() + } +} + +class APIPatchBundle(name: String, id: Int, directory: File, endpoint: String) : + RemotePatchBundle(name, id, directory, endpoint) { + private val api: ReVancedAPI by inject() + + override suspend fun getLatestInfo() = api.getPatchesUpdate().getOrThrow() +} \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/domain/installer/RootInstaller.kt b/app/src/main/java/app/revanced/manager/domain/installer/RootInstaller.kt new file mode 100644 index 0000000000..293484ca04 --- /dev/null +++ b/app/src/main/java/app/revanced/manager/domain/installer/RootInstaller.kt @@ -0,0 +1,181 @@ +package app.revanced.manager.domain.installer + +import android.app.Application +import android.content.ComponentName +import android.content.Intent +import android.content.ServiceConnection +import android.os.IBinder +import app.revanced.manager.IRootSystemService +import app.revanced.manager.service.ManagerRootService +import app.revanced.manager.util.PM +import com.topjohnwu.superuser.Shell +import com.topjohnwu.superuser.ipc.RootService +import com.topjohnwu.superuser.nio.FileSystemManager +import kotlinx.coroutines.CompletableDeferred +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.time.withTimeoutOrNull +import kotlinx.coroutines.withContext +import java.io.File +import java.time.Duration + +class RootInstaller( + private val app: Application, + private val pm: PM +) : ServiceConnection { + private var remoteFS = CompletableDeferred<FileSystemManager>() + + override fun onServiceConnected(name: ComponentName?, service: IBinder?) { + val ipc = IRootSystemService.Stub.asInterface(service) + val binder = ipc.fileSystemService + + remoteFS.complete(FileSystemManager.getRemote(binder)) + } + + override fun onServiceDisconnected(name: ComponentName?) { + remoteFS = CompletableDeferred() + } + + private suspend fun awaitRemoteFS(): FileSystemManager { + if (remoteFS.isActive) { + withContext(Dispatchers.Main) { + val intent = Intent(app, ManagerRootService::class.java) + RootService.bind(intent, this@RootInstaller) + } + } + + return withTimeoutOrNull(Duration.ofSeconds(20L)) { + remoteFS.await() + } ?: throw RootServiceException() + } + + private suspend fun getShell() = with(CompletableDeferred<Shell>()) { + Shell.getShell(::complete) + + await() + } + + suspend fun execute(vararg commands: String) = getShell().newJob().add(*commands).exec() + + fun hasRootAccess() = Shell.isAppGrantedRoot() ?: false + + fun isDeviceRooted() = System.getenv("PATH")?.split(":")?.any { path -> + File(path, "su").canExecute() + } ?: false + + suspend fun isAppInstalled(packageName: String) = + awaitRemoteFS().getFile("$modulesPath/$packageName-revanced").exists() + + suspend fun isAppMounted(packageName: String) = withContext(Dispatchers.IO) { + pm.getPackageInfo(packageName)?.applicationInfo?.sourceDir?.let { + execute("mount | grep \"$it\"").isSuccess + } ?: false + } + + suspend fun mount(packageName: String) { + if (isAppMounted(packageName)) return + + withContext(Dispatchers.IO) { + val stockAPK = pm.getPackageInfo(packageName)?.applicationInfo?.sourceDir + ?: throw Exception("Failed to load application info") + val patchedAPK = "$modulesPath/$packageName-revanced/$packageName.apk" + + execute("mount -o bind \"$patchedAPK\" \"$stockAPK\"").assertSuccess("Failed to mount APK") + } + } + + suspend fun unmount(packageName: String) { + if (!isAppMounted(packageName)) return + + withContext(Dispatchers.IO) { + val stockAPK = pm.getPackageInfo(packageName)?.applicationInfo?.sourceDir + ?: throw Exception("Failed to load application info") + + execute("umount -l \"$stockAPK\"").assertSuccess("Failed to unmount APK") + } + } + + suspend fun install( + patchedAPK: File, + stockAPK: File?, + packageName: String, + version: String, + label: String + ) = withContext(Dispatchers.IO) { + val remoteFS = awaitRemoteFS() + val assets = app.assets + val modulePath = "$modulesPath/$packageName-revanced" + + unmount(packageName) + + stockAPK?.let { stockApp -> + pm.getPackageInfo(packageName)?.let { packageInfo -> + // TODO: get user id programmatically + if (pm.getVersionCode(packageInfo) <= pm.getVersionCode( + pm.getPackageInfo(patchedAPK) + ?: error("Failed to get package info for patched app") + ) + ) + execute("pm uninstall -k --user 0 $packageName").assertSuccess("Failed to uninstall stock app") + } + + execute("pm install \"${stockApp.absolutePath}\"").assertSuccess("Failed to install stock app") + } + + remoteFS.getFile(modulePath).mkdir() + + listOf( + "service.sh", + "module.prop", + ).forEach { file -> + assets.open("root/$file").use { inputStream -> + remoteFS.getFile("$modulePath/$file").newOutputStream() + .use { outputStream -> + val content = String(inputStream.readBytes()) + .replace("__PKG_NAME__", packageName) + .replace("__VERSION__", version) + .replace("__LABEL__", label) + .toByteArray() + + outputStream.write(content) + } + } + } + + "$modulePath/$packageName.apk".let { apkPath -> + + remoteFS.getFile(patchedAPK.absolutePath) + .also { if (!it.exists()) throw Exception("File doesn't exist") } + .newInputStream().use { inputStream -> + remoteFS.getFile(apkPath).newOutputStream().use { outputStream -> + inputStream.copyTo(outputStream) + } + } + + execute( + "chmod 644 $apkPath", + "chown system:system $apkPath", + "chcon u:object_r:apk_data_file:s0 $apkPath", + "chmod +x $modulePath/service.sh" + ).assertSuccess("Failed to set file permissions") + } + } + + suspend fun uninstall(packageName: String) { + val remoteFS = awaitRemoteFS() + if (isAppMounted(packageName)) + unmount(packageName) + + remoteFS.getFile("$modulesPath/$packageName-revanced").deleteRecursively() + .also { if (!it) throw Exception("Failed to delete files") } + } + + companion object { + const val modulesPath = "/data/adb/modules" + + private fun Shell.Result.assertSuccess(errorMessage: String) { + if (!isSuccess) throw Exception(errorMessage) + } + } +} + +class RootServiceException : Exception("Root not available") \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/domain/manager/KeystoreManager.kt b/app/src/main/java/app/revanced/manager/domain/manager/KeystoreManager.kt new file mode 100644 index 0000000000..4f9dc5a34d --- /dev/null +++ b/app/src/main/java/app/revanced/manager/domain/manager/KeystoreManager.kt @@ -0,0 +1,95 @@ +package app.revanced.manager.domain.manager + +import android.app.Application +import android.content.Context +import app.revanced.library.ApkSigner +import app.revanced.library.ApkUtils +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import java.io.ByteArrayInputStream +import java.io.File +import java.io.InputStream +import java.io.OutputStream +import java.nio.file.Files +import java.security.UnrecoverableKeyException +import java.util.Date +import kotlin.time.Duration.Companion.days + +class KeystoreManager(app: Application, private val prefs: PreferencesManager) { + companion object Constants { + /** + * Default alias and password for the keystore. + */ + const val DEFAULT = "ReVanced" + private val eightYearsFromNow get() = Date(System.currentTimeMillis() + (365.days * 8).inWholeMilliseconds * 24) + } + + private val keystorePath = + app.getDir("signing", Context.MODE_PRIVATE).resolve("manager.keystore") + + private suspend fun updatePrefs(cn: String, pass: String) = prefs.edit { + prefs.keystoreCommonName.value = cn + prefs.keystorePass.value = pass + } + + private suspend fun signingDetails(path: File = keystorePath) = ApkUtils.KeyStoreDetails( + keyStore = path, + keyStorePassword = null, + alias = prefs.keystoreCommonName.get(), + password = prefs.keystorePass.get() + ) + + suspend fun sign(input: File, output: File) = withContext(Dispatchers.Default) { + ApkUtils.signApk(input, output, prefs.keystoreCommonName.get(), signingDetails()) + } + + suspend fun regenerate() = withContext(Dispatchers.Default) { + val keyCertPair = ApkSigner.newPrivateKeyCertificatePair( + prefs.keystoreCommonName.get(), + eightYearsFromNow + ) + val ks = ApkSigner.newKeyStore( + setOf( + ApkSigner.KeyStoreEntry( + DEFAULT, DEFAULT, keyCertPair + ) + ) + ) + withContext(Dispatchers.IO) { + keystorePath.outputStream().use { + ks.store(it, null) + } + } + + updatePrefs(DEFAULT, DEFAULT) + } + + suspend fun import(cn: String, pass: String, keystore: InputStream): Boolean { + val keystoreData = withContext(Dispatchers.IO) { keystore.readBytes() } + + try { + val ks = ApkSigner.readKeyStore(ByteArrayInputStream(keystoreData), null) + + ApkSigner.readPrivateKeyCertificatePair(ks, cn, pass) + } catch (_: UnrecoverableKeyException) { + return false + } catch (_: IllegalArgumentException) { + return false + } + + withContext(Dispatchers.IO) { + Files.write(keystorePath.toPath(), keystoreData) + } + + updatePrefs(cn, pass) + return true + } + + fun hasKeystore() = keystorePath.exists() + + suspend fun export(target: OutputStream) { + withContext(Dispatchers.IO) { + Files.copy(keystorePath.toPath(), target) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/domain/manager/PreferencesManager.kt b/app/src/main/java/app/revanced/manager/domain/manager/PreferencesManager.kt new file mode 100644 index 0000000000..dbf2f1005d --- /dev/null +++ b/app/src/main/java/app/revanced/manager/domain/manager/PreferencesManager.kt @@ -0,0 +1,31 @@ +package app.revanced.manager.domain.manager + +import android.content.Context +import app.revanced.manager.domain.manager.base.BasePreferencesManager +import app.revanced.manager.ui.theme.Theme + +class PreferencesManager( + context: Context +) : BasePreferencesManager(context, "settings") { + val dynamicColor = booleanPreference("dynamic_color", true) + val theme = enumPreference("theme", Theme.SYSTEM) + + val api = stringPreference("api_url", "https://api.revanced.app") + + val useProcessRuntime = booleanPreference("use_process_runtime", false) + val patcherProcessMemoryLimit = intPreference("process_runtime_memory_limit", 700) + + val keystoreCommonName = stringPreference("keystore_cn", KeystoreManager.DEFAULT) + val keystorePass = stringPreference("keystore_pass", KeystoreManager.DEFAULT) + + val firstLaunch = booleanPreference("first_launch", true) + val managerAutoUpdates = booleanPreference("manager_auto_updates", false) + val showManagerUpdateDialogOnLaunch = booleanPreference("show_manager_update_dialog_on_launch", true) + + val disablePatchVersionCompatCheck = booleanPreference("disable_patch_version_compatibility_check", false) + val disableSelectionWarning = booleanPreference("disable_selection_warning", false) + val disableUniversalPatchWarning = booleanPreference("disable_universal_patch_warning", false) + val suggestedVersionSafeguard = booleanPreference("suggested_version_safeguard", true) + + val acknowledgedDownloaderPlugins = stringSetPreference("acknowledged_downloader_plugins", emptySet()) +} diff --git a/app/src/main/java/app/revanced/manager/domain/manager/base/BasePreferencesManager.kt b/app/src/main/java/app/revanced/manager/domain/manager/base/BasePreferencesManager.kt new file mode 100644 index 0000000000..06f75465d3 --- /dev/null +++ b/app/src/main/java/app/revanced/manager/domain/manager/base/BasePreferencesManager.kt @@ -0,0 +1,150 @@ +package app.revanced.manager.domain.manager.base + +import android.content.Context +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.datastore.core.DataStore +import androidx.datastore.preferences.core.* +import androidx.datastore.preferences.preferencesDataStore +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import app.revanced.manager.domain.manager.base.BasePreferencesManager.Companion.editor +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.runBlocking + +abstract class BasePreferencesManager(private val context: Context, name: String) { + private val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = name) + protected val dataStore get() = context.dataStore + + suspend fun preload() { + dataStore.data.first() + } + + suspend fun edit(block: EditorContext.() -> Unit) = dataStore.editor(block) + + protected fun stringPreference(key: String, default: String) = + StringPreference(dataStore, key, default) + + protected fun stringSetPreference(key: String, default: Set<String>) = + StringSetPreference(dataStore, key, default) + + protected fun booleanPreference(key: String, default: Boolean) = + BooleanPreference(dataStore, key, default) + + protected fun intPreference(key: String, default: Int) = IntPreference(dataStore, key, default) + + protected fun floatPreference(key: String, default: Float) = + FloatPreference(dataStore, key, default) + + protected inline fun <reified E : Enum<E>> enumPreference( + key: String, + default: E + ) = EnumPreference(dataStore, key, default, enumValues()) + + companion object { + suspend inline fun DataStore<Preferences>.editor(crossinline block: EditorContext.() -> Unit) { + edit { + EditorContext(it).run(block) + } + } + } +} + +class EditorContext(private val prefs: MutablePreferences) { + var <T> Preference<T>.value + get() = prefs.run { read() } + set(value) = prefs.run { write(value) } + + operator fun Preference<Set<String>>.plusAssign(value: String) = prefs.run { + write(read() + value) + } +} + +abstract class Preference<T>( + private val dataStore: DataStore<Preferences>, + val default: T +) { + internal abstract fun Preferences.read(): T + internal abstract fun MutablePreferences.write(value: T) + + val flow = dataStore.data.map { with(it) { read() } ?: default }.distinctUntilChanged() + + suspend fun get() = flow.first() + fun getBlocking() = runBlocking { get() } + + @Composable + fun getAsState() = flow.collectAsStateWithLifecycle(initialValue = remember { + getBlocking() + }) + + suspend fun update(value: T) = dataStore.editor { + this@Preference.value = value + } +} + +class EnumPreference<E : Enum<E>>( + dataStore: DataStore<Preferences>, + key: String, + default: E, + private val enumValues: Array<E> +) : Preference<E>(dataStore, default) { + private val key = stringPreferencesKey(key) + override fun Preferences.read() = + this[key]?.let { name -> + enumValues.find { it.name == name } + } ?: default + + override fun MutablePreferences.write(value: E) { + this[key] = value.name + } +} + +abstract class BasePreference<T>(dataStore: DataStore<Preferences>, default: T) : + Preference<T>(dataStore, default) { + protected abstract val key: Preferences.Key<T> + override fun Preferences.read() = this[key] ?: default + override fun MutablePreferences.write(value: T) { + this[key] = value + } +} + +class StringPreference( + dataStore: DataStore<Preferences>, + key: String, + default: String +) : BasePreference<String>(dataStore, default) { + override val key = stringPreferencesKey(key) +} + +class StringSetPreference( + dataStore: DataStore<Preferences>, + key: String, + default: Set<String> +) : BasePreference<Set<String>>(dataStore, default) { + override val key = stringSetPreferencesKey(key) +} + +class BooleanPreference( + dataStore: DataStore<Preferences>, + key: String, + default: Boolean +) : BasePreference<Boolean>(dataStore, default) { + override val key = booleanPreferencesKey(key) +} + +class IntPreference( + dataStore: DataStore<Preferences>, + key: String, + default: Int +) : BasePreference<Int>(dataStore, default) { + override val key = intPreferencesKey(key) +} + +class FloatPreference( + dataStore: DataStore<Preferences>, + key: String, + default: Float +) : BasePreference<Float>(dataStore, default) { + override val key = floatPreferencesKey(key) +} \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/domain/repository/DownloadedAppRepository.kt b/app/src/main/java/app/revanced/manager/domain/repository/DownloadedAppRepository.kt new file mode 100644 index 0000000000..b4598fb915 --- /dev/null +++ b/app/src/main/java/app/revanced/manager/domain/repository/DownloadedAppRepository.kt @@ -0,0 +1,133 @@ +package app.revanced.manager.domain.repository + +import android.app.Application +import android.content.Context +import android.os.Parcelable +import app.revanced.manager.data.room.AppDatabase +import app.revanced.manager.data.room.AppDatabase.Companion.generateUid +import app.revanced.manager.data.room.apps.downloaded.DownloadedApp +import app.revanced.manager.network.downloader.LoadedDownloaderPlugin +import app.revanced.manager.plugin.downloader.OutputDownloadScope +import app.revanced.manager.util.PM +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.channelFlow +import kotlinx.coroutines.flow.conflate +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.flowOn +import java.io.File +import java.io.FilterOutputStream +import java.nio.file.StandardOpenOption +import java.util.concurrent.atomic.AtomicLong +import kotlin.io.path.outputStream + +class DownloadedAppRepository( + private val app: Application, + db: AppDatabase, + private val pm: PM +) { + private val dir = app.getDir("downloaded-apps", Context.MODE_PRIVATE) + private val dao = db.downloadedAppDao() + + fun getAll() = dao.getAllApps().distinctUntilChanged() + + fun getApkFileForApp(app: DownloadedApp): File = + getApkFileForDir(dir.resolve(app.directory)) + + private fun getApkFileForDir(directory: File) = directory.listFiles()!!.first() + + suspend fun download( + plugin: LoadedDownloaderPlugin, + data: Parcelable, + expectedPackageName: String, + expectedVersion: String?, + onDownload: suspend (downloadProgress: Pair<Long, Long?>) -> Unit, + ): File { + // Converted integers cannot contain / or .. unlike the package name or version, so they are safer to use here. + val relativePath = File(generateUid().toString()) + val saveDir = dir.resolve(relativePath).also { it.mkdirs() } + val targetFile = saveDir.resolve("base.apk").toPath() + + try { + val downloadSize = AtomicLong(0) + val downloadedBytes = AtomicLong(0) + + channelFlow { + val scope = object : OutputDownloadScope { + override val pluginPackageName = plugin.packageName + override val hostPackageName = app.packageName + override suspend fun reportSize(size: Long) { + require(size > 0) { "Size must be greater than zero" } + require( + downloadSize.compareAndSet( + 0, + size + ) + ) { "Download size has already been set" } + send(downloadedBytes.get() to size) + } + } + + fun emitProgress(bytes: Long) { + val newValue = downloadedBytes.addAndGet(bytes) + val totalSize = downloadSize.get() + if (totalSize < 1) return + trySend(newValue to totalSize).getOrThrow() + } + + targetFile.outputStream(StandardOpenOption.CREATE_NEW).buffered().use { + val stream = object : FilterOutputStream(it) { + override fun write(b: Int) = out.write(b).also { emitProgress(1) } + + override fun write(b: ByteArray?, off: Int, len: Int) = + out.write(b, off, len).also { + emitProgress( + (len - off).toLong() + ) + } + } + plugin.download(scope, data, stream) + } + } + .conflate() + .flowOn(Dispatchers.IO) + .collect { (downloaded, size) -> onDownload(downloaded to size) } + + if (downloadedBytes.get() < 1) error("Downloader did not download anything.") + val pkgInfo = + pm.getPackageInfo(targetFile.toFile()) ?: error("Downloaded APK file is invalid") + if (pkgInfo.packageName != expectedPackageName) error("Downloaded APK has the wrong package name. Expected: $expectedPackageName, Actual: ${pkgInfo.packageName}") + if (expectedVersion != null && pkgInfo.versionName != expectedVersion) error("Downloaded APK has the wrong version. Expected: $expectedVersion, Actual: ${pkgInfo.versionName}") + + // Delete the previous copy (if present). + dao.get(pkgInfo.packageName, pkgInfo.versionName!!)?.directory?.let { + if (!dir.resolve(it).deleteRecursively()) throw Exception("Failed to delete existing directory") + } + dao.upsert( + DownloadedApp( + packageName = pkgInfo.packageName, + version = pkgInfo.versionName!!, + directory = relativePath, + ) + ) + } catch (e: Exception) { + saveDir.deleteRecursively() + throw e + } + + // Return the Apk file. + return getApkFileForDir(saveDir) + } + + suspend fun get(packageName: String, version: String, markUsed: Boolean = false) = + dao.get(packageName, version)?.also { + if (markUsed) dao.markUsed(packageName, version) + } + + suspend fun delete(downloadedApps: Collection<DownloadedApp>) { + downloadedApps.forEach { + dir.resolve(it.directory).deleteRecursively() + } + + dao.delete(downloadedApps) + } +} \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/domain/repository/DownloaderPluginRepository.kt b/app/src/main/java/app/revanced/manager/domain/repository/DownloaderPluginRepository.kt new file mode 100644 index 0000000000..791a09ac11 --- /dev/null +++ b/app/src/main/java/app/revanced/manager/domain/repository/DownloaderPluginRepository.kt @@ -0,0 +1,168 @@ +package app.revanced.manager.domain.repository + +import android.app.Application +import android.content.pm.PackageManager +import android.os.Parcelable +import android.util.Log +import app.revanced.manager.data.room.AppDatabase +import app.revanced.manager.data.room.plugins.TrustedDownloaderPlugin +import app.revanced.manager.domain.manager.PreferencesManager +import app.revanced.manager.network.downloader.DownloaderPluginState +import app.revanced.manager.network.downloader.LoadedDownloaderPlugin +import app.revanced.manager.network.downloader.ParceledDownloaderData +import app.revanced.manager.plugin.downloader.DownloaderBuilder +import app.revanced.manager.plugin.downloader.PluginHostApi +import app.revanced.manager.plugin.downloader.Scope +import app.revanced.manager.util.PM +import app.revanced.manager.util.tag +import dalvik.system.PathClassLoader +import kotlinx.coroutines.CancellationException +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.withContext +import java.lang.reflect.Modifier + +@OptIn(PluginHostApi::class) +class DownloaderPluginRepository( + private val pm: PM, + private val prefs: PreferencesManager, + private val app: Application, + db: AppDatabase +) { + private val trustDao = db.trustedDownloaderPluginDao() + private val _pluginStates = MutableStateFlow(emptyMap<String, DownloaderPluginState>()) + val pluginStates = _pluginStates.asStateFlow() + val loadedPluginsFlow = pluginStates.map { states -> + states.values.filterIsInstance<DownloaderPluginState.Loaded>().map { it.plugin } + } + + private val acknowledgedDownloaderPlugins = prefs.acknowledgedDownloaderPlugins + private val installedPluginPackageNames = MutableStateFlow(emptySet<String>()) + val newPluginPackageNames = combine( + installedPluginPackageNames, + acknowledgedDownloaderPlugins.flow + ) { installed, acknowledged -> + installed subtract acknowledged + } + + suspend fun reload() { + val plugins = + withContext(Dispatchers.IO) { + pm.getPackagesWithFeature(PLUGIN_FEATURE) + .associate { it.packageName to loadPlugin(it.packageName) } + } + + _pluginStates.value = plugins + installedPluginPackageNames.value = plugins.keys + + val acknowledgedPlugins = acknowledgedDownloaderPlugins.get() + val uninstalledPlugins = acknowledgedPlugins subtract installedPluginPackageNames.value + if (uninstalledPlugins.isNotEmpty()) { + Log.d(tag, "Uninstalled plugins: ${uninstalledPlugins.joinToString(", ")}") + acknowledgedDownloaderPlugins.update(acknowledgedPlugins subtract uninstalledPlugins) + trustDao.removeAll(uninstalledPlugins) + } + } + + fun unwrapParceledData(data: ParceledDownloaderData): Pair<LoadedDownloaderPlugin, Parcelable> { + val plugin = + (_pluginStates.value[data.pluginPackageName] as? DownloaderPluginState.Loaded)?.plugin + ?: throw Exception("Downloader plugin with name ${data.pluginPackageName} is not available") + + return plugin to data.unwrapWith(plugin) + } + + private suspend fun loadPlugin(packageName: String): DownloaderPluginState { + try { + if (!verify(packageName)) return DownloaderPluginState.Untrusted + } catch (e: CancellationException) { + throw e + } catch (e: Exception) { + Log.e(tag, "Got exception while verifying plugin $packageName", e) + return DownloaderPluginState.Failed(e) + } + + return try { + val packageInfo = pm.getPackageInfo(packageName, flags = PackageManager.GET_META_DATA)!! + val className = packageInfo.applicationInfo!!.metaData.getString(METADATA_PLUGIN_CLASS) + ?: throw Exception("Missing metadata attribute $METADATA_PLUGIN_CLASS") + + val classLoader = + PathClassLoader(packageInfo.applicationInfo!!.sourceDir, app.classLoader) + val pluginContext = app.createPackageContext(packageName, 0) + + val downloader = classLoader + .loadClass(className) + .getDownloaderBuilder() + .build( + scopeImpl = object : Scope { + override val hostPackageName = app.packageName + override val pluginPackageName = pluginContext.packageName + }, + context = pluginContext + ) + + DownloaderPluginState.Loaded( + LoadedDownloaderPlugin( + packageName, + with(pm) { packageInfo.label() }, + packageInfo.versionName!!, + downloader.get, + downloader.download, + classLoader + ) + ) + } catch (e: CancellationException) { + throw e + } catch (t: Throwable) { + Log.e(tag, "Failed to load plugin $packageName", t) + DownloaderPluginState.Failed(t) + } + } + + suspend fun trustPackage(packageName: String) { + trustDao.upsertTrust( + TrustedDownloaderPlugin( + packageName, + pm.getSignature(packageName).toByteArray() + ) + ) + + reload() + prefs.edit { + acknowledgedDownloaderPlugins += packageName + } + } + + suspend fun revokeTrustForPackage(packageName: String) = + trustDao.remove(packageName).also { reload() } + + suspend fun acknowledgeAllNewPlugins() = + acknowledgedDownloaderPlugins.update(installedPluginPackageNames.value) + + private suspend fun verify(packageName: String): Boolean { + val expectedSignature = + trustDao.getTrustedSignature(packageName) ?: return false + + return pm.hasSignature(packageName, expectedSignature) + } + + private companion object { + const val PLUGIN_FEATURE = "app.revanced.manager.plugin.downloader" + const val METADATA_PLUGIN_CLASS = "app.revanced.manager.plugin.downloader.class" + + const val PUBLIC_STATIC = Modifier.PUBLIC or Modifier.STATIC + val Int.isPublicStatic get() = (this and PUBLIC_STATIC) == PUBLIC_STATIC + val Class<*>.isDownloaderBuilder get() = DownloaderBuilder::class.java.isAssignableFrom(this) + + @Suppress("UNCHECKED_CAST") + fun Class<*>.getDownloaderBuilder() = + declaredMethods + .firstOrNull { it.modifiers.isPublicStatic && it.returnType.isDownloaderBuilder && it.parameterTypes.isEmpty() } + ?.let { it(null) as DownloaderBuilder<Parcelable> } + ?: throw Exception("Could not find a valid downloader implementation in class $canonicalName") + } +} \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/domain/repository/InstalledAppRepository.kt b/app/src/main/java/app/revanced/manager/domain/repository/InstalledAppRepository.kt new file mode 100644 index 0000000000..cd3a010007 --- /dev/null +++ b/app/src/main/java/app/revanced/manager/domain/repository/InstalledAppRepository.kt @@ -0,0 +1,51 @@ +package app.revanced.manager.domain.repository + +import app.revanced.manager.data.room.AppDatabase +import app.revanced.manager.data.room.apps.installed.AppliedPatch +import app.revanced.manager.data.room.apps.installed.InstallType +import app.revanced.manager.data.room.apps.installed.InstalledApp +import app.revanced.manager.util.PatchSelection +import kotlinx.coroutines.flow.distinctUntilChanged + +class InstalledAppRepository( + db: AppDatabase +) { + private val dao = db.installedAppDao() + + fun getAll() = dao.getAll().distinctUntilChanged() + + suspend fun get(packageName: String) = dao.get(packageName) + + suspend fun getAppliedPatches(packageName: String): PatchSelection = + dao.getPatchesSelection(packageName).mapValues { (_, patches) -> patches.toSet() } + + suspend fun addOrUpdate( + currentPackageName: String, + originalPackageName: String, + version: String, + installType: InstallType, + patchSelection: PatchSelection + ) { + dao.upsertApp( + InstalledApp( + currentPackageName = currentPackageName, + originalPackageName = originalPackageName, + version = version, + installType = installType + ), + patchSelection.flatMap { (uid, patches) -> + patches.map { patch -> + AppliedPatch( + packageName = currentPackageName, + bundle = uid, + patchName = patch + ) + } + } + ) + } + + suspend fun delete(installedApp: InstalledApp) { + dao.delete(installedApp) + } +} \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/domain/repository/PatchBundlePersistenceRepository.kt b/app/src/main/java/app/revanced/manager/domain/repository/PatchBundlePersistenceRepository.kt new file mode 100644 index 0000000000..5711d99758 --- /dev/null +++ b/app/src/main/java/app/revanced/manager/domain/repository/PatchBundlePersistenceRepository.kt @@ -0,0 +1,55 @@ +package app.revanced.manager.domain.repository + +import app.revanced.manager.data.room.AppDatabase +import app.revanced.manager.data.room.AppDatabase.Companion.generateUid +import app.revanced.manager.data.room.bundles.PatchBundleEntity +import app.revanced.manager.data.room.bundles.Source +import kotlinx.coroutines.flow.distinctUntilChanged + +class PatchBundlePersistenceRepository(db: AppDatabase) { + private val dao = db.patchBundleDao() + + suspend fun loadConfiguration(): List<PatchBundleEntity> { + val all = dao.all() + if (all.isEmpty()) { + dao.add(defaultSource) + return listOf(defaultSource) + } + + return all + } + + suspend fun reset() = dao.reset() + + suspend fun create(name: String, source: Source, autoUpdate: Boolean = false) = + PatchBundleEntity( + uid = generateUid(), + name = name, + version = null, + source = source, + autoUpdate = autoUpdate + ).also { + dao.add(it) + } + + suspend fun delete(uid: Int) = dao.remove(uid) + + suspend fun updateVersion(uid: Int, version: String?) = + dao.updateVersion(uid, version) + + suspend fun setAutoUpdate(uid: Int, value: Boolean) = dao.setAutoUpdate(uid, value) + + suspend fun setName(uid: Int, name: String) = dao.setName(uid, name) + + fun getProps(id: Int) = dao.getPropsById(id).distinctUntilChanged() + + private companion object { + val defaultSource = PatchBundleEntity( + uid = 0, + name = "", + version = null, + source = Source.API, + autoUpdate = false + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/domain/repository/PatchBundleRepository.kt b/app/src/main/java/app/revanced/manager/domain/repository/PatchBundleRepository.kt new file mode 100644 index 0000000000..79bb5cea64 --- /dev/null +++ b/app/src/main/java/app/revanced/manager/domain/repository/PatchBundleRepository.kt @@ -0,0 +1,184 @@ +package app.revanced.manager.domain.repository + +import android.app.Application +import android.content.Context +import android.util.Log +import app.revanced.library.mostCommonCompatibleVersions +import app.revanced.manager.R +import app.revanced.manager.data.platform.NetworkInfo +import app.revanced.manager.data.room.bundles.PatchBundleEntity +import app.revanced.manager.domain.bundles.APIPatchBundle +import app.revanced.manager.domain.bundles.JsonPatchBundle +import app.revanced.manager.data.room.bundles.Source as SourceInfo +import app.revanced.manager.domain.bundles.LocalPatchBundle +import app.revanced.manager.domain.bundles.RemotePatchBundle +import app.revanced.manager.domain.bundles.PatchBundleSource +import app.revanced.manager.domain.manager.PreferencesManager +import app.revanced.manager.patcher.patch.PatchInfo +import app.revanced.manager.util.flatMapLatestAndCombine +import app.revanced.manager.util.tag +import app.revanced.manager.util.uiSafe +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import java.io.InputStream + +class PatchBundleRepository( + private val app: Application, + private val persistenceRepo: PatchBundlePersistenceRepository, + private val networkInfo: NetworkInfo, + private val prefs: PreferencesManager, +) { + private val bundlesDir = app.getDir("patch_bundles", Context.MODE_PRIVATE) + + private val _sources: MutableStateFlow<Map<Int, PatchBundleSource>> = + MutableStateFlow(emptyMap()) + val sources = _sources.map { it.values.toList() } + + val bundles = sources.flatMapLatestAndCombine( + combiner = { + it.mapNotNull { (uid, state) -> + val bundle = state.patchBundleOrNull() ?: return@mapNotNull null + uid to bundle + }.toMap() + } + ) { + it.state.map { state -> it.uid to state } + } + + val suggestedVersions = bundles.map { + val allPatches = + it.values.flatMap { bundle -> bundle.patches.map(PatchInfo::toPatcherPatch) }.toSet() + + allPatches.mostCommonCompatibleVersions(countUnusedPatches = true) + .mapValues { (_, versions) -> + if (versions.keys.size < 2) + return@mapValues versions.keys.firstOrNull() + + // The entries are ordered from most compatible to least compatible. + // If there are entries with the same number of compatible patches, older versions will be first, which is undesirable. + // This means we have to pick the last entry we find that has the highest patch count. + // The order may change in future versions of ReVanced Library. + var currentHighestPatchCount = -1 + versions.entries.last { (_, patchCount) -> + if (patchCount >= currentHighestPatchCount) { + currentHighestPatchCount = patchCount + true + } else false + }.key + } + } + + suspend fun isVersionAllowed(packageName: String, version: String) = + withContext(Dispatchers.Default) { + if (!prefs.suggestedVersionSafeguard.get()) return@withContext true + + val suggestedVersion = suggestedVersions.first()[packageName] ?: return@withContext true + suggestedVersion == version + } + + /** + * Get the directory of the [PatchBundleSource] with the specified [uid], creating it if needed. + */ + private fun directoryOf(uid: Int) = bundlesDir.resolve(uid.toString()).also { it.mkdirs() } + + private fun PatchBundleEntity.load(): PatchBundleSource { + val dir = directoryOf(uid) + + return when (source) { + is SourceInfo.Local -> LocalPatchBundle(name, uid, dir) + is SourceInfo.API -> APIPatchBundle(name, uid, dir, SourceInfo.API.SENTINEL) + is SourceInfo.Remote -> JsonPatchBundle( + name, + uid, + dir, + source.url.toString() + ) + } + } + + suspend fun reload() = withContext(Dispatchers.Default) { + val entities = persistenceRepo.loadConfiguration().onEach { + Log.d(tag, "Bundle: $it") + } + + _sources.value = entities.associate { + it.uid to it.load() + } + } + + suspend fun reset() = withContext(Dispatchers.Default) { + persistenceRepo.reset() + _sources.value = emptyMap() + bundlesDir.apply { + deleteRecursively() + mkdirs() + } + + reload() + } + + suspend fun remove(bundle: PatchBundleSource) = withContext(Dispatchers.Default) { + persistenceRepo.delete(bundle.uid) + directoryOf(bundle.uid).deleteRecursively() + + _sources.update { + it.filterKeys { key -> + key != bundle.uid + } + } + } + + private fun addBundle(patchBundle: PatchBundleSource) = + _sources.update { it.toMutableMap().apply { put(patchBundle.uid, patchBundle) } } + + suspend fun createLocal(patches: InputStream) = withContext(Dispatchers.Default) { + val uid = persistenceRepo.create("", SourceInfo.Local).uid + val bundle = LocalPatchBundle("", uid, directoryOf(uid)) + + bundle.replace(patches) + addBundle(bundle) + } + + suspend fun createRemote(url: String, autoUpdate: Boolean) = withContext(Dispatchers.Default) { + val entity = persistenceRepo.create("", SourceInfo.from(url), autoUpdate) + addBundle(entity.load()) + } + + private suspend inline fun <reified T> getBundlesByType() = + sources.first().filterIsInstance<T>() + + suspend fun reloadApiBundles() { + getBundlesByType<APIPatchBundle>().forEach { + it.deleteLocalFiles() + } + + reload() + } + + suspend fun redownloadRemoteBundles() = + getBundlesByType<RemotePatchBundle>().forEach { it.downloadLatest() } + + suspend fun updateCheck() = + uiSafe(app, R.string.source_download_fail, "Failed to update bundles") { + coroutineScope { + if (!networkInfo.isSafe()) { + Log.d(tag, "Skipping update check because the network is down or metered.") + return@coroutineScope + } + + getBundlesByType<RemotePatchBundle>().forEach { + launch { + if (!it.getProps().autoUpdate) return@launch + Log.d(tag, "Updating patch bundle: ${it.getName()}") + it.update() + } + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/domain/repository/PatchOptionsRepository.kt b/app/src/main/java/app/revanced/manager/domain/repository/PatchOptionsRepository.kt new file mode 100644 index 0000000000..9fe5fdc298 --- /dev/null +++ b/app/src/main/java/app/revanced/manager/domain/repository/PatchOptionsRepository.kt @@ -0,0 +1,82 @@ +package app.revanced.manager.domain.repository + +import android.util.Log +import app.revanced.manager.data.room.AppDatabase +import app.revanced.manager.data.room.options.Option +import app.revanced.manager.data.room.options.OptionGroup +import app.revanced.manager.patcher.patch.PatchInfo +import app.revanced.manager.util.Options +import app.revanced.manager.util.tag +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.map + +class PatchOptionsRepository(db: AppDatabase) { + private val dao = db.optionDao() + + private suspend fun getOrCreateGroup(bundleUid: Int, packageName: String) = + dao.getGroupId(bundleUid, packageName) ?: OptionGroup( + uid = AppDatabase.generateUid(), + patchBundle = bundleUid, + packageName = packageName + ).also { dao.createOptionGroup(it) }.uid + + suspend fun getOptions( + packageName: String, + bundlePatches: Map<Int, Map<String, PatchInfo>> + ): Options { + val options = dao.getOptions(packageName) + // Bundle -> Patches + return buildMap<Int, MutableMap<String, MutableMap<String, Any?>>>(options.size) { + options.forEach { (sourceUid, bundlePatchOptionsList) -> + // Patches -> Patch options + this[sourceUid] = + bundlePatchOptionsList.fold(mutableMapOf()) { bundlePatchOptions, dbOption -> + val deserializedPatchOptions = + bundlePatchOptions.getOrPut(dbOption.patchName, ::mutableMapOf) + + val option = + bundlePatches[sourceUid]?.get(dbOption.patchName)?.options?.find { it.key == dbOption.key } + if (option != null) { + try { + deserializedPatchOptions[option.key] = + dbOption.value.deserializeFor(option) + } catch (e: Option.SerializationException) { + Log.w( + tag, + "Option ${dbOption.patchName}:${option.key} could not be deserialized", + e + ) + } + } + + bundlePatchOptions + } + } + } + } + + suspend fun saveOptions(packageName: String, options: Options) = + dao.updateOptions(options.entries.associate { (sourceUid, bundlePatchOptions) -> + val groupId = getOrCreateGroup(sourceUid, packageName) + + groupId to bundlePatchOptions.flatMap { (patchName, patchOptions) -> + patchOptions.mapNotNull { (key, value) -> + val serialized = try { + Option.SerializedValue.fromValue(value) + } catch (e: Option.SerializationException) { + Log.e(tag, "Option $patchName:$key could not be serialized", e) + return@mapNotNull null + } + + Option(groupId, patchName, key, serialized) + } + } + }) + + fun getPackagesWithSavedOptions() = + dao.getPackagesWithOptions().map(Iterable<String>::toSet).distinctUntilChanged() + + suspend fun clearOptionsForPackage(packageName: String) = dao.clearForPackage(packageName) + suspend fun clearOptionsForPatchBundle(uid: Int) = dao.clearForPatchBundle(uid) + suspend fun reset() = dao.reset() +} \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/domain/repository/PatchSelectionRepository.kt b/app/src/main/java/app/revanced/manager/domain/repository/PatchSelectionRepository.kt new file mode 100644 index 0000000000..c34e5efd6b --- /dev/null +++ b/app/src/main/java/app/revanced/manager/domain/repository/PatchSelectionRepository.kt @@ -0,0 +1,47 @@ +package app.revanced.manager.domain.repository + +import app.revanced.manager.data.room.AppDatabase +import app.revanced.manager.data.room.AppDatabase.Companion.generateUid +import app.revanced.manager.data.room.selection.PatchSelection + +class PatchSelectionRepository(db: AppDatabase) { + private val dao = db.selectionDao() + + private suspend fun getOrCreateSelection(bundleUid: Int, packageName: String) = + dao.getSelectionId(bundleUid, packageName) ?: PatchSelection( + uid = generateUid(), + patchBundle = bundleUid, + packageName = packageName + ).also { dao.createSelection(it) }.uid + + suspend fun getSelection(packageName: String): Map<Int, Set<String>> = + dao.getSelectedPatches(packageName).mapValues { it.value.toSet() } + + suspend fun updateSelection(packageName: String, selection: Map<Int, Set<String>>) = + dao.updateSelections(selection.mapKeys { (sourceUid, _) -> + getOrCreateSelection( + sourceUid, + packageName + ) + }) + + suspend fun clearSelection(packageName: String) { + dao.clearForPackage(packageName) + } + + suspend fun reset() = dao.reset() + + suspend fun export(bundleUid: Int): SerializedSelection = dao.exportSelection(bundleUid) + + suspend fun import(bundleUid: Int, selection: SerializedSelection) { + dao.clearForPatchBundle(bundleUid) + dao.updateSelections(selection.entries.associate { (packageName, patches) -> + getOrCreateSelection(bundleUid, packageName) to patches.toSet() + }) + } +} + +/** + * A [Map] of package name -> selected patches. + */ +typealias SerializedSelection = Map<String, List<String>> \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/domain/worker/Worker.kt b/app/src/main/java/app/revanced/manager/domain/worker/Worker.kt new file mode 100644 index 0000000000..85dc3df355 --- /dev/null +++ b/app/src/main/java/app/revanced/manager/domain/worker/Worker.kt @@ -0,0 +1,7 @@ +package app.revanced.manager.domain.worker + +import android.content.Context +import androidx.work.CoroutineWorker +import androidx.work.WorkerParameters + +abstract class Worker<ARGS>(context: Context, parameters: WorkerParameters) : CoroutineWorker(context, parameters) \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/domain/worker/WorkerRepository.kt b/app/src/main/java/app/revanced/manager/domain/worker/WorkerRepository.kt new file mode 100644 index 0000000000..222a31c48b --- /dev/null +++ b/app/src/main/java/app/revanced/manager/domain/worker/WorkerRepository.kt @@ -0,0 +1,36 @@ +package app.revanced.manager.domain.worker + +import android.app.Application +import androidx.work.ExistingWorkPolicy +import androidx.work.OneTimeWorkRequest +import androidx.work.OutOfQuotaPolicy +import androidx.work.WorkManager +import java.util.UUID + +class WorkerRepository(app: Application) { + val workManager = WorkManager.getInstance(app) + + /** + * The standard WorkManager communication APIs use [androidx.work.Data], which has too many limitations. + * We can get around those limits by passing inputs using global variables instead. + */ + val workerInputs = mutableMapOf<UUID, Any>() + + @Suppress("UNCHECKED_CAST") + fun <A : Any, W : Worker<A>> claimInput(worker: W): A { + val data = workerInputs[worker.id] ?: throw IllegalStateException("Worker was not launched via WorkerRepository") + workerInputs.remove(worker.id) + + return data as A + } + + inline fun <reified W : Worker<A>, A : Any> launchExpedited(name: String, input: A): UUID { + val request = + OneTimeWorkRequest.Builder(W::class.java) // create Worker + .setExpedited(OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST) + .build() + workerInputs[request.id] = input + workManager.enqueueUniqueWork(name, ExistingWorkPolicy.REPLACE, request) + return request.id + } +} \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/network/api/ReVancedAPI.kt b/app/src/main/java/app/revanced/manager/network/api/ReVancedAPI.kt new file mode 100644 index 0000000000..bb36558027 --- /dev/null +++ b/app/src/main/java/app/revanced/manager/network/api/ReVancedAPI.kt @@ -0,0 +1,42 @@ +package app.revanced.manager.network.api + +import android.os.Build +import app.revanced.manager.domain.manager.PreferencesManager +import app.revanced.manager.network.dto.ReVancedAsset +import app.revanced.manager.network.dto.ReVancedGitRepository +import app.revanced.manager.network.dto.ReVancedInfo +import app.revanced.manager.network.service.HttpService +import app.revanced.manager.network.utils.APIResponse +import app.revanced.manager.network.utils.getOrThrow +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import io.ktor.client.request.url + +class ReVancedAPI( + private val client: HttpService, + private val prefs: PreferencesManager +) { + private suspend fun apiUrl() = prefs.api.get() + + private suspend inline fun <reified T> request(api: String, route: String): APIResponse<T> = + withContext( + Dispatchers.IO + ) { + client.request { + url("$api/v4/$route") + } + } + + private suspend inline fun <reified T> request(route: String) = request<T>(apiUrl(), route) + + suspend fun getAppUpdate() = + getLatestAppInfo().getOrThrow().takeIf { it.version != Build.VERSION.RELEASE } + + suspend fun getLatestAppInfo() = request<ReVancedAsset>("manager") + + suspend fun getPatchesUpdate() = request<ReVancedAsset>("patches") + + suspend fun getContributors() = request<List<ReVancedGitRepository>>("contributors") + + suspend fun getInfo(api: String? = null) = request<ReVancedInfo>(api ?: apiUrl(), "about") +} \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/network/downloader/DownloaderPluginState.kt b/app/src/main/java/app/revanced/manager/network/downloader/DownloaderPluginState.kt new file mode 100644 index 0000000000..a72d60c75f --- /dev/null +++ b/app/src/main/java/app/revanced/manager/network/downloader/DownloaderPluginState.kt @@ -0,0 +1,9 @@ +package app.revanced.manager.network.downloader + +sealed interface DownloaderPluginState { + data object Untrusted : DownloaderPluginState + + data class Loaded(val plugin: LoadedDownloaderPlugin) : DownloaderPluginState + + data class Failed(val throwable: Throwable) : DownloaderPluginState +} \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/network/downloader/LoadedDownloaderPlugin.kt b/app/src/main/java/app/revanced/manager/network/downloader/LoadedDownloaderPlugin.kt new file mode 100644 index 0000000000..50ddd561b0 --- /dev/null +++ b/app/src/main/java/app/revanced/manager/network/downloader/LoadedDownloaderPlugin.kt @@ -0,0 +1,15 @@ +package app.revanced.manager.network.downloader + +import android.os.Parcelable +import app.revanced.manager.plugin.downloader.OutputDownloadScope +import app.revanced.manager.plugin.downloader.GetScope +import java.io.OutputStream + +class LoadedDownloaderPlugin( + val packageName: String, + val name: String, + val version: String, + val get: suspend GetScope.(packageName: String, version: String?) -> Pair<Parcelable, String?>?, + val download: suspend OutputDownloadScope.(data: Parcelable, outputStream: OutputStream) -> Unit, + val classLoader: ClassLoader +) \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/network/downloader/ParceledDownloaderData.kt b/app/src/main/java/app/revanced/manager/network/downloader/ParceledDownloaderData.kt new file mode 100644 index 0000000000..a43db93041 --- /dev/null +++ b/app/src/main/java/app/revanced/manager/network/downloader/ParceledDownloaderData.kt @@ -0,0 +1,45 @@ +package app.revanced.manager.network.downloader + +import android.os.Build +import android.os.Bundle +import android.os.Parcelable +import kotlinx.parcelize.Parcelize + +@Parcelize +/** + * A container for [Parcelable] data returned from downloaders. Instances of this class can be safely stored in a bundle without needing to set the [ClassLoader]. + */ +class ParceledDownloaderData private constructor( + val pluginPackageName: String, + private val bundle: Bundle +) : Parcelable { + constructor(plugin: LoadedDownloaderPlugin, data: Parcelable) : this( + plugin.packageName, + createBundle(data) + ) + + fun unwrapWith(plugin: LoadedDownloaderPlugin): Parcelable { + bundle.classLoader = plugin.classLoader + + return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + val className = bundle.getString(CLASS_NAME_KEY)!! + val clazz = plugin.classLoader.loadClass(className) + + bundle.getParcelable(DATA_KEY, clazz)!! as Parcelable + } else @Suppress("Deprecation") bundle.getParcelable(DATA_KEY)!! + } + + private companion object { + const val CLASS_NAME_KEY = "class" + const val DATA_KEY = "data" + + fun createBundle(data: Parcelable) = Bundle().apply { + putParcelable(DATA_KEY, data) + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) putString( + CLASS_NAME_KEY, + data::class.java.canonicalName + ) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/network/dto/ReVancedAsset.kt b/app/src/main/java/app/revanced/manager/network/dto/ReVancedAsset.kt new file mode 100644 index 0000000000..64c05f3133 --- /dev/null +++ b/app/src/main/java/app/revanced/manager/network/dto/ReVancedAsset.kt @@ -0,0 +1,18 @@ +package app.revanced.manager.network.dto + +import kotlinx.datetime.LocalDateTime +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class ReVancedAsset ( + @SerialName("download_url") + val downloadUrl: String, + @SerialName("created_at") + val createdAt: LocalDateTime, + @SerialName("signature_download_url") + val signatureDownloadUrl: String? = null, + val description: String, + val version: String, +) + diff --git a/app/src/main/java/app/revanced/manager/network/dto/ReVancedContributors.kt b/app/src/main/java/app/revanced/manager/network/dto/ReVancedContributors.kt new file mode 100644 index 0000000000..6583ba7c09 --- /dev/null +++ b/app/src/main/java/app/revanced/manager/network/dto/ReVancedContributors.kt @@ -0,0 +1,17 @@ +package app.revanced.manager.network.dto + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class ReVancedGitRepository( + val name: String, + val url: String, + val contributors: List<ReVancedContributor>, +) + +@Serializable +data class ReVancedContributor( + @SerialName("name") val username: String, + @SerialName("avatar_url") val avatarUrl: String, +) \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/network/dto/ReVancedInfo.kt b/app/src/main/java/app/revanced/manager/network/dto/ReVancedInfo.kt new file mode 100644 index 0000000000..89ed7445c6 --- /dev/null +++ b/app/src/main/java/app/revanced/manager/network/dto/ReVancedInfo.kt @@ -0,0 +1,53 @@ +package app.revanced.manager.network.dto + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class ReVancedInfo( + val name: String, + val about: String, + val branding: ReVancedBranding, + val contact: ReVancedContact, + val socials: List<ReVancedSocial>, + val donations: ReVancedDonation, +) + +@Serializable +data class ReVancedBranding( + val logo: String, +) + +@Serializable +data class ReVancedContact( + val email: String, +) + +@Serializable +data class ReVancedSocial( + val name: String, + val url: String, + val preferred: Boolean, +) + +@Serializable +data class ReVancedDonation( + val wallets: List<ReVancedWallet>, + val links: List<ReVancedDonationLink>, +) + +@Serializable +data class ReVancedWallet( + val network: String, + @SerialName("currency_code") + val currencyCode: String, + val address: String, + val preferred: Boolean +) + +@Serializable +data class ReVancedDonationLink( + val name: String, + val url: String, + val preferred: Boolean, +) diff --git a/app/src/main/java/app/revanced/manager/network/service/HttpService.kt b/app/src/main/java/app/revanced/manager/network/service/HttpService.kt new file mode 100644 index 0000000000..e0b69aa686 --- /dev/null +++ b/app/src/main/java/app/revanced/manager/network/service/HttpService.kt @@ -0,0 +1,101 @@ +package app.revanced.manager.network.service + +import android.util.Log +import app.revanced.manager.network.utils.APIError +import app.revanced.manager.network.utils.APIFailure +import app.revanced.manager.network.utils.APIResponse +import app.revanced.manager.util.tag +import io.ktor.client.HttpClient +import io.ktor.client.call.body +import io.ktor.client.request.HttpRequestBuilder +import io.ktor.client.request.get +import io.ktor.client.request.prepareGet +import io.ktor.client.request.request +import io.ktor.client.statement.bodyAsText +import io.ktor.http.HttpStatusCode +import io.ktor.http.isSuccess +import io.ktor.utils.io.ByteReadChannel +import io.ktor.utils.io.core.isNotEmpty +import io.ktor.utils.io.core.readBytes +import it.skrape.core.htmlDocument +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import kotlinx.serialization.json.Json +import java.io.File +import java.io.OutputStream + +/** + * @author Aliucord Authors, DiamondMiner88 + */ +class HttpService( + val json: Json, + val http: HttpClient, +) { + suspend inline fun <reified T> request(builder: HttpRequestBuilder.() -> Unit = {}): APIResponse<T> { + var body: String? = null + + val response = try { + val response = http.request(builder) + + if (response.status.isSuccess()) { + body = response.bodyAsText() + + if (T::class == String::class) { + return APIResponse.Success(body as T) + } + + APIResponse.Success(json.decodeFromString<T>(body)) + } else { + body = try { + response.bodyAsText() + } catch (t: Throwable) { + null + } + + Log.e( + tag, + "Failed to fetch: API error, http status: ${response.status}, body: $body" + ) + APIResponse.Error(APIError(response.status, body)) + } + } catch (t: Throwable) { + Log.e(tag, "Failed to fetch: error: $t, body: $body") + APIResponse.Failure(APIFailure(t, body)) + } + return response + } + + suspend fun streamTo( + outputStream: OutputStream, + builder: HttpRequestBuilder.() -> Unit + ) { + http.prepareGet(builder).execute { httpResponse -> + if (httpResponse.status.isSuccess()) { + val channel: ByteReadChannel = httpResponse.body() + withContext(Dispatchers.IO) { + while (!channel.isClosedForRead) { + val packet = channel.readRemaining(DEFAULT_BUFFER_SIZE.toLong()) + while (packet.isNotEmpty) { + val bytes = packet.readBytes() + outputStream.write(bytes) + } + } + } + + } else { + throw HttpException(httpResponse.status) + } + } + } + + suspend fun download( + saveLocation: File, + builder: HttpRequestBuilder.() -> Unit + ) = saveLocation.outputStream().use { streamTo(it, builder) } + + suspend fun getHtml(builder: HttpRequestBuilder.() -> Unit) = htmlDocument( + html = http.get(builder).bodyAsText() + ) + + class HttpException(status: HttpStatusCode) : Exception("Failed to fetch: http status: $status") +} \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/network/utils/APIResponse.kt b/app/src/main/java/app/revanced/manager/network/utils/APIResponse.kt new file mode 100644 index 0000000000..04765f888f --- /dev/null +++ b/app/src/main/java/app/revanced/manager/network/utils/APIResponse.kt @@ -0,0 +1,86 @@ +@file:Suppress("NOTHING_TO_INLINE") + +package app.revanced.manager.network.utils + +import io.ktor.http.* + +/** + * @author Aliucord Authors, DiamondMiner88 + */ + +sealed interface APIResponse<T> { + data class Success<T>(val data: T) : APIResponse<T> + data class Error<T>(val error: APIError) : APIResponse<T> + data class Failure<T>(val error: APIFailure) : APIResponse<T> +} + +class APIError(code: HttpStatusCode, body: String?) : Exception("HTTP Code $code, Body: $body") + +class APIFailure(error: Throwable, body: String?) : Exception(body ?: error.message, error) + +inline fun <T, R> APIResponse<T>.fold( + success: (T) -> R, + error: (APIError) -> R, + failure: (APIFailure) -> R +): R { + return when (this) { + is APIResponse.Success -> success(this.data) + is APIResponse.Error -> error(this.error) + is APIResponse.Failure -> failure(this.error) + } +} + +inline fun <T, R> APIResponse<T>.fold( + success: (T) -> R, + fail: (Exception) -> R, +): R { + return when (this) { + is APIResponse.Success -> success(data) + is APIResponse.Error -> fail(error) + is APIResponse.Failure -> fail(error) + } +} + +@Suppress("UNCHECKED_CAST") +inline fun <T, R> APIResponse<T>.transform(block: (T) -> R): APIResponse<R> { + return if (this !is APIResponse.Success) { + // Error and Failure do not use the generic value + this as APIResponse<R> + } else { + APIResponse.Success(block(data)) + } +} + +inline fun <T> APIResponse<T>.getOrThrow(): T { + return fold( + success = { it }, + fail = { throw it } + ) +} + +inline fun <T> APIResponse<T>.getOrNull(): T? { + return fold( + success = { it }, + fail = { null } + ) +} + +@Suppress("UNCHECKED_CAST") +inline fun <T, R> APIResponse<T>.chain(block: (T) -> APIResponse<R>): APIResponse<R> { + return if (this !is APIResponse.Success) { + // Error and Failure do not use the generic value + this as APIResponse<R> + } else { + block(data) + } +} + +@Suppress("UNCHECKED_CAST") +inline fun <T, R> APIResponse<T>.chain(secondary: APIResponse<R>): APIResponse<R> { + return if (secondary is APIResponse.Success) { + secondary + } else { + // Error and Failure do not use the generic value + this as APIResponse<R> + } +} \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/patcher/LibraryResolver.kt b/app/src/main/java/app/revanced/manager/patcher/LibraryResolver.kt new file mode 100644 index 0000000000..e0fe293f9a --- /dev/null +++ b/app/src/main/java/app/revanced/manager/patcher/LibraryResolver.kt @@ -0,0 +1,10 @@ +package app.revanced.manager.patcher + +import android.content.Context +import java.io.File + +abstract class LibraryResolver { + protected fun findLibrary(context: Context, searchTerm: String): File? = File(context.applicationInfo.nativeLibraryDir).run { + list { _, f -> !File(f).isDirectory && f.contains(searchTerm) }?.firstOrNull()?.let { resolve(it) } + } +} \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/patcher/Session.kt b/app/src/main/java/app/revanced/manager/patcher/Session.kt new file mode 100644 index 0000000000..dd5e7dc4b3 --- /dev/null +++ b/app/src/main/java/app/revanced/manager/patcher/Session.kt @@ -0,0 +1,136 @@ +package app.revanced.manager.patcher + +import android.content.Context +import app.revanced.library.ApkUtils.applyTo +import app.revanced.manager.R +import app.revanced.manager.patcher.logger.Logger +import app.revanced.manager.ui.model.State +import app.revanced.patcher.Patcher +import app.revanced.patcher.PatcherConfig +import app.revanced.patcher.patch.Patch +import app.revanced.patcher.patch.PatchResult +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import java.io.Closeable +import java.io.File +import java.nio.file.Files +import java.nio.file.StandardCopyOption + +internal typealias PatchList = List<Patch<*>> + +class Session( + cacheDir: String, + frameworkDir: String, + aaptPath: String, + private val androidContext: Context, + private val logger: Logger, + private val input: File, + private val onPatchCompleted: suspend () -> Unit, + private val onProgress: (name: String?, state: State?, message: String?) -> Unit +) : Closeable { + private fun updateProgress(name: String? = null, state: State? = null, message: String? = null) = + onProgress(name, state, message) + + private val tempDir = File(cacheDir).resolve("patcher").also { it.mkdirs() } + private val patcher = Patcher( + PatcherConfig( + apkFile = input, + temporaryFilesPath = tempDir, + frameworkFileDirectory = frameworkDir, + aaptBinaryPath = aaptPath + ) + ) + + private suspend fun Patcher.applyPatchesVerbose(selectedPatches: PatchList) { + var nextPatchIndex = 0 + + updateProgress( + name = androidContext.getString(R.string.executing_patch, selectedPatches[nextPatchIndex]), + state = State.RUNNING + ) + + this().collect { (patch, exception) -> + if (patch !in selectedPatches) return@collect + + if (exception != null) { + updateProgress( + name = androidContext.getString(R.string.failed_to_execute_patch, patch.name), + state = State.FAILED, + message = exception.stackTraceToString() + ) + + logger.error("${patch.name} failed:") + logger.error(exception.stackTraceToString()) + throw exception + } + + nextPatchIndex++ + + onPatchCompleted() + + selectedPatches.getOrNull(nextPatchIndex)?.let { nextPatch -> + updateProgress( + name = androidContext.getString(R.string.executing_patch, nextPatch.name) + ) + } + + logger.info("${patch.name} succeeded") + } + + updateProgress( + state = State.COMPLETED, + name = androidContext.resources.getQuantityString( + R.plurals.patches_executed, + selectedPatches.size, + selectedPatches.size + ) + ) + } + + suspend fun run(output: File, selectedPatches: PatchList) { + updateProgress(state = State.COMPLETED) // Unpacking + + java.util.logging.Logger.getLogger("").apply { + handlers.forEach { + it.close() + removeHandler(it) + } + + addHandler(logger.handler) + } + + with(patcher) { + logger.info("Merging integrations") + this += selectedPatches.toSet() + + logger.info("Applying patches...") + applyPatchesVerbose(selectedPatches.sortedBy { it.name }) + } + + logger.info("Writing patched files...") + val result = patcher.get() + + val patched = tempDir.resolve("result.apk") + withContext(Dispatchers.IO) { + Files.copy(input.toPath(), patched.toPath(), StandardCopyOption.REPLACE_EXISTING) + } + result.applyTo(patched) + + logger.info("Patched apk saved to $patched") + + withContext(Dispatchers.IO) { + Files.move(patched.toPath(), output.toPath(), StandardCopyOption.REPLACE_EXISTING) + } + updateProgress(state = State.COMPLETED) // Saving + } + + override fun close() { + tempDir.deleteRecursively() + patcher.close() + } + + companion object { + operator fun PatchResult.component1() = patch + operator fun PatchResult.component2() = exception + } +} \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/patcher/aapt/Aapt.kt b/app/src/main/java/app/revanced/manager/patcher/aapt/Aapt.kt new file mode 100644 index 0000000000..406c9e9d1f --- /dev/null +++ b/app/src/main/java/app/revanced/manager/patcher/aapt/Aapt.kt @@ -0,0 +1,12 @@ +package app.revanced.manager.patcher.aapt + +import android.content.Context +import app.revanced.manager.patcher.LibraryResolver +import android.os.Build.SUPPORTED_ABIS as DEVICE_ABIS +object Aapt : LibraryResolver() { + private val WORKING_ABIS = setOf("arm64-v8a", "x86", "x86_64", "armeabi-v7a") + + fun supportsDevice() = (DEVICE_ABIS intersect WORKING_ABIS).isNotEmpty() + + fun binary(context: Context) = findLibrary(context, "aapt") +} diff --git a/app/src/main/java/app/revanced/manager/patcher/logger/Logger.kt b/app/src/main/java/app/revanced/manager/patcher/logger/Logger.kt new file mode 100644 index 0000000000..88f2a1333a --- /dev/null +++ b/app/src/main/java/app/revanced/manager/patcher/logger/Logger.kt @@ -0,0 +1,37 @@ +package app.revanced.manager.patcher.logger + +import java.util.logging.Handler +import java.util.logging.Level +import java.util.logging.LogRecord + +abstract class Logger { + abstract fun log(level: LogLevel, message: String) + + fun trace(msg: String) = log(LogLevel.TRACE, msg) + fun info(msg: String) = log(LogLevel.INFO, msg) + fun warn(msg: String) = log(LogLevel.WARN, msg) + fun error(msg: String) = log(LogLevel.ERROR, msg) + + val handler = object : Handler() { + override fun publish(record: LogRecord) { + val msg = record.message + + when (record.level) { + Level.INFO -> info(msg) + Level.SEVERE -> error(msg) + Level.WARNING -> warn(msg) + else -> trace(msg) + } + } + + override fun flush() = Unit + override fun close() = Unit + } +} + +enum class LogLevel { + TRACE, + INFO, + WARN, + ERROR, +} \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/patcher/patch/PatchBundle.kt b/app/src/main/java/app/revanced/manager/patcher/patch/PatchBundle.kt new file mode 100644 index 0000000000..2b93a8294a --- /dev/null +++ b/app/src/main/java/app/revanced/manager/patcher/patch/PatchBundle.kt @@ -0,0 +1,56 @@ +package app.revanced.manager.patcher.patch + +import android.util.Log +import app.revanced.manager.util.tag +import app.revanced.patcher.patch.Patch +import app.revanced.patcher.patch.PatchLoader +import java.io.File +import java.io.IOException +import java.util.jar.JarFile + +class PatchBundle(val patchesJar: File) { + private val loader = object : Iterable<Patch<*>> { + private fun load(): Iterable<Patch<*>> { + patchesJar.setReadOnly() + return PatchLoader.Dex(setOf(patchesJar)) + } + + override fun iterator(): Iterator<Patch<*>> = load().iterator() + } + + init { + Log.d(tag, "Loaded patch bundle: $patchesJar") + } + + /** + * A list containing the metadata of every patch inside this bundle. + */ + val patches = loader.map(::PatchInfo) + + /** + * The [java.util.jar.Manifest] of [patchesJar]. + */ + private val manifest = try { + JarFile(patchesJar).use { it.manifest } + } catch (_: IOException) { + null + } + + fun readManifestAttribute(name: String) = manifest?.mainAttributes?.getValue(name) + + /** + * Load all patches compatible with the specified package. + */ + fun patches(packageName: String) = loader.filter { patch -> + val compatiblePackages = patch.compatiblePackages + ?: // The patch has no compatibility constraints, which means it is universal. + return@filter true + + if (!compatiblePackages.any { (name, _) -> name == packageName }) { + // Patch is not compatible with this package. + return@filter false + } + + true + } +} \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/patcher/patch/PatchInfo.kt b/app/src/main/java/app/revanced/manager/patcher/patch/PatchInfo.kt new file mode 100644 index 0000000000..2babc7f4e1 --- /dev/null +++ b/app/src/main/java/app/revanced/manager/patcher/patch/PatchInfo.kt @@ -0,0 +1,87 @@ +package app.revanced.manager.patcher.patch + +import androidx.compose.runtime.Immutable +import app.revanced.patcher.patch.Patch +import app.revanced.patcher.patch.Option as PatchOption +import app.revanced.patcher.patch.resourcePatch +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.ImmutableSet +import kotlinx.collections.immutable.toImmutableList +import kotlinx.collections.immutable.toImmutableSet +import kotlin.reflect.KType + +data class PatchInfo( + val name: String, + val description: String?, + val include: Boolean, + val compatiblePackages: ImmutableList<CompatiblePackage>?, + val options: ImmutableList<Option<*>>? +) { + constructor(patch: Patch<*>) : this( + patch.name.orEmpty(), + patch.description, + patch.use, + patch.compatiblePackages?.map { (pkgName, versions) -> + CompatiblePackage( + pkgName, + versions?.toImmutableSet() + ) + }?.toImmutableList(), + patch.options.map { (_, option) -> Option(option) }.ifEmpty { null }?.toImmutableList() + ) + + fun compatibleWith(packageName: String) = + compatiblePackages?.any { it.packageName == packageName } ?: true + + fun supports(packageName: String, versionName: String?): Boolean { + val packages = compatiblePackages ?: return true // Universal patch + + return packages.any { pkg -> + if (pkg.packageName != packageName) return@any false + if (pkg.versions == null) return@any true + + versionName != null && versionName in pkg.versions + } + } + + /** + * Create a fake [Patch] with the same metadata as the [PatchInfo] instance. + * The resulting patch cannot be executed. + * This is necessary because some functions in ReVanced Library only accept full [Patch] objects. + */ + fun toPatcherPatch(): Patch<*> = + resourcePatch(name = name, description = description, use = include) { + compatiblePackages?.let { pkgs -> + compatibleWith(*pkgs.map { it.packageName to it.versions }.toTypedArray()) + } + } +} + +@Immutable +data class CompatiblePackage( + val packageName: String, + val versions: ImmutableSet<String>? +) + +@Immutable +data class Option<T>( + val title: String, + val key: String, + val description: String, + val required: Boolean, + val type: KType, + val default: T?, + val presets: Map<String, T?>?, + val validator: (T?) -> Boolean, +) { + constructor(option: PatchOption<T>) : this( + option.title ?: option.key, + option.key, + option.description.orEmpty(), + option.required, + option.type, + option.default, + option.values, + { option.validator(option, it) }, + ) +} \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/patcher/runtime/CoroutineRuntime.kt b/app/src/main/java/app/revanced/manager/patcher/runtime/CoroutineRuntime.kt new file mode 100644 index 0000000000..eb50bd35b9 --- /dev/null +++ b/app/src/main/java/app/revanced/manager/patcher/runtime/CoroutineRuntime.kt @@ -0,0 +1,66 @@ +package app.revanced.manager.patcher.runtime + +import android.content.Context +import app.revanced.manager.patcher.Session +import app.revanced.manager.patcher.logger.Logger +import app.revanced.manager.patcher.worker.ProgressEventHandler +import app.revanced.manager.ui.model.State +import app.revanced.manager.util.Options +import app.revanced.manager.util.PatchSelection +import java.io.File + +/** + * Simple [Runtime] implementation that runs the patcher using coroutines. + */ +class CoroutineRuntime(private val context: Context) : Runtime(context) { + override suspend fun execute( + inputFile: String, + outputFile: String, + packageName: String, + selectedPatches: PatchSelection, + options: Options, + logger: Logger, + onPatchCompleted: suspend () -> Unit, + onProgress: ProgressEventHandler, + ) { + val bundles = bundles() + + val selectedBundles = selectedPatches.keys + val allPatches = bundles.filterKeys { selectedBundles.contains(it) } + .mapValues { (_, bundle) -> bundle.patches(packageName) } + + val patchList = selectedPatches.flatMap { (bundle, selected) -> + allPatches[bundle]?.filter { selected.contains(it.name) } + ?: throw IllegalArgumentException("Patch bundle $bundle does not exist") + } + + // Set all patch options. + options.forEach { (bundle, bundlePatchOptions) -> + val patches = allPatches[bundle] ?: return@forEach + bundlePatchOptions.forEach { (patchName, configuredPatchOptions) -> + val patchOptions = patches.single { it.name == patchName }.options + configuredPatchOptions.forEach { (key, value) -> + patchOptions[key] = value + } + } + } + + onProgress(null, State.COMPLETED, null) // Loading patches + + Session( + cacheDir, + frameworkPath, + aaptPath, + context, + logger, + File(inputFile), + onPatchCompleted = onPatchCompleted, + onProgress + ).use { session -> + session.run( + File(outputFile), + patchList + ) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/patcher/runtime/ProcessRuntime.kt b/app/src/main/java/app/revanced/manager/patcher/runtime/ProcessRuntime.kt new file mode 100644 index 0000000000..d7e9d342fe --- /dev/null +++ b/app/src/main/java/app/revanced/manager/patcher/runtime/ProcessRuntime.kt @@ -0,0 +1,188 @@ +package app.revanced.manager.patcher.runtime + +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.content.IntentFilter +import android.util.Log +import androidx.core.content.ContextCompat +import app.revanced.manager.BuildConfig +import app.revanced.manager.patcher.runtime.process.IPatcherEvents +import app.revanced.manager.patcher.runtime.process.IPatcherProcess +import app.revanced.manager.patcher.LibraryResolver +import app.revanced.manager.patcher.logger.Logger +import app.revanced.manager.patcher.runtime.process.Parameters +import app.revanced.manager.patcher.runtime.process.PatchConfiguration +import app.revanced.manager.patcher.runtime.process.PatcherProcess +import app.revanced.manager.patcher.worker.ProgressEventHandler +import app.revanced.manager.ui.model.State +import app.revanced.manager.util.Options +import app.revanced.manager.util.PM +import app.revanced.manager.util.PatchSelection +import app.revanced.manager.util.tag +import com.github.pgreze.process.Redirect +import com.github.pgreze.process.process +import kotlinx.coroutines.CompletableDeferred +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.launch +import kotlinx.coroutines.withTimeout +import org.koin.core.component.inject + +/** + * Runs the patcher in another process by using the app_process binary and IPC. + */ +class ProcessRuntime(private val context: Context) : Runtime(context) { + private val pm: PM by inject() + + private suspend fun awaitBinderConnection(): IPatcherProcess { + val binderFuture = CompletableDeferred<IPatcherProcess>() + val receiver = object : BroadcastReceiver() { + override fun onReceive(context: Context, intent: Intent) { + val binder = + intent.getBundleExtra(INTENT_BUNDLE_KEY)?.getBinder(BUNDLE_BINDER_KEY)!! + + binderFuture.complete(IPatcherProcess.Stub.asInterface(binder)) + } + } + + ContextCompat.registerReceiver(context, receiver, IntentFilter().apply { + addAction(CONNECT_TO_APP_ACTION) + }, ContextCompat.RECEIVER_NOT_EXPORTED) + + return try { + withTimeout(10000L) { + binderFuture.await() + } + } finally { + context.unregisterReceiver(receiver) + } + } + + override suspend fun execute( + inputFile: String, + outputFile: String, + packageName: String, + selectedPatches: PatchSelection, + options: Options, + logger: Logger, + onPatchCompleted: suspend () -> Unit, + onProgress: ProgressEventHandler, + ) = coroutineScope { + // Get the location of our own Apk. + val managerBaseApk = pm.getPackageInfo(context.packageName)!!.applicationInfo!!.sourceDir + + val limit = "${prefs.patcherProcessMemoryLimit.get()}M" + val propOverride = resolvePropOverride(context)?.absolutePath + ?: throw Exception("Couldn't find prop override library") + + val env = + System.getenv().toMutableMap().apply { + putAll( + mapOf( + "CLASSPATH" to managerBaseApk, + // Override the props used by ART to set the memory limit. + "LD_PRELOAD" to propOverride, + "PROP_dalvik.vm.heapgrowthlimit" to limit, + "PROP_dalvik.vm.heapsize" to limit, + ) + ) + } + + launch(Dispatchers.IO) { + val result = process( + APP_PROCESS_BIN_PATH, + "-Djava.io.tmpdir=$cacheDir", // The process will use /tmp if this isn't set, which is a problem because that folder is not accessible on Android. + "/", // The unused cmd-dir parameter + "--nice-name=${context.packageName}:Patcher", + PatcherProcess::class.java.name, // The class with the main function. + context.packageName, + env = env, + stdout = Redirect.CAPTURE, + stderr = Redirect.CAPTURE, + ) { line -> + // The process shouldn't generally be writing to stdio. Log any lines we get as warnings. + logger.warn("[STDIO]: $line") + } + + Log.d(tag, "Process finished with exit code ${result.resultCode}") + + if (result.resultCode != 0) throw Exception("Process exited with nonzero exit code ${result.resultCode}") + } + + val patching = CompletableDeferred<Unit>() + + launch(Dispatchers.IO) { + val binder = awaitBinderConnection() + + // Android Studio's fast deployment feature causes an issue where the other process will be running older code compared to the main process. + // The patcher process is running outdated code if the randomly generated BUILD_ID numbers don't match. + // To fix it, clear the cache in the Android settings or disable fast deployment (Run configurations -> Edit Configurations -> app -> Enable "always deploy with package manager"). + if (binder.buildId() != BuildConfig.BUILD_ID) throw Exception("app_process is running outdated code. Clear the app cache or disable disable Android 11 deployment optimizations in your IDE") + + val eventHandler = object : IPatcherEvents.Stub() { + override fun log(level: String, msg: String) = logger.log(enumValueOf(level), msg) + + override fun patchSucceeded() { + launch { onPatchCompleted() } + } + + override fun progress(name: String?, state: String?, msg: String?) = + onProgress(name, state?.let { enumValueOf<State>(it) }, msg) + + override fun finished(exceptionStackTrace: String?) { + binder.exit() + + exceptionStackTrace?.let { + patching.completeExceptionally(RemoteFailureException(it)) + return + } + patching.complete(Unit) + } + } + + val bundles = bundles() + + val parameters = Parameters( + aaptPath = aaptPath, + frameworkDir = frameworkPath, + cacheDir = cacheDir, + packageName = packageName, + inputFile = inputFile, + outputFile = outputFile, + configurations = selectedPatches.map { (id, patches) -> + val bundle = bundles[id]!! + + PatchConfiguration( + bundle.patchesJar.absolutePath, + patches, + options[id].orEmpty() + ) + } + ) + + binder.start(parameters, eventHandler) + } + + // Wait until patching finishes. + patching.await() + } + + companion object : LibraryResolver() { + private const val APP_PROCESS_BIN_PATH = "/system/bin/app_process" + + const val CONNECT_TO_APP_ACTION = "CONNECT_TO_APP_ACTION" + const val INTENT_BUNDLE_KEY = "BUNDLE" + const val BUNDLE_BINDER_KEY = "BINDER" + + private fun resolvePropOverride(context: Context) = findLibrary(context, "prop_override") + } + + /** + * An [Exception] occured in the remote process while patching. + * + * @param originalStackTrace The stack trace of the original [Exception]. + */ + class RemoteFailureException(val originalStackTrace: String) : Exception() +} + diff --git a/app/src/main/java/app/revanced/manager/patcher/runtime/Runtime.kt b/app/src/main/java/app/revanced/manager/patcher/runtime/Runtime.kt new file mode 100644 index 0000000000..7f4616bcd5 --- /dev/null +++ b/app/src/main/java/app/revanced/manager/patcher/runtime/Runtime.kt @@ -0,0 +1,40 @@ +package app.revanced.manager.patcher.runtime + +import android.content.Context +import app.revanced.manager.data.platform.Filesystem +import app.revanced.manager.domain.manager.PreferencesManager +import app.revanced.manager.domain.repository.PatchBundleRepository +import app.revanced.manager.patcher.aapt.Aapt +import app.revanced.manager.patcher.logger.Logger +import app.revanced.manager.patcher.worker.ProgressEventHandler +import app.revanced.manager.util.Options +import app.revanced.manager.util.PatchSelection +import kotlinx.coroutines.flow.first +import org.koin.core.component.KoinComponent +import org.koin.core.component.inject +import java.io.FileNotFoundException + +sealed class Runtime(context: Context) : KoinComponent { + private val fs: Filesystem by inject() + private val patchBundlesRepo: PatchBundleRepository by inject() + protected val prefs: PreferencesManager by inject() + + protected val cacheDir: String = fs.tempDir.absolutePath + protected val aaptPath = Aapt.binary(context)?.absolutePath + ?: throw FileNotFoundException("Could not resolve aapt.") + protected val frameworkPath: String = + context.cacheDir.resolve("framework").also { it.mkdirs() }.absolutePath + + protected suspend fun bundles() = patchBundlesRepo.bundles.first() + + abstract suspend fun execute( + inputFile: String, + outputFile: String, + packageName: String, + selectedPatches: PatchSelection, + options: Options, + logger: Logger, + onPatchCompleted: suspend () -> Unit, + onProgress: ProgressEventHandler, + ) +} \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/patcher/runtime/process/Parameters.kt b/app/src/main/java/app/revanced/manager/patcher/runtime/process/Parameters.kt new file mode 100644 index 0000000000..b00d558a98 --- /dev/null +++ b/app/src/main/java/app/revanced/manager/patcher/runtime/process/Parameters.kt @@ -0,0 +1,23 @@ +package app.revanced.manager.patcher.runtime.process + +import android.os.Parcelable +import kotlinx.parcelize.Parcelize +import kotlinx.parcelize.RawValue + +@Parcelize +data class Parameters( + val cacheDir: String, + val aaptPath: String, + val frameworkDir: String, + val packageName: String, + val inputFile: String, + val outputFile: String, + val configurations: List<PatchConfiguration>, +) : Parcelable + +@Parcelize +data class PatchConfiguration( + val bundlePath: String, + val patches: Set<String>, + val options: @RawValue Map<String, Map<String, Any?>> +) : Parcelable \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/patcher/runtime/process/PatcherProcess.kt b/app/src/main/java/app/revanced/manager/patcher/runtime/process/PatcherProcess.kt new file mode 100644 index 0000000000..b0f8e248a9 --- /dev/null +++ b/app/src/main/java/app/revanced/manager/patcher/runtime/process/PatcherProcess.kt @@ -0,0 +1,123 @@ +package app.revanced.manager.patcher.runtime.process + +import android.app.ActivityThread +import android.content.Context +import android.content.Intent +import android.os.Bundle +import android.os.Looper +import app.revanced.manager.BuildConfig +import app.revanced.manager.patcher.Session +import app.revanced.manager.patcher.logger.LogLevel +import app.revanced.manager.patcher.logger.Logger +import app.revanced.manager.patcher.patch.PatchBundle +import app.revanced.manager.patcher.runtime.ProcessRuntime +import app.revanced.manager.ui.model.State +import kotlinx.coroutines.CoroutineExceptionHandler +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import java.io.File +import kotlin.system.exitProcess + +/** + * The main class that runs inside the runner process launched by [ProcessRuntime]. + */ +class PatcherProcess(private val context: Context) : IPatcherProcess.Stub() { + private var eventBinder: IPatcherEvents? = null + + private val scope = + CoroutineScope(Dispatchers.Default + CoroutineExceptionHandler { _, throwable -> + // Try to send the exception information to the main app. + eventBinder?.let { + try { + it.finished(throwable.stackTraceToString()) + return@CoroutineExceptionHandler + } catch (_: Exception) { + } + } + + throwable.printStackTrace() + exitProcess(1) + }) + + override fun buildId() = BuildConfig.BUILD_ID + override fun exit() = exitProcess(0) + + override fun start(parameters: Parameters, events: IPatcherEvents) { + eventBinder = events + + scope.launch { + val logger = object : Logger() { + override fun log(level: LogLevel, message: String) = + events.log(level.name, message) + } + + logger.info("Memory limit: ${Runtime.getRuntime().maxMemory() / (1024 * 1024)}MB") + + val patchList = parameters.configurations.flatMap { config -> + val bundle = PatchBundle(File(config.bundlePath)) + + val patches = + bundle.patches(parameters.packageName).filter { it.name in config.patches } + .associateBy { it.name } + + config.options.forEach { (patchName, opts) -> + val patchOptions = patches[patchName]?.options + ?: throw Exception("Patch with name $patchName does not exist.") + + opts.forEach { (key, value) -> + patchOptions[key] = value + } + } + + patches.values + } + + events.progress(null, State.COMPLETED.name, null) // Loading patches + + Session( + cacheDir = parameters.cacheDir, + aaptPath = parameters.aaptPath, + frameworkDir = parameters.frameworkDir, + androidContext = context, + logger = logger, + input = File(parameters.inputFile), + onPatchCompleted = { events.patchSucceeded() }, + onProgress = { name, state, message -> + events.progress(name, state?.name, message) + } + ).use { + it.run(File(parameters.outputFile), patchList) + } + + events.finished(null) + } + } + + companion object { + @JvmStatic + fun main(args: Array<String>) { + Looper.prepare() + + val managerPackageName = args[0] + + // Abuse hidden APIs to get a context. + val systemContext = ActivityThread.systemMain().systemContext as Context + val appContext = systemContext.createPackageContext(managerPackageName, 0) + + val ipcInterface = PatcherProcess(appContext) + + appContext.sendBroadcast(Intent().apply { + action = ProcessRuntime.CONNECT_TO_APP_ACTION + `package` = managerPackageName + + putExtra(ProcessRuntime.INTENT_BUNDLE_KEY, Bundle().apply { + putBinder(ProcessRuntime.BUNDLE_BINDER_KEY, ipcInterface.asBinder()) + }) + }) + + Looper.loop() + exitProcess(1) // Shouldn't happen + } + } +} \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/patcher/worker/PatcherWorker.kt b/app/src/main/java/app/revanced/manager/patcher/worker/PatcherWorker.kt new file mode 100644 index 0000000000..5096170caa --- /dev/null +++ b/app/src/main/java/app/revanced/manager/patcher/worker/PatcherWorker.kt @@ -0,0 +1,257 @@ +package app.revanced.manager.patcher.worker + +import android.app.Activity +import android.app.Notification +import android.app.NotificationChannel +import android.app.NotificationManager +import android.app.PendingIntent +import android.content.Context +import android.content.Intent +import android.content.pm.ServiceInfo +import android.graphics.drawable.Icon +import android.os.Build +import android.os.Parcelable +import android.os.PowerManager +import android.util.Log +import androidx.activity.result.ActivityResult +import androidx.core.content.ContextCompat +import androidx.work.ForegroundInfo +import androidx.work.WorkerParameters +import app.revanced.manager.R +import app.revanced.manager.data.platform.Filesystem +import app.revanced.manager.data.room.apps.installed.InstallType +import app.revanced.manager.domain.installer.RootInstaller +import app.revanced.manager.domain.manager.KeystoreManager +import app.revanced.manager.domain.manager.PreferencesManager +import app.revanced.manager.domain.repository.DownloadedAppRepository +import app.revanced.manager.domain.repository.DownloaderPluginRepository +import app.revanced.manager.domain.repository.InstalledAppRepository +import app.revanced.manager.domain.worker.Worker +import app.revanced.manager.domain.worker.WorkerRepository +import app.revanced.manager.network.downloader.LoadedDownloaderPlugin +import app.revanced.manager.patcher.logger.Logger +import app.revanced.manager.patcher.runtime.CoroutineRuntime +import app.revanced.manager.patcher.runtime.ProcessRuntime +import app.revanced.manager.plugin.downloader.GetScope +import app.revanced.manager.plugin.downloader.PluginHostApi +import app.revanced.manager.plugin.downloader.UserInteractionException +import app.revanced.manager.ui.model.SelectedApp +import app.revanced.manager.ui.model.State +import app.revanced.manager.util.Options +import app.revanced.manager.util.PM +import app.revanced.manager.util.PatchSelection +import app.revanced.manager.util.tag +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.withContext +import org.koin.core.component.KoinComponent +import org.koin.core.component.inject +import java.io.File + +typealias ProgressEventHandler = (name: String?, state: State?, message: String?) -> Unit + +@OptIn(PluginHostApi::class) +class PatcherWorker( + context: Context, + parameters: WorkerParameters +) : Worker<PatcherWorker.Args>(context, parameters), KoinComponent { + private val workerRepository: WorkerRepository by inject() + private val prefs: PreferencesManager by inject() + private val keystoreManager: KeystoreManager by inject() + private val downloaderPluginRepository: DownloaderPluginRepository by inject() + private val downloadedAppRepository: DownloadedAppRepository by inject() + private val pm: PM by inject() + private val fs: Filesystem by inject() + private val installedAppRepository: InstalledAppRepository by inject() + private val rootInstaller: RootInstaller by inject() + + class Args( + val input: SelectedApp, + val output: String, + val selectedPatches: PatchSelection, + val options: Options, + val logger: Logger, + val onDownloadProgress: suspend (Pair<Long, Long?>?) -> Unit, + val onPatchCompleted: suspend () -> Unit, + val handleStartActivityRequest: suspend (LoadedDownloaderPlugin, Intent) -> ActivityResult, + val setInputFile: suspend (File) -> Unit, + val onProgress: ProgressEventHandler + ) { + val packageName get() = input.packageName + } + + override suspend fun getForegroundInfo() = + ForegroundInfo( + 1, + createNotification(), + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) ServiceInfo.FOREGROUND_SERVICE_TYPE_SPECIAL_USE else 0 + ) + + private fun createNotification(): Notification { + val notificationIntent = Intent(applicationContext, PatcherWorker::class.java) + val pendingIntent: PendingIntent = PendingIntent.getActivity( + applicationContext, 0, notificationIntent, PendingIntent.FLAG_IMMUTABLE + ) + val channel = NotificationChannel( + "revanced-patcher-patching", "Patching", NotificationManager.IMPORTANCE_HIGH + ) + val notificationManager = + ContextCompat.getSystemService(applicationContext, NotificationManager::class.java) + notificationManager!!.createNotificationChannel(channel) + return Notification.Builder(applicationContext, channel.id) + .setContentTitle(applicationContext.getText(R.string.app_name)) + .setContentText(applicationContext.getText(R.string.patcher_notification_message)) + .setLargeIcon(Icon.createWithResource(applicationContext, R.drawable.ic_notification)) + .setSmallIcon(Icon.createWithResource(applicationContext, R.drawable.ic_notification)) + .setContentIntent(pendingIntent).build() + } + + override suspend fun doWork(): Result { + if (runAttemptCount > 0) { + Log.d(tag, "Android requested retrying but retrying is disabled.".logFmt()) + return Result.failure() + } + + try { + // This does not always show up for some reason. + setForeground(getForegroundInfo()) + } catch (e: Exception) { + Log.d(tag, "Failed to set foreground info:", e) + } + + val wakeLock: PowerManager.WakeLock = + (applicationContext.getSystemService(Context.POWER_SERVICE) as PowerManager) + .newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "$tag::Patcher") + .apply { + acquire(10 * 60 * 1000L) + Log.d(tag, "Acquired wakelock.") + } + + val args = workerRepository.claimInput(this) + + return try { + runPatcher(args) + } finally { + wakeLock.release() + } + } + + private suspend fun runPatcher(args: Args): Result { + + fun updateProgress(name: String? = null, state: State? = null, message: String? = null) = + args.onProgress(name, state, message) + + val patchedApk = fs.tempDir.resolve("patched.apk") + + return try { + if (args.input is SelectedApp.Installed) { + installedAppRepository.get(args.packageName)?.let { + if (it.installType == InstallType.MOUNT) { + rootInstaller.unmount(args.packageName) + } + } + } + + suspend fun download(plugin: LoadedDownloaderPlugin, data: Parcelable) = + downloadedAppRepository.download( + plugin, + data, + args.packageName, + args.input.version, + onDownload = args.onDownloadProgress + ).also { + args.setInputFile(it) + updateProgress(state = State.COMPLETED) // Download APK + } + + val inputFile = when (val selectedApp = args.input) { + is SelectedApp.Download -> { + val (plugin, data) = downloaderPluginRepository.unwrapParceledData(selectedApp.data) + + download(plugin, data) + } + + is SelectedApp.Search -> { + downloaderPluginRepository.loadedPluginsFlow.first() + .firstNotNullOfOrNull { plugin -> + try { + val getScope = object : GetScope { + override val pluginPackageName = plugin.packageName + override val hostPackageName = applicationContext.packageName + override suspend fun requestStartActivity(intent: Intent): Intent? { + val result = args.handleStartActivityRequest(plugin, intent) + return when (result.resultCode) { + Activity.RESULT_OK -> result.data + Activity.RESULT_CANCELED -> throw UserInteractionException.Activity.Cancelled() + else -> throw UserInteractionException.Activity.NotCompleted( + result.resultCode, + result.data + ) + } + } + } + withContext(Dispatchers.IO) { + plugin.get( + getScope, + selectedApp.packageName, + selectedApp.version + ) + }?.takeIf { (_, version) -> selectedApp.version == null || version == selectedApp.version } + } catch (e: UserInteractionException.Activity.NotCompleted) { + throw e + } catch (_: UserInteractionException) { + null + }?.let { (data, _) -> download(plugin, data) } + } ?: throw Exception("App is not available.") + } + + is SelectedApp.Local -> selectedApp.file.also { args.setInputFile(it) } + is SelectedApp.Installed -> File(pm.getPackageInfo(selectedApp.packageName)!!.applicationInfo!!.sourceDir) + } + + val runtime = if (prefs.useProcessRuntime.get()) { + ProcessRuntime(applicationContext) + } else { + CoroutineRuntime(applicationContext) + } + + runtime.execute( + inputFile.absolutePath, + patchedApk.absolutePath, + args.packageName, + args.selectedPatches, + args.options, + args.logger, + args.onPatchCompleted, + args.onProgress + ) + + keystoreManager.sign(patchedApk, File(args.output)) + updateProgress(state = State.COMPLETED) // Signing + + Log.i(tag, "Patching succeeded".logFmt()) + Result.success() + } catch (e: ProcessRuntime.RemoteFailureException) { + Log.e( + tag, + "An exception occurred in the remote process while patching. ${e.originalStackTrace}".logFmt() + ) + updateProgress(state = State.FAILED, message = e.originalStackTrace) + Result.failure() + } catch (e: Exception) { + Log.e(tag, "An exception occurred while patching".logFmt(), e) + updateProgress(state = State.FAILED, message = e.stackTraceToString()) + Result.failure() + } finally { + patchedApk.delete() + if (args.input is SelectedApp.Local && args.input.temporary) { + args.input.file.delete() + } + } + } + + companion object { + private const val LOG_PREFIX = "[Worker]" + private fun String.logFmt() = "$LOG_PREFIX $this" + } +} \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/service/InstallService.kt b/app/src/main/java/app/revanced/manager/service/InstallService.kt new file mode 100644 index 0000000000..7bf2d2131a --- /dev/null +++ b/app/src/main/java/app/revanced/manager/service/InstallService.kt @@ -0,0 +1,53 @@ +package app.revanced.manager.service + +import android.app.Service +import android.content.Intent +import android.content.pm.PackageInstaller +import android.os.Build +import android.os.IBinder + +@Suppress("DEPRECATION") +class InstallService : Service() { + + override fun onStartCommand( + intent: Intent, flags: Int, startId: Int + ): Int { + val extraStatus = intent.getIntExtra(PackageInstaller.EXTRA_STATUS, -999) + val extraStatusMessage = intent.getStringExtra(PackageInstaller.EXTRA_STATUS_MESSAGE) + val extraPackageName = intent.getStringExtra(PackageInstaller.EXTRA_PACKAGE_NAME) + when (extraStatus) { + PackageInstaller.STATUS_PENDING_USER_ACTION -> { + startActivity(if (Build.VERSION.SDK_INT >= 33) { + intent.getParcelableExtra(Intent.EXTRA_INTENT, Intent::class.java) + } else { + intent.getParcelableExtra(Intent.EXTRA_INTENT) + }.apply { + this?.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + }) + } + + else -> { + sendBroadcast(Intent().apply { + action = APP_INSTALL_ACTION + `package` = packageName + putExtra(EXTRA_INSTALL_STATUS, extraStatus) + putExtra(EXTRA_INSTALL_STATUS_MESSAGE, extraStatusMessage) + putExtra(EXTRA_PACKAGE_NAME, extraPackageName) + }) + } + } + stopSelf() + return START_NOT_STICKY + } + + override fun onBind(intent: Intent?): IBinder? = null + + companion object { + const val APP_INSTALL_ACTION = "APP_INSTALL_ACTION" + + const val EXTRA_INSTALL_STATUS = "EXTRA_INSTALL_STATUS" + const val EXTRA_INSTALL_STATUS_MESSAGE = "EXTRA_INSTALL_STATUS_MESSAGE" + const val EXTRA_PACKAGE_NAME = "EXTRA_PACKAGE_NAME" + } + +} \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/service/RootService.kt b/app/src/main/java/app/revanced/manager/service/RootService.kt new file mode 100644 index 0000000000..ed475e50fc --- /dev/null +++ b/app/src/main/java/app/revanced/manager/service/RootService.kt @@ -0,0 +1,16 @@ +package app.revanced.manager.service + +import android.content.Intent +import android.os.IBinder +import app.revanced.manager.IRootSystemService +import com.topjohnwu.superuser.ipc.RootService +import com.topjohnwu.superuser.nio.FileSystemManager + +class ManagerRootService : RootService() { + class RootSystemService : IRootSystemService.Stub() { + override fun getFileSystemService() = + FileSystemManager.getService() + } + + override fun onBind(intent: Intent): IBinder = RootSystemService() +} \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/service/UninstallService.kt b/app/src/main/java/app/revanced/manager/service/UninstallService.kt new file mode 100644 index 0000000000..6bb4d4fdfe --- /dev/null +++ b/app/src/main/java/app/revanced/manager/service/UninstallService.kt @@ -0,0 +1,53 @@ +package app.revanced.manager.service + +import android.app.Service +import android.content.Intent +import android.content.pm.PackageInstaller +import android.os.Build +import android.os.IBinder + +@Suppress("DEPRECATION") +class UninstallService : Service() { + + override fun onStartCommand( + intent: Intent, + flags: Int, + startId: Int + ): Int { + val extraStatus = intent.getIntExtra(PackageInstaller.EXTRA_STATUS, -999) + val extraStatusMessage = intent.getStringExtra(PackageInstaller.EXTRA_STATUS_MESSAGE) + + when (extraStatus) { + PackageInstaller.STATUS_PENDING_USER_ACTION -> { + startActivity(if (Build.VERSION.SDK_INT >= 33) { + intent.getParcelableExtra(Intent.EXTRA_INTENT, Intent::class.java) + } else { + intent.getParcelableExtra(Intent.EXTRA_INTENT) + }.apply { + this?.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + }) + } + + else -> { + sendBroadcast(Intent().apply { + action = APP_UNINSTALL_ACTION + `package` = packageName + putExtra(EXTRA_UNINSTALL_STATUS, extraStatus) + putExtra(EXTRA_UNINSTALL_STATUS_MESSAGE, extraStatusMessage) + }) + } + } + stopSelf() + return START_NOT_STICKY + } + + override fun onBind(intent: Intent?): IBinder? = null + + companion object { + const val APP_UNINSTALL_ACTION = "APP_UNINSTALL_ACTION" + + const val EXTRA_UNINSTALL_STATUS = "EXTRA_UNINSTALL_STATUS" + const val EXTRA_UNINSTALL_STATUS_MESSAGE = "EXTRA_INSTALL_STATUS_MESSAGE" + } + +} \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/ui/component/AlertDialogExtended.kt b/app/src/main/java/app/revanced/manager/ui/component/AlertDialogExtended.kt new file mode 100644 index 0000000000..c2089d5811 --- /dev/null +++ b/app/src/main/java/app/revanced/manager/ui/component/AlertDialogExtended.kt @@ -0,0 +1,152 @@ +package app.revanced.manager.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.ExperimentalLayoutApi +import androidx.compose.foundation.layout.FlowRow +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.AlertDialogDefaults +import androidx.compose.material3.BasicAlertDialog +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.LocalContentColor +import androidx.compose.material3.LocalTextStyle +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.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.Shape +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp + +@OptIn(ExperimentalLayoutApi::class, ExperimentalMaterial3Api::class) +@Composable +fun AlertDialogExtended( + modifier: Modifier = Modifier, + onDismissRequest: () -> Unit, + confirmButton: @Composable () -> Unit, + dismissButton: @Composable (() -> Unit)? = null, + tertiaryButton: @Composable (() -> Unit)? = null, + icon: @Composable (() -> Unit)? = null, + title: @Composable (() -> Unit)? = null, + text: @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, + textHorizontalPadding: PaddingValues = TextHorizontalPadding +) { + BasicAlertDialog(onDismissRequest = onDismissRequest) { + Surface( + modifier = modifier, + shape = shape, + color = containerColor, + tonalElevation = tonalElevation, + ) { + Column(modifier = Modifier.padding(vertical = 24.dp)) { + Column( + modifier = Modifier.padding(horizontal = 24.dp).fillMaxWidth() + ) { + icon?.let { + ContentStyle(color = iconContentColor) { + Box( + Modifier + .padding(bottom = 16.dp) + .align(Alignment.CenterHorizontally) + ) { + icon() + } + } + } + title?.let { + ContentStyle( + color = titleContentColor, + textStyle = MaterialTheme.typography.headlineSmall + ) { + Box( + // Align the title to the center when an icon is present. + Modifier + .padding(bottom = 16.dp) + .align( + if (icon == null) { + Alignment.Start + } else { + Alignment.CenterHorizontally + } + ) + ) { + title() + } + } + } + } + text?.let { + ContentStyle( + color = textContentColor, + textStyle = MaterialTheme.typography.bodyMedium + ) { + Box( + Modifier + .weight(weight = 1f, fill = false) + .padding(bottom = 24.dp) + .padding(textHorizontalPadding) + .align(Alignment.Start) + ) { + text() + } + } + } + Box( + modifier = Modifier + .padding(horizontal = 24.dp) + ) { + ContentStyle( + color = MaterialTheme.colorScheme.primary, + textStyle = MaterialTheme.typography.labelLarge + ) { + FlowRow( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy( + 12.dp, + if (tertiaryButton != null) Alignment.Start else Alignment.End + ), + verticalArrangement = Arrangement.spacedBy(8.dp) + ) { + tertiaryButton?.let { + it() + Spacer(modifier = Modifier.weight(1f)) + } + dismissButton?.invoke() + confirmButton() + } + } + } + } + } + } +} + +@Composable +private fun ContentStyle( + color: Color = LocalContentColor.current, + textStyle: TextStyle = LocalTextStyle.current, + content: @Composable () -> Unit +) { + CompositionLocalProvider(LocalContentColor provides color) { + ProvideTextStyle(textStyle) { + content() + } + } +} + +val TextHorizontalPadding = PaddingValues(horizontal = 24.dp) \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/ui/component/AppIcon.kt b/app/src/main/java/app/revanced/manager/ui/component/AppIcon.kt new file mode 100644 index 0000000000..0d8cd822a0 --- /dev/null +++ b/app/src/main/java/app/revanced/manager/ui/component/AppIcon.kt @@ -0,0 +1,47 @@ +package app.revanced.manager.ui.component + +import android.content.pm.PackageInfo +import androidx.compose.foundation.Image +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Android +import androidx.compose.material3.LocalContentColor +import androidx.compose.material3.MaterialTheme +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.Modifier +import androidx.compose.ui.graphics.ColorFilter +import androidx.compose.ui.graphics.vector.rememberVectorPainter +import coil.compose.AsyncImage +import io.github.fornewid.placeholder.material3.placeholder + +@Composable +fun AppIcon( + packageInfo: PackageInfo?, + contentDescription: String?, + modifier: Modifier = Modifier +) { + var showPlaceHolder by rememberSaveable { mutableStateOf(true) } + + if (packageInfo == null) { + val image = rememberVectorPainter(Icons.Default.Android) + val colorFilter = ColorFilter.tint(LocalContentColor.current) + + Image( + image, + contentDescription, + modifier, + colorFilter = colorFilter + ) + } else { + AsyncImage( + packageInfo, + contentDescription, + Modifier.placeholder(visible = showPlaceHolder, color = MaterialTheme.colorScheme.inverseOnSurface, shape = RoundedCornerShape(100)).then(modifier), + onSuccess = { showPlaceHolder = false } + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/ui/component/AppInfo.kt b/app/src/main/java/app/revanced/manager/ui/component/AppInfo.kt new file mode 100644 index 0000000000..6d45b20bee --- /dev/null +++ b/app/src/main/java/app/revanced/manager/ui/component/AppInfo.kt @@ -0,0 +1,39 @@ +package app.revanced.manager.ui.component + +import android.content.pm.PackageInfo +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp + +@Composable +fun AppInfo(appInfo: PackageInfo?, placeholderLabel: String? = null, extraContent: @Composable () -> Unit = {}) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 24.dp, vertical = 16.dp), + horizontalAlignment = Alignment.CenterHorizontally + ) { + AppIcon( + appInfo, + contentDescription = null, + modifier = Modifier + .size(100.dp) + .padding(bottom = 5.dp) + ) + + AppLabel( + appInfo, + modifier = Modifier.padding(top = 16.dp), + style = MaterialTheme.typography.titleLarge, + defaultText = placeholderLabel + ) + + extraContent() + } +} \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/ui/component/AppLabel.kt b/app/src/main/java/app/revanced/manager/ui/component/AppLabel.kt new file mode 100644 index 0000000000..33a1e20152 --- /dev/null +++ b/app/src/main/java/app/revanced/manager/ui/component/AppLabel.kt @@ -0,0 +1,52 @@ +package app.revanced.manager.ui.component + +import android.content.pm.PackageInfo +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.LocalTextStyle +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.TextStyle +import app.revanced.manager.R +import io.github.fornewid.placeholder.material3.placeholder +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext + +@Composable +fun AppLabel( + packageInfo: PackageInfo?, + modifier: Modifier = Modifier, + style: TextStyle = LocalTextStyle.current, + defaultText: String? = stringResource(R.string.not_installed) +) { + val context = LocalContext.current + + var label: String? by rememberSaveable { mutableStateOf(null) } + + LaunchedEffect(packageInfo) { + label = withContext(Dispatchers.IO) { + packageInfo?.applicationInfo?.loadLabel(context.packageManager)?.toString() + ?: defaultText + } + } + + Text( + label ?: stringResource(R.string.loading), + modifier = Modifier + .placeholder( + visible = label == null, + color = MaterialTheme.colorScheme.inverseOnSurface, + shape = RoundedCornerShape(100) + ) + .then(modifier), + style = style + ) +} \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/ui/component/AppScaffold.kt b/app/src/main/java/app/revanced/manager/ui/component/AppScaffold.kt new file mode 100644 index 0000000000..d63decf4f0 --- /dev/null +++ b/app/src/main/java/app/revanced/manager/ui/component/AppScaffold.kt @@ -0,0 +1,83 @@ +package app.revanced.manager.ui.component + +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.RowScope +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.ArrowBack +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar +import androidx.compose.material3.TopAppBarDefaults +import androidx.compose.material3.TopAppBarScrollBehavior +import androidx.compose.material3.rememberTopAppBarState +import androidx.compose.material3.surfaceColorAtElevation +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.input.nestedscroll.nestedScroll +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import app.revanced.manager.R + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun AppScaffold( + topBar: @Composable (TopAppBarScrollBehavior) -> Unit = {}, + bottomBar: @Composable () -> Unit = {}, + floatingActionButton: @Composable () -> Unit = {}, + content: @Composable (PaddingValues) -> Unit +) { + val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState()) + + Scaffold( + modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection), + topBar = { topBar(scrollBehavior) }, + bottomBar = bottomBar, + floatingActionButton = floatingActionButton, + content = content + ) +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun AppTopBar( + title: String, + onBackClick: (() -> Unit)? = null, + backIcon: @Composable (() -> Unit) = @Composable { + Icon( + imageVector = Icons.AutoMirrored.Filled.ArrowBack, contentDescription = stringResource( + R.string.back + ) + ) + }, + actions: @Composable (RowScope.() -> Unit) = {}, + scrollBehavior: TopAppBarScrollBehavior? = null, + applyContainerColor: Boolean = false +) { + val containerColor = if (applyContainerColor) { + MaterialTheme.colorScheme.surfaceColorAtElevation(3.0.dp) + } else { + Color.Unspecified + } + + TopAppBar( + title = { Text(title) }, + scrollBehavior = scrollBehavior, + navigationIcon = { + if (onBackClick != null) { + IconButton(onClick = onBackClick) { + backIcon() + } + } + }, + actions = actions, + colors = TopAppBarDefaults.topAppBarColors( + containerColor = containerColor + ) + ) +} + diff --git a/app/src/main/java/app/revanced/manager/ui/component/ArrowButton.kt b/app/src/main/java/app/revanced/manager/ui/component/ArrowButton.kt new file mode 100644 index 0000000000..aed7e0c7db --- /dev/null +++ b/app/src/main/java/app/revanced/manager/ui/component/ArrowButton.kt @@ -0,0 +1,46 @@ +package app.revanced.manager.ui.component + +import androidx.compose.animation.core.animateFloatAsState +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.KeyboardArrowUp +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.rotate +import androidx.compose.ui.res.stringResource +import app.revanced.manager.R + +@Composable +fun ArrowButton( + modifier: Modifier = Modifier, + expanded: Boolean, + onClick: (() -> Unit)?, + rotationInitial: Float = 0f, + rotationFinal: Float = 180f +) { + val description = if (expanded) R.string.collapse_content else R.string.expand_content + val rotation by animateFloatAsState( + targetValue = if (expanded) rotationInitial else rotationFinal, + label = "rotation" + ) + + onClick?.let { + IconButton(onClick = it) { + Icon( + imageVector = Icons.Filled.KeyboardArrowUp, + contentDescription = stringResource(description), + modifier = Modifier + .rotate(rotation) + .then(modifier) + ) + } + } ?: Icon( + imageVector = Icons.Filled.KeyboardArrowUp, + contentDescription = stringResource(description), + modifier = Modifier + .rotate(rotation) + .then(modifier) + ) +} \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/ui/component/AutoUpdatesDialog.kt b/app/src/main/java/app/revanced/manager/ui/component/AutoUpdatesDialog.kt new file mode 100644 index 0000000000..29f2f97026 --- /dev/null +++ b/app/src/main/java/app/revanced/manager/ui/component/AutoUpdatesDialog.kt @@ -0,0 +1,83 @@ +package app.revanced.manager.ui.component + +import androidx.annotation.StringRes +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.Source +import androidx.compose.material.icons.outlined.Update +import androidx.compose.material3.AlertDialog +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.Icon +import androidx.compose.material3.ListItem +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.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import app.revanced.manager.R +import app.revanced.manager.ui.component.haptics.HapticCheckbox +import app.revanced.manager.util.transparentListItemColors + +@Composable +fun AutoUpdatesDialog(onSubmit: (Boolean, Boolean) -> Unit) { + var patchesEnabled by rememberSaveable { mutableStateOf(true) } + var managerEnabled by rememberSaveable { mutableStateOf(true) } + + AlertDialog( + onDismissRequest = {}, + confirmButton = { + TextButton(onClick = { onSubmit(managerEnabled, patchesEnabled) }) { + Text(stringResource(R.string.save)) + } + }, + icon = { Icon(Icons.Outlined.Update, null) }, + title = { Text(text = stringResource(R.string.auto_updates_dialog_title)) }, + text = { + Column( + verticalArrangement = Arrangement.spacedBy(8.dp), + ) { + Text(text = stringResource(R.string.auto_updates_dialog_description)) + + Column { + AutoUpdatesItem( + headline = R.string.auto_updates_dialog_manager, + icon = Icons.Outlined.Update, + checked = managerEnabled, + onCheckedChange = { managerEnabled = it } + ) + HorizontalDivider() + AutoUpdatesItem( + headline = R.string.auto_updates_dialog_patches, + icon = Icons.Outlined.Source, + checked = patchesEnabled, + onCheckedChange = { patchesEnabled = it } + ) + } + + Text(text = stringResource(R.string.auto_updates_dialog_note)) + } + } + ) +} + +@Composable +private fun AutoUpdatesItem( + @StringRes headline: Int, + icon: ImageVector, + checked: Boolean, + onCheckedChange: (Boolean) -> Unit +) = ListItem( + leadingContent = { Icon(icon, null) }, + headlineContent = { Text(stringResource(headline)) }, + trailingContent = { HapticCheckbox(checked = checked, onCheckedChange = null) }, + modifier = Modifier.clickable { onCheckedChange(!checked) }, + colors = transparentListItemColors +) diff --git a/app/src/main/java/app/revanced/manager/ui/component/AvailableUpdateDialog.kt b/app/src/main/java/app/revanced/manager/ui/component/AvailableUpdateDialog.kt new file mode 100644 index 0000000000..7b6ecd9a12 --- /dev/null +++ b/app/src/main/java/app/revanced/manager/ui/component/AvailableUpdateDialog.kt @@ -0,0 +1,84 @@ +package app.revanced.manager.ui.component + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.padding +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.Update +import androidx.compose.material3.* +import androidx.compose.runtime.* +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import app.revanced.manager.R +import app.revanced.manager.ui.component.haptics.HapticCheckbox +import app.revanced.manager.util.transparentListItemColors + +@Composable +fun AvailableUpdateDialog( + onDismiss: () -> Unit, + onConfirm: () -> Unit, + setShowManagerUpdateDialogOnLaunch: (Boolean) -> Unit, + newVersion: String +) { + var dontShowAgain by rememberSaveable { mutableStateOf(false) } + val dismissDialog = { + setShowManagerUpdateDialogOnLaunch(!dontShowAgain) + onDismiss() + } + + AlertDialogExtended( + onDismissRequest = dismissDialog, + confirmButton = { + TextButton( + onClick = { + dismissDialog() + onConfirm() + } + ) { + Text(stringResource(R.string.show)) + } + }, + dismissButton = { + TextButton( + onClick = dismissDialog + ) { + Text(stringResource(R.string.dismiss)) + } + }, + icon = { + Icon(imageVector = Icons.Outlined.Update, contentDescription = null) + }, + title = { + Text(stringResource(R.string.update_available)) + }, + text = { + Column( + modifier = Modifier.padding(horizontal = 8.dp), + verticalArrangement = Arrangement.spacedBy(4.dp), + ) { + Text( + modifier = Modifier.padding(horizontal = 16.dp), + text = stringResource(R.string.update_available_dialog_description, newVersion) + ) + ListItem( + modifier = Modifier.clickable { dontShowAgain = !dontShowAgain }, + headlineContent = { + Text(stringResource(R.string.never_show_again)) + }, + leadingContent = { + CompositionLocalProvider(LocalMinimumInteractiveComponentSize provides Dp.Unspecified) { + HapticCheckbox(checked = dontShowAgain, onCheckedChange = { dontShowAgain = it }) + } + }, + colors = transparentListItemColors + ) + } + }, + textHorizontalPadding = PaddingValues(0.dp) + ) +} diff --git a/app/src/main/java/app/revanced/manager/ui/component/CheckedFilterChip.kt b/app/src/main/java/app/revanced/manager/ui/component/CheckedFilterChip.kt new file mode 100644 index 0000000000..a81c456f7b --- /dev/null +++ b/app/src/main/java/app/revanced/manager/ui/component/CheckedFilterChip.kt @@ -0,0 +1,61 @@ +package app.revanced.manager.ui.component + +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.expandIn +import androidx.compose.animation.shrinkOut +import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.size +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.SelectableChipColors +import androidx.compose.material3.SelectableChipElevation +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Shape + +@Composable +fun CheckedFilterChip( + selected: Boolean, + onClick: () -> Unit, + label: @Composable () -> Unit, + modifier: Modifier = Modifier, + enabled: Boolean = true, + trailingIcon: @Composable (() -> Unit)? = null, + shape: Shape = FilterChipDefaults.shape, + colors: SelectableChipColors = FilterChipDefaults.filterChipColors(), + elevation: SelectableChipElevation? = FilterChipDefaults.filterChipElevation(), + border: BorderStroke? = FilterChipDefaults.filterChipBorder(enabled, selected), + interactionSource: MutableInteractionSource? = null +) { + FilterChip( + selected = selected, + onClick = onClick, + label = label, + modifier = modifier, + enabled = enabled, + leadingIcon = { + AnimatedVisibility( + visible = selected, + enter = expandIn(expandFrom = Alignment.CenterStart), + exit = shrinkOut(shrinkTowards = Alignment.CenterStart) + ) { + Icon( + modifier = Modifier.size(FilterChipDefaults.IconSize), + imageVector = Icons.Filled.Done, + contentDescription = null, + ) + } + }, + trailingIcon = trailingIcon, + shape = shape, + colors = colors, + elevation = elevation, + border = border, + interactionSource = interactionSource + ) +} \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/ui/component/ColumnWithScrollbar.kt b/app/src/main/java/app/revanced/manager/ui/component/ColumnWithScrollbar.kt new file mode 100644 index 0000000000..bc35877966 --- /dev/null +++ b/app/src/main/java/app/revanced/manager/ui/component/ColumnWithScrollbar.kt @@ -0,0 +1,29 @@ +package app.revanced.manager.ui.component + +import androidx.compose.foundation.ScrollState +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.ColumnScope +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier + +@Composable +fun ColumnWithScrollbar( + modifier: Modifier = Modifier, + state: ScrollState = rememberScrollState(), + verticalArrangement: Arrangement.Vertical = Arrangement.Top, + horizontalAlignment: Alignment.Horizontal = Alignment.Start, + content: @Composable ColumnScope.() -> Unit +) { + Column( + modifier = modifier.then(Modifier.verticalScroll(state)), + verticalArrangement = verticalArrangement, + horizontalAlignment = horizontalAlignment, + content = content + ) + Scrollbar(state, Modifier.then(modifier.padding())) // Get the modifier's padding to maintain scrollbar within the screen, e.g. paddingValues +} \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/ui/component/ContentSelector.kt b/app/src/main/java/app/revanced/manager/ui/component/ContentSelector.kt new file mode 100644 index 0000000000..3e7117b1f8 --- /dev/null +++ b/app/src/main/java/app/revanced/manager/ui/component/ContentSelector.kt @@ -0,0 +1,21 @@ +package app.revanced.manager.ui.component + +import android.net.Uri +import androidx.activity.compose.rememberLauncherForActivityResult +import androidx.activity.result.contract.ActivityResultContracts +import androidx.compose.material3.Button +import androidx.compose.runtime.Composable + +@Composable +fun ContentSelector(mime: String, onSelect: (Uri) -> Unit, content: @Composable () -> Unit) { + val activityLauncher = rememberLauncherForActivityResult(ActivityResultContracts.GetContent()) { uri -> + uri?.let(onSelect) + } + Button( + onClick = { + activityLauncher.launch(mime) + } + ) { + content() + } +} \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/ui/component/ExceptionViewerDialog.kt b/app/src/main/java/app/revanced/manager/ui/component/ExceptionViewerDialog.kt new file mode 100644 index 0000000000..1ceb9cef27 --- /dev/null +++ b/app/src/main/java/app/revanced/manager/ui/component/ExceptionViewerDialog.kt @@ -0,0 +1,79 @@ +package app.revanced.manager.ui.component + +import android.content.Intent +import androidx.compose.foundation.horizontalScroll +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.rememberScrollState +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.ArrowBack +import androidx.compose.material.icons.outlined.Share +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.window.Dialog +import androidx.compose.ui.window.DialogProperties +import app.revanced.manager.R +import app.revanced.manager.ui.component.bundle.BundleTopBar + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun ExceptionViewerDialog(text: String, onDismiss: () -> Unit) { + val context = LocalContext.current + + Dialog( + onDismissRequest = onDismiss, + properties = DialogProperties( + usePlatformDefaultWidth = false, + dismissOnBackPress = true + ) + ) { + Scaffold( + topBar = { + BundleTopBar( + title = stringResource(R.string.bundle_error), + onBackClick = onDismiss, + backIcon = { + Icon( + Icons.AutoMirrored.Filled.ArrowBack, + stringResource(R.string.back) + ) + }, + actions = { + IconButton( + onClick = { + val sendIntent: Intent = Intent().apply { + action = Intent.ACTION_SEND + putExtra( + Intent.EXTRA_TEXT, + text + ) + type = "text/plain" + } + + val shareIntent = Intent.createChooser(sendIntent, null) + context.startActivity(shareIntent) + } + ) { + Icon( + Icons.Outlined.Share, + contentDescription = stringResource(R.string.share) + ) + } + } + ) + } + ) { paddingValues -> + ColumnWithScrollbar( + modifier = Modifier.padding(paddingValues) + ) { + Text(text, modifier = Modifier.horizontalScroll(rememberScrollState())) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/ui/component/GroupHeader.kt b/app/src/main/java/app/revanced/manager/ui/component/GroupHeader.kt new file mode 100644 index 0000000000..b07b23e683 --- /dev/null +++ b/app/src/main/java/app/revanced/manager/ui/component/GroupHeader.kt @@ -0,0 +1,23 @@ +package app.revanced.manager.ui.component + +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.semantics.heading +import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.unit.dp + +@Composable +fun GroupHeader( + title: String, + modifier: Modifier = Modifier +) { + Text( + text = title, + color = MaterialTheme.colorScheme.primary, + style = MaterialTheme.typography.labelLarge, + modifier = Modifier.padding(24.dp).semantics { heading() }.then(modifier) + ) +} \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/ui/component/InstallerStatusDialog.kt b/app/src/main/java/app/revanced/manager/ui/component/InstallerStatusDialog.kt new file mode 100644 index 0000000000..2ae48ce6ff --- /dev/null +++ b/app/src/main/java/app/revanced/manager/ui/component/InstallerStatusDialog.kt @@ -0,0 +1,149 @@ +package app.revanced.manager.ui.component + +import android.content.pm.PackageInstaller +import androidx.annotation.RequiresApi +import androidx.annotation.StringRes +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.ErrorOutline +import androidx.compose.material3.AlertDialog +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import app.revanced.manager.R +import app.revanced.manager.ui.model.InstallerModel +import com.github.materiiapps.enumutil.FromValue + +private typealias InstallerStatusDialogButtonHandler = ((model: InstallerModel) -> Unit) +private typealias InstallerStatusDialogButton = @Composable (model: InstallerModel, dismiss: () -> Unit) -> Unit + +@Composable +fun InstallerStatusDialog(installerStatus: Int, model: InstallerModel, onDismiss: () -> Unit) { + val dialogKind = remember { + DialogKind.fromValue(installerStatus) ?: DialogKind.FAILURE + } + + AlertDialog( + onDismissRequest = onDismiss, + confirmButton = { + dialogKind.confirmButton(model, onDismiss) + }, + dismissButton = { + dialogKind.dismissButton?.invoke(model, onDismiss) + }, + icon = { + Icon(dialogKind.icon, null) + }, + title = { + Text( + text = stringResource(dialogKind.title), + style = MaterialTheme.typography.headlineSmall.copy(textAlign = TextAlign.Center), + color = MaterialTheme.colorScheme.onSurface, + ) + }, + text = { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(16.dp), + ) { + Text(stringResource(dialogKind.contentStringResId)) + } + } + ) +} + +private fun installerStatusDialogButton( + @StringRes buttonStringResId: Int, + buttonHandler: InstallerStatusDialogButtonHandler = { }, +): InstallerStatusDialogButton = { model, dismiss -> + TextButton( + onClick = { + dismiss() + buttonHandler(model) + } + ) { + Text(stringResource(buttonStringResId)) + } +} + +@FromValue("flag") +enum class DialogKind( + val flag: Int, + val title: Int, + @StringRes val contentStringResId: Int, + val icon: ImageVector = Icons.Outlined.ErrorOutline, + val confirmButton: InstallerStatusDialogButton = installerStatusDialogButton(R.string.ok), + val dismissButton: InstallerStatusDialogButton? = null, +) { + FAILURE( + flag = PackageInstaller.STATUS_FAILURE, + title = R.string.installation_failed_dialog_title, + contentStringResId = R.string.installation_failed_description, + confirmButton = installerStatusDialogButton(R.string.install_app) { model -> + model.install() + } + ), + FAILURE_ABORTED( + flag = PackageInstaller.STATUS_FAILURE_ABORTED, + title = R.string.installation_cancelled_dialog_title, + contentStringResId = R.string.installation_aborted_description, + confirmButton = installerStatusDialogButton(R.string.install_app) { model -> + model.install() + } + ), + FAILURE_BLOCKED( + flag = PackageInstaller.STATUS_FAILURE_BLOCKED, + title = R.string.installation_blocked_dialog_title, + contentStringResId = R.string.installation_blocked_description, + ), + FAILURE_CONFLICT( + flag = PackageInstaller.STATUS_FAILURE_CONFLICT, + title = R.string.installation_conflict_dialog_title, + contentStringResId = R.string.installation_conflict_description, + confirmButton = installerStatusDialogButton(R.string.reinstall) { model -> + model.reinstall() + }, + dismissButton = installerStatusDialogButton(R.string.cancel), + ), + FAILURE_INCOMPATIBLE( + flag = PackageInstaller.STATUS_FAILURE_INCOMPATIBLE, + title = R.string.installation_incompatible_dialog_title, + contentStringResId = R.string.installation_incompatible_description, + ), + FAILURE_INVALID( + flag = PackageInstaller.STATUS_FAILURE_INVALID, + title = R.string.installation_invalid_dialog_title, + contentStringResId = R.string.installation_invalid_description, + confirmButton = installerStatusDialogButton(R.string.reinstall) { model -> + model.reinstall() + }, + dismissButton = installerStatusDialogButton(R.string.cancel), + ), + FAILURE_STORAGE( + flag = PackageInstaller.STATUS_FAILURE_STORAGE, + title = R.string.installation_storage_issue_dialog_title, + contentStringResId = R.string.installation_storage_issue_description, + ), + + @RequiresApi(34) + FAILURE_TIMEOUT( + flag = PackageInstaller.STATUS_FAILURE_TIMEOUT, + title = R.string.installation_timeout_dialog_title, + contentStringResId = R.string.installation_timeout_description, + confirmButton = installerStatusDialogButton(R.string.install_app) { model -> + model.install() + }, + ); + + // Needed due to the @FromValue annotation. + companion object +} diff --git a/app/src/main/java/app/revanced/manager/ui/component/LazyColumnWithScrollbar.kt b/app/src/main/java/app/revanced/manager/ui/component/LazyColumnWithScrollbar.kt new file mode 100644 index 0000000000..e1f984c546 --- /dev/null +++ b/app/src/main/java/app/revanced/manager/ui/component/LazyColumnWithScrollbar.kt @@ -0,0 +1,42 @@ +package app.revanced.manager.ui.component + +import androidx.compose.foundation.gestures.FlingBehavior +import androidx.compose.foundation.gestures.ScrollableDefaults +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.LazyListScope +import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp + +@Composable +fun LazyColumnWithScrollbar( + modifier: Modifier = Modifier, + state: LazyListState = rememberLazyListState(), + contentPadding: PaddingValues = PaddingValues(0.dp), + reverseLayout: Boolean = false, + verticalArrangement: Arrangement.Vertical = + if (!reverseLayout) Arrangement.Top else Arrangement.Bottom, + horizontalAlignment: Alignment.Horizontal = Alignment.Start, + flingBehavior: FlingBehavior = ScrollableDefaults.flingBehavior(), + userScrollEnabled: Boolean = true, + content: LazyListScope.() -> Unit +) { + LazyColumn( + modifier = modifier, + state = state, + contentPadding = contentPadding, + reverseLayout = reverseLayout, + verticalArrangement = verticalArrangement, + horizontalAlignment = horizontalAlignment, + flingBehavior = flingBehavior, + userScrollEnabled = userScrollEnabled, + content = content + ) + Scrollbar(state, Modifier.then(modifier.padding())) // Get the modifier's padding to maintain scrollbar within the screen, e.g. paddingValues +} \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/ui/component/LoadingIndicator.kt b/app/src/main/java/app/revanced/manager/ui/component/LoadingIndicator.kt new file mode 100644 index 0000000000..44d5c9cd9d --- /dev/null +++ b/app/src/main/java/app/revanced/manager/ui/component/LoadingIndicator.kt @@ -0,0 +1,37 @@ +package app.revanced.manager.ui.component + +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.ProgressIndicatorDefaults +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.StrokeCap +import androidx.compose.ui.unit.Dp + +@Composable +fun LoadingIndicator( + modifier: Modifier = Modifier, + progress: () -> Float? = { null }, + color: Color = ProgressIndicatorDefaults.circularColor, + strokeWidth: Dp = ProgressIndicatorDefaults.CircularStrokeWidth, + trackColor: Color = ProgressIndicatorDefaults.circularTrackColor, + strokeCap: StrokeCap = ProgressIndicatorDefaults.CircularDeterminateStrokeCap +) { + progress()?.let { + CircularProgressIndicator( + progress = { it }, + modifier = modifier, + color = color, + strokeWidth = strokeWidth, + trackColor = trackColor, + strokeCap = strokeCap + ) + } ?: + CircularProgressIndicator( + modifier = modifier, + color = color, + strokeWidth = strokeWidth, + trackColor = trackColor, + strokeCap = strokeCap + ) +} \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/ui/component/Markdown.kt b/app/src/main/java/app/revanced/manager/ui/component/Markdown.kt new file mode 100644 index 0000000000..2b5b276ea5 --- /dev/null +++ b/app/src/main/java/app/revanced/manager/ui/component/Markdown.kt @@ -0,0 +1,32 @@ +package app.revanced.manager.ui.component + +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.text.font.FontWeight +import com.mikepenz.markdown.compose.Markdown +import com.mikepenz.markdown.m3.markdownColor +import com.mikepenz.markdown.m3.markdownTypography + +@Composable +fun Markdown( + text: String +) { + val markdown = text.trimIndent() + + Markdown( + content = markdown, + colors = markdownColor( + text = MaterialTheme.colorScheme.onSurfaceVariant, + codeBackground = MaterialTheme.colorScheme.secondaryContainer, + codeText = MaterialTheme.colorScheme.onSecondaryContainer, + linkText = MaterialTheme.colorScheme.primary + ), + typography = markdownTypography( + h1 = MaterialTheme.typography.headlineSmall.copy(fontWeight = FontWeight.Bold), + h2 = MaterialTheme.typography.titleLarge.copy(fontWeight = FontWeight.Bold), + h3 = MaterialTheme.typography.titleMedium.copy(fontWeight = FontWeight.Bold), + text = MaterialTheme.typography.bodyMedium, + list = MaterialTheme.typography.bodyMedium + ) + ) +} \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/ui/component/NotificationCard.kt b/app/src/main/java/app/revanced/manager/ui/component/NotificationCard.kt new file mode 100644 index 0000000000..0357a18f8e --- /dev/null +++ b/app/src/main/java/app/revanced/manager/ui/component/NotificationCard.kt @@ -0,0 +1,182 @@ +package app.revanced.manager.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.Row +import androidx.compose.foundation.layout.RowScope +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.Close +import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import app.revanced.manager.R + +@Composable +fun NotificationCard( + text: String, + icon: ImageVector, + modifier: Modifier = Modifier, + actions: (@Composable RowScope.() -> Unit)? = null, + title: String? = null, + isWarning: Boolean = false +) { + val color = + if (isWarning) MaterialTheme.colorScheme.onError else MaterialTheme.colorScheme.onPrimaryContainer + + NotificationCardInstance(modifier = modifier, isWarning = isWarning) { + Row( + modifier = Modifier.padding(16.dp), + horizontalArrangement = Arrangement.spacedBy(16.dp) + ) { + Box( + modifier = Modifier.size(28.dp), + contentAlignment = Alignment.Center + ) { + Icon( + modifier = Modifier.size(24.dp), + imageVector = icon, + contentDescription = null, + tint = color, + ) + } + + Column( + verticalArrangement = Arrangement.spacedBy(8.dp) + ) { + title?.let { + Text( + text = it, + style = MaterialTheme.typography.titleLarge, + color = color, + ) + } + Text( + text = text, + style = MaterialTheme.typography.bodyMedium, + color = color, + ) + Row( + horizontalArrangement = Arrangement.End, + modifier = Modifier.fillMaxWidth() + ) { + actions?.invoke(this) + } + } + } + } +} + +@Composable +fun NotificationCard( + text: String, + icon: ImageVector, + modifier: Modifier = Modifier, + title: String? = null, + isWarning: Boolean = false, + onDismiss: (() -> Unit)? = null, + onClick: (() -> Unit)? = null +) { + val color = + if (isWarning) MaterialTheme.colorScheme.onError else MaterialTheme.colorScheme.onPrimaryContainer + + NotificationCardInstance(modifier = modifier, isWarning = isWarning, onClick = onClick) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(16.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(16.dp), + ) { + Box( + modifier = Modifier.size(28.dp), + contentAlignment = Alignment.Center + ) { + Icon( + modifier = Modifier.size(24.dp), + imageVector = icon, + contentDescription = null, + tint = color, + ) + } + if (title != null) { + Column( + modifier = Modifier.weight(1f), + verticalArrangement = Arrangement.spacedBy(6.dp) + ) { + Text( + text = title, + style = MaterialTheme.typography.titleLarge, + color = color, + ) + Text( + text = text, + style = MaterialTheme.typography.bodyMedium, + color = color, + ) + } + } else { + Text( + modifier = Modifier.weight(1f), + text = text, + style = MaterialTheme.typography.bodyMedium, + color = color, + ) + } + if (onDismiss != null) { + IconButton(onClick = onDismiss) { + Icon( + imageVector = Icons.Outlined.Close, + contentDescription = stringResource(R.string.close), + tint = color, + ) + } + } + } + } +} + +@Composable +private fun NotificationCardInstance( + modifier: Modifier = Modifier, + isWarning: Boolean = false, + onClick: (() -> Unit)? = null, + content: @Composable () -> Unit, +) { + val colors = + CardDefaults.cardColors(containerColor = if (isWarning) MaterialTheme.colorScheme.error else MaterialTheme.colorScheme.primaryContainer) + val defaultModifier = Modifier + .fillMaxWidth() + .clip(RoundedCornerShape(24.dp)) + + if (onClick != null) { + Card( + onClick = onClick, + colors = colors, + modifier = modifier.then(defaultModifier) + ) { + content() + } + } else { + Card( + colors = colors, + modifier = modifier.then(defaultModifier) + ) { + content() + } + } +} \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/ui/component/NumberInputDialog.kt b/app/src/main/java/app/revanced/manager/ui/component/NumberInputDialog.kt new file mode 100644 index 0000000000..5a293165fd --- /dev/null +++ b/app/src/main/java/app/revanced/manager/ui/component/NumberInputDialog.kt @@ -0,0 +1,99 @@ +package app.revanced.manager.ui.component + +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.material3.AlertDialog +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.DisallowComposableCalls +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import app.revanced.manager.R + +@Composable +private inline fun <T> NumberInputDialog( + current: T?, + name: String, + crossinline onSubmit: (T?) -> Unit, + crossinline validator: @DisallowComposableCalls (T) -> Boolean, + crossinline toNumberOrNull: @DisallowComposableCalls String.() -> T? +) { + var fieldValue by rememberSaveable { + mutableStateOf(current?.toString().orEmpty()) + } + val numberFieldValue by remember { + derivedStateOf { fieldValue.toNumberOrNull() } + } + val validatorFailed by remember { + derivedStateOf { numberFieldValue?.let { !validator(it) } ?: false } + } + + AlertDialog( + onDismissRequest = { onSubmit(null) }, + title = { Text(name) }, + text = { + OutlinedTextField( + value = fieldValue, + onValueChange = { fieldValue = it }, + placeholder = { + Text(stringResource(R.string.dialog_input_placeholder)) + }, + isError = validatorFailed, + supportingText = { + if (validatorFailed) { + Text( + stringResource(R.string.input_dialog_value_invalid), + modifier = Modifier.fillMaxWidth(), + color = MaterialTheme.colorScheme.error + ) + } + } + ) + }, + confirmButton = { + TextButton( + onClick = { numberFieldValue?.let(onSubmit) }, + enabled = numberFieldValue != null && !validatorFailed, + ) { + Text(stringResource(R.string.save)) + } + }, + dismissButton = { + TextButton(onClick = { onSubmit(null) }) { + Text(stringResource(R.string.cancel)) + } + }, + ) +} + +@Composable +fun IntInputDialog( + current: Int?, + name: String, + validator: (Int) -> Boolean = { true }, + onSubmit: (Int?) -> Unit +) = NumberInputDialog(current, name, onSubmit, validator, String::toIntOrNull) + +@Composable +fun LongInputDialog( + current: Long?, + name: String, + validator: (Long) -> Boolean = { true }, + onSubmit: (Long?) -> Unit +) = NumberInputDialog(current, name, onSubmit, validator, String::toLongOrNull) + +@Composable +fun FloatInputDialog( + current: Float?, + name: String, + validator: (Float) -> Boolean = { true }, + onSubmit: (Float?) -> Unit +) = NumberInputDialog(current, name, onSubmit, validator, String::toFloatOrNull) \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/ui/component/PasswordField.kt b/app/src/main/java/app/revanced/manager/ui/component/PasswordField.kt new file mode 100644 index 0000000000..ee64c05ba0 --- /dev/null +++ b/app/src/main/java/app/revanced/manager/ui/component/PasswordField.kt @@ -0,0 +1,50 @@ +package app.revanced.manager.ui.component + +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.Visibility +import androidx.compose.material.icons.outlined.VisibilityOff +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.OutlinedTextField +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.input.KeyboardType +import androidx.compose.ui.text.input.PasswordVisualTransformation +import androidx.compose.ui.text.input.VisualTransformation +import app.revanced.manager.R + +@Composable +fun PasswordField(modifier: Modifier = Modifier, value: String, onValueChange: (String) -> Unit, label: @Composable (() -> Unit)? = null, placeholder: @Composable (() -> Unit)? = null) { + var visible by rememberSaveable { + mutableStateOf(false) + } + + OutlinedTextField( + value = value, + onValueChange = onValueChange, + placeholder = placeholder, + label = label, + modifier = modifier, + trailingIcon = { + IconButton(onClick = { + visible = !visible + }) { + val (icon, description) = remember(visible) { + if (visible) Icons.Outlined.VisibilityOff to R.string.hide_password_field else Icons.Outlined.Visibility to R.string.show_password_field + } + Icon(icon, stringResource(description)) + } + }, + keyboardOptions = KeyboardOptions( + keyboardType = KeyboardType.Password + ), + visualTransformation = if (visible) VisualTransformation.None else PasswordVisualTransformation() + ) +} \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/ui/component/SafeguardDialog.kt b/app/src/main/java/app/revanced/manager/ui/component/SafeguardDialog.kt new file mode 100644 index 0000000000..6aabe12a77 --- /dev/null +++ b/app/src/main/java/app/revanced/manager/ui/component/SafeguardDialog.kt @@ -0,0 +1,51 @@ +package app.revanced.manager.ui.component + +import androidx.annotation.StringRes +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.WarningAmber +import androidx.compose.material3.AlertDialog +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextAlign +import app.revanced.manager.R + +@Composable +fun SafeguardDialog( + onDismiss: () -> Unit, + @StringRes title: Int, + body: String, +) { + AlertDialog( + onDismissRequest = onDismiss, + confirmButton = { + TextButton(onClick = onDismiss) { + Text(stringResource(R.string.ok)) + } + }, + icon = { + Icon(Icons.Outlined.WarningAmber, null) + }, + title = { + Text( + text = stringResource(title), + style = MaterialTheme.typography.headlineSmall.copy(textAlign = TextAlign.Center) + ) + }, + text = { + Text(body) + } + ) +} + +@Composable +fun NonSuggestedVersionDialog(suggestedVersion: String, onDismiss: () -> Unit) { + SafeguardDialog( + onDismiss = onDismiss, + title = R.string.non_suggested_version_warning_title, + body = stringResource(R.string.non_suggested_version_warning_description, suggestedVersion), + ) +} \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/ui/component/Scrollbar.kt b/app/src/main/java/app/revanced/manager/ui/component/Scrollbar.kt new file mode 100644 index 0000000000..88917f8291 --- /dev/null +++ b/app/src/main/java/app/revanced/manager/ui/component/Scrollbar.kt @@ -0,0 +1,64 @@ +package app.revanced.manager.ui.component + +import androidx.compose.foundation.ScrollState +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import com.gigamole.composescrollbars.Scrollbars +import com.gigamole.composescrollbars.ScrollbarsState +import com.gigamole.composescrollbars.config.ScrollbarsConfig +import com.gigamole.composescrollbars.config.ScrollbarsOrientation +import com.gigamole.composescrollbars.config.layercontenttype.ScrollbarsLayerContentType +import com.gigamole.composescrollbars.config.layersType.ScrollbarsLayersType +import com.gigamole.composescrollbars.config.layersType.thicknessType.ScrollbarsThicknessType +import com.gigamole.composescrollbars.config.visibilitytype.ScrollbarsVisibilityType +import com.gigamole.composescrollbars.scrolltype.ScrollbarsScrollType +import com.gigamole.composescrollbars.scrolltype.knobtype.ScrollbarsDynamicKnobType +import com.gigamole.composescrollbars.scrolltype.knobtype.ScrollbarsStaticKnobType + +@Composable +fun Scrollbar(scrollState: ScrollState, modifier: Modifier = Modifier) { + Scrollbar( + ScrollbarsScrollType.Scroll( + knobType = ScrollbarsStaticKnobType.Auto(), + state = scrollState + ), + modifier + ) +} + +@Composable +fun Scrollbar(lazyListState: LazyListState, modifier: Modifier = Modifier) { + Scrollbar( + ScrollbarsScrollType.Lazy.List.Dynamic( + knobType = ScrollbarsDynamicKnobType.Auto(), + state = lazyListState + ), + modifier + ) +} + +@Composable +private fun Scrollbar(scrollType: ScrollbarsScrollType, modifier: Modifier = Modifier) { + Scrollbars( + state = ScrollbarsState( + ScrollbarsConfig( + orientation = ScrollbarsOrientation.Vertical, + paddingValues = PaddingValues(0.dp), + layersType = ScrollbarsLayersType.Wrap(ScrollbarsThicknessType.Exact(4.dp)), + knobLayerContentType = ScrollbarsLayerContentType.Default.Colored.Idle( + idleColor = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.35f) + ), + visibilityType = ScrollbarsVisibilityType.Dynamic.Fade( + isVisibleOnTouchDown = true, + isStaticWhenScrollPossible = false + ) + ), + scrollType + ), + modifier = modifier + ) +} \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/ui/component/SearchBar.kt b/app/src/main/java/app/revanced/manager/ui/component/SearchBar.kt new file mode 100644 index 0000000000..7c48b8121c --- /dev/null +++ b/app/src/main/java/app/revanced/manager/ui/component/SearchBar.kt @@ -0,0 +1,60 @@ +package app.revanced.manager.ui.component + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.ColumnScope +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.sizeIn +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.SearchBar +import androidx.compose.material3.SearchBarColors +import androidx.compose.material3.SearchBarDefaults +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalSoftwareKeyboardController +import androidx.compose.ui.unit.dp + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun SearchBar( + query: String, + onQueryChange: (String) -> Unit, + expanded: Boolean, + onExpandedChange: (Boolean) -> Unit, + placeholder: (@Composable () -> Unit)? = null, + leadingIcon: @Composable (() -> Unit)? = null, + trailingIcon: @Composable (() -> Unit)? = null, + content: @Composable ColumnScope.() -> Unit +) { + val colors = SearchBarColors( + containerColor = MaterialTheme.colorScheme.surfaceContainerHigh, + dividerColor = MaterialTheme.colorScheme.outline + ) + val keyboardController = LocalSoftwareKeyboardController.current + + Box(modifier = Modifier.fillMaxWidth()) { + SearchBar( + modifier = Modifier.align(Alignment.Center), + inputField = { + SearchBarDefaults.InputField( + modifier = Modifier.sizeIn(minWidth = 380.dp), + query = query, + onQueryChange = onQueryChange, + onSearch = { + keyboardController?.hide() + }, + expanded = expanded, + onExpandedChange = onExpandedChange, + placeholder = placeholder, + leadingIcon = leadingIcon, + trailingIcon = trailingIcon + ) + }, + expanded = expanded, + onExpandedChange = onExpandedChange, + colors = colors, + content = content + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/ui/component/SearchView.kt b/app/src/main/java/app/revanced/manager/ui/component/SearchView.kt new file mode 100644 index 0000000000..04b5b58949 --- /dev/null +++ b/app/src/main/java/app/revanced/manager/ui/component/SearchView.kt @@ -0,0 +1,70 @@ +package app.revanced.manager.ui.component + +import androidx.compose.foundation.layout.ColumnScope +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.ArrowBack +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.SearchBar +import androidx.compose.material3.SearchBarColors +import androidx.compose.material3.SearchBarDefaults +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.platform.LocalSoftwareKeyboardController +import androidx.compose.ui.res.stringResource +import app.revanced.manager.R + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun SearchView( + query: String, + onQueryChange: (String) -> Unit, + onActiveChange: (Boolean) -> Unit, + placeholder: (@Composable () -> Unit)? = null, + content: @Composable ColumnScope.() -> Unit +) { + val colors = SearchBarColors( + containerColor = MaterialTheme.colorScheme.surfaceContainerHigh, + dividerColor = MaterialTheme.colorScheme.outline + ) + val focusRequester = remember { FocusRequester() } + val keyboardController = LocalSoftwareKeyboardController.current + + SearchBar( + inputField = { + SearchBarDefaults.InputField( + query = query, + onQueryChange = onQueryChange, + onSearch = { + keyboardController?.hide() + }, + expanded = true, + onExpandedChange = onActiveChange, + placeholder = placeholder, + leadingIcon = { + IconButton(onClick = { onActiveChange(false) }) { + Icon( + Icons.AutoMirrored.Filled.ArrowBack, + stringResource(R.string.back) + ) + } + } + ) + }, + expanded = true, + onExpandedChange = onActiveChange, + modifier = Modifier.focusRequester(focusRequester), + colors = colors, + content = content + ) + + LaunchedEffect(Unit) { + focusRequester.requestFocus() + } +} \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/ui/component/SegmentedButton.kt b/app/src/main/java/app/revanced/manager/ui/component/SegmentedButton.kt new file mode 100644 index 0000000000..bf07d72582 --- /dev/null +++ b/app/src/main/java/app/revanced/manager/ui/component/SegmentedButton.kt @@ -0,0 +1,80 @@ +package app.revanced.manager.ui.component + +import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.background +import androidx.compose.foundation.basicMarquee +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.RowScope +import androidx.compose.foundation.layout.padding +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.CompositionLocalProvider +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.painter.Painter +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.unit.dp + +/** + * Credits to [Vendetta](https://github.com/vendetta-mod) + */ +@OptIn(ExperimentalFoundationApi::class) +@Composable +fun RowScope.SegmentedButton( + icon: Any, + text: String, + onClick: () -> Unit, + iconDescription: String? = null, + enabled: Boolean = true +) { + val contentColor = if (enabled) + MaterialTheme.colorScheme.primary + else + MaterialTheme.colorScheme.onSurface.copy(0.38f) + + CompositionLocalProvider(LocalContentColor provides contentColor) { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(8.dp, Alignment.CenterVertically), + modifier = Modifier + .clickable(enabled = enabled, onClick = onClick) + .background( + if (enabled) + MaterialTheme.colorScheme.surfaceColorAtElevation(2.dp) + else + MaterialTheme.colorScheme.onSurface.copy(0.12f) + ) + .weight(1f) + .padding(vertical = 20.dp) + ) { + when (icon) { + is ImageVector -> { + Icon( + imageVector = icon, + contentDescription = iconDescription + ) + } + + is Painter -> { + Icon( + painter = icon, + contentDescription = iconDescription + ) + } + } + + Text( + text = text, + style = MaterialTheme.typography.labelLarge, + maxLines = 1, + modifier = Modifier.basicMarquee() + ) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/ui/component/TextInputDialog.kt b/app/src/main/java/app/revanced/manager/ui/component/TextInputDialog.kt new file mode 100644 index 0000000000..d0484f1e98 --- /dev/null +++ b/app/src/main/java/app/revanced/manager/ui/component/TextInputDialog.kt @@ -0,0 +1,51 @@ +package app.revanced.manager.ui.component + +import androidx.compose.material3.AlertDialog +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.material3.TextField +import androidx.compose.runtime.Composable +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.ui.res.stringResource +import app.revanced.manager.R + +@Composable +fun TextInputDialog( + initial: String, + title: String, + onDismissRequest: () -> Unit, + onConfirm: (String) -> Unit, + validator: (String) -> Boolean = String::isNotEmpty, +) { + val (value, setValue) = rememberSaveable(initial) { + mutableStateOf(initial) + } + val valid = remember(value, validator) { + validator(value) + } + + AlertDialog( + onDismissRequest = onDismissRequest, + confirmButton = { + TextButton( + onClick = { onConfirm(value) }, + enabled = valid + ) { + Text(stringResource(R.string.ok)) + } + }, + dismissButton = { + TextButton(onClick = onDismissRequest) { + Text(stringResource(R.string.cancel)) + } + }, + title = { + Text(title) + }, + text = { + TextField(value = value, onValueChange = setValue) + } + ) +} \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/ui/component/bundle/BaseBundleDialog.kt b/app/src/main/java/app/revanced/manager/ui/component/bundle/BaseBundleDialog.kt new file mode 100644 index 0000000000..dfc63735b9 --- /dev/null +++ b/app/src/main/java/app/revanced/manager/ui/component/bundle/BaseBundleDialog.kt @@ -0,0 +1,181 @@ +package app.revanced.manager.ui.component.bundle + +import android.webkit.URLUtil +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.* +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.outlined.ArrowRight +import androidx.compose.material.icons.outlined.Extension +import androidx.compose.material.icons.outlined.Inventory2 +import androidx.compose.material.icons.outlined.Sell +import androidx.compose.material3.* +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.graphics.vector.ImageVector +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import app.revanced.manager.R +import app.revanced.manager.ui.component.ColumnWithScrollbar +import app.revanced.manager.ui.component.TextInputDialog +import app.revanced.manager.ui.component.haptics.HapticSwitch + +@Composable +fun BaseBundleDialog( + modifier: Modifier = Modifier, + isDefault: Boolean, + name: String?, + remoteUrl: String?, + onRemoteUrlChange: ((String) -> Unit)? = null, + patchCount: Int, + version: String?, + autoUpdate: Boolean, + onAutoUpdateChange: (Boolean) -> Unit, + onPatchesClick: () -> Unit, + extraFields: @Composable ColumnScope.() -> Unit = {} +) { + ColumnWithScrollbar( + modifier = Modifier + .fillMaxWidth() + .then(modifier), + ) { + Column( + modifier = Modifier.padding(16.dp), + verticalArrangement = Arrangement.spacedBy(4.dp) + ) { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(8.dp, Alignment.Start), + verticalAlignment = Alignment.CenterVertically + ) { + Icon( + imageVector = Icons.Outlined.Inventory2, + contentDescription = null, + tint = MaterialTheme.colorScheme.primary, + modifier = Modifier.size(32.dp) + ) + name?.let { + Text( + text = it, + style = MaterialTheme.typography.titleLarge.copy(fontWeight = FontWeight(800)), + color = MaterialTheme.colorScheme.primary, + ) + } + } + Row( + horizontalArrangement = Arrangement.spacedBy(16.dp), + modifier = Modifier + .fillMaxWidth() + .padding(start = 2.dp) + ) { + version?.let { + Tag(Icons.Outlined.Sell, it) + } + Tag(Icons.Outlined.Extension, patchCount.toString()) + } + } + + HorizontalDivider( + modifier = Modifier.padding(horizontal = 16.dp), + color = MaterialTheme.colorScheme.outlineVariant + ) + + if (remoteUrl != null) { + BundleListItem( + headlineText = stringResource(R.string.bundle_auto_update), + supportingText = stringResource(R.string.bundle_auto_update_description), + trailingContent = { + HapticSwitch( + checked = autoUpdate, + onCheckedChange = onAutoUpdateChange + ) + }, + modifier = Modifier.clickable { + onAutoUpdateChange(!autoUpdate) + } + ) + } + + remoteUrl?.takeUnless { isDefault }?.let { url -> + var showUrlInputDialog by rememberSaveable { + mutableStateOf(false) + } + if (showUrlInputDialog) { + TextInputDialog( + initial = url, + title = stringResource(R.string.bundle_input_source_url), + onDismissRequest = { showUrlInputDialog = false }, + onConfirm = { + showUrlInputDialog = false + onRemoteUrlChange?.invoke(it) + }, + validator = { + if (it.isEmpty()) return@TextInputDialog false + + URLUtil.isValidUrl(it) + } + ) + } + + BundleListItem( + modifier = Modifier.clickable( + enabled = onRemoteUrlChange != null, + onClick = { + showUrlInputDialog = true + } + ), + headlineText = stringResource(R.string.bundle_input_source_url), + supportingText = url.ifEmpty { + stringResource(R.string.field_not_set) + } + ) + } + + val patchesClickable = patchCount > 0 + BundleListItem( + headlineText = stringResource(R.string.patches), + supportingText = stringResource(R.string.bundle_view_patches), + modifier = Modifier.clickable( + enabled = patchesClickable, + onClick = onPatchesClick + ) + ) { + if (patchesClickable) { + Icon( + Icons.AutoMirrored.Outlined.ArrowRight, + stringResource(R.string.patches) + ) + } + } + + extraFields() + } +} + +@Composable +private fun Tag( + icon: ImageVector, + text: String +) { + Row( + horizontalArrangement = Arrangement.spacedBy(6.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Icon( + imageVector = icon, + contentDescription = null, + modifier = Modifier.size(16.dp), + tint = MaterialTheme.colorScheme.outline, + ) + Text( + text, + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.outline, + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/ui/component/bundle/BundleInformationDialog.kt b/app/src/main/java/app/revanced/manager/ui/component/bundle/BundleInformationDialog.kt new file mode 100644 index 0000000000..eaebd8341e --- /dev/null +++ b/app/src/main/java/app/revanced/manager/ui/component/bundle/BundleInformationDialog.kt @@ -0,0 +1,146 @@ +package app.revanced.manager.ui.component.bundle + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.padding +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.ArrowBack +import androidx.compose.material.icons.automirrored.outlined.ArrowRight +import androidx.compose.material.icons.outlined.DeleteOutline +import androidx.compose.material.icons.outlined.Update +import androidx.compose.material3.* +import androidx.compose.runtime.* +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.window.Dialog +import androidx.compose.ui.window.DialogProperties +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import app.revanced.manager.R +import app.revanced.manager.domain.bundles.LocalPatchBundle +import app.revanced.manager.domain.bundles.PatchBundleSource +import app.revanced.manager.domain.bundles.PatchBundleSource.Extensions.asRemoteOrNull +import app.revanced.manager.domain.bundles.PatchBundleSource.Extensions.isDefault +import app.revanced.manager.domain.bundles.PatchBundleSource.Extensions.nameState +import app.revanced.manager.ui.component.ExceptionViewerDialog +import kotlinx.coroutines.launch + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun BundleInformationDialog( + onDismissRequest: () -> Unit, + onDeleteRequest: () -> Unit, + bundle: PatchBundleSource, + onUpdate: () -> Unit, +) { + val composableScope = rememberCoroutineScope() + var viewCurrentBundlePatches by remember { mutableStateOf(false) } + val isLocal = bundle is LocalPatchBundle + val state by bundle.state.collectAsStateWithLifecycle() + val props by remember(bundle) { + bundle.propsFlow() + }.collectAsStateWithLifecycle(null) + val patchCount = remember(state) { + state.patchBundleOrNull()?.patches?.size ?: 0 + } + + if (viewCurrentBundlePatches) { + BundlePatchesDialog( + onDismissRequest = { + viewCurrentBundlePatches = false + }, + bundle = bundle, + ) + } + + Dialog( + onDismissRequest = onDismissRequest, + properties = DialogProperties( + usePlatformDefaultWidth = false, + dismissOnBackPress = true + ) + ) { + val bundleName by bundle.nameState + + Scaffold( + topBar = { + BundleTopBar( + title = stringResource(R.string.patch_bundle_field), + onBackClick = onDismissRequest, + backIcon = { + Icon( + imageVector = Icons.AutoMirrored.Filled.ArrowBack, + contentDescription = stringResource(R.string.back) + ) + }, + actions = { + if (!bundle.isDefault) { + IconButton(onClick = onDeleteRequest) { + Icon( + Icons.Outlined.DeleteOutline, + stringResource(R.string.delete) + ) + } + } + if (!isLocal) { + IconButton(onClick = onUpdate) { + Icon( + Icons.Outlined.Update, + stringResource(R.string.refresh) + ) + } + } + } + ) + }, + ) { paddingValues -> + BaseBundleDialog( + modifier = Modifier.padding(paddingValues), + isDefault = bundle.isDefault, + name = bundleName, + remoteUrl = bundle.asRemoteOrNull?.endpoint, + patchCount = patchCount, + version = props?.version, + autoUpdate = props?.autoUpdate ?: false, + onAutoUpdateChange = { + composableScope.launch { + bundle.asRemoteOrNull?.setAutoUpdate(it) + } + }, + onPatchesClick = { + viewCurrentBundlePatches = true + }, + extraFields = { + (state as? PatchBundleSource.State.Failed)?.throwable?.let { + var showDialog by rememberSaveable { + mutableStateOf(false) + } + if (showDialog) ExceptionViewerDialog( + onDismiss = { showDialog = false }, + text = remember(it) { it.stackTraceToString() } + ) + + BundleListItem( + headlineText = stringResource(R.string.bundle_error), + supportingText = stringResource(R.string.bundle_error_description), + trailingContent = { + Icon( + Icons.AutoMirrored.Outlined.ArrowRight, + null + ) + }, + modifier = Modifier.clickable { showDialog = true } + ) + } + + if (state is PatchBundleSource.State.Missing && !isLocal) { + BundleListItem( + headlineText = stringResource(R.string.bundle_error), + supportingText = stringResource(R.string.bundle_not_downloaded), + modifier = Modifier.clickable(onClick = onUpdate) + ) + } + } + ) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/ui/component/bundle/BundleItem.kt b/app/src/main/java/app/revanced/manager/ui/component/bundle/BundleItem.kt new file mode 100644 index 0000000000..6f3ae914ee --- /dev/null +++ b/app/src/main/java/app/revanced/manager/ui/component/bundle/BundleItem.kt @@ -0,0 +1,110 @@ +package app.revanced.manager.ui.component.bundle + +import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.combinedClickable +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.size +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.ErrorOutline +import androidx.compose.material.icons.outlined.Warning +import androidx.compose.material3.Icon +import androidx.compose.material3.ListItem +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.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.pluralStringResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import app.revanced.manager.R +import app.revanced.manager.domain.bundles.PatchBundleSource +import app.revanced.manager.ui.component.haptics.HapticCheckbox +import app.revanced.manager.domain.bundles.PatchBundleSource.Extensions.nameState +import kotlinx.coroutines.flow.map + +@OptIn(ExperimentalFoundationApi::class) +@Composable +fun BundleItem( + bundle: PatchBundleSource, + onDelete: () -> Unit, + onUpdate: () -> Unit, + selectable: Boolean, + onSelect: () -> Unit, + isBundleSelected: Boolean, + toggleSelection: (Boolean) -> Unit, +) { + var viewBundleDialogPage by rememberSaveable { mutableStateOf(false) } + val state by bundle.state.collectAsStateWithLifecycle() + + val version by remember(bundle) { + bundle.propsFlow().map { props -> props?.version } + }.collectAsStateWithLifecycle(null) + val name by bundle.nameState + + if (viewBundleDialogPage) { + BundleInformationDialog( + onDismissRequest = { viewBundleDialogPage = false }, + onDeleteRequest = { + viewBundleDialogPage = false + onDelete() + }, + bundle = bundle, + onUpdate = onUpdate, + ) + } + + ListItem( + modifier = Modifier + .height(64.dp) + .fillMaxWidth() + .combinedClickable( + onClick = { viewBundleDialogPage = true }, + onLongClick = onSelect, + ), + leadingContent = if (selectable) { + { + HapticCheckbox( + checked = isBundleSelected, + onCheckedChange = toggleSelection, + ) + } + } else null, + + headlineContent = { Text(name) }, + supportingContent = { + state.patchBundleOrNull()?.patches?.size?.let { patchCount -> + Text(pluralStringResource(R.plurals.patch_count, patchCount, patchCount)) + } + }, + trailingContent = { + Row { + val icon = remember(state) { + when (state) { + is PatchBundleSource.State.Failed -> Icons.Outlined.ErrorOutline to R.string.bundle_error + is PatchBundleSource.State.Missing -> Icons.Outlined.Warning to R.string.bundle_missing + is PatchBundleSource.State.Loaded -> null + } + } + + icon?.let { (vector, description) -> + Icon( + vector, + contentDescription = stringResource(description), + modifier = Modifier.size(24.dp), + tint = MaterialTheme.colorScheme.error + ) + } + + version?.let { Text(text = it) } + } + }, + ) +} \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/ui/component/bundle/BundleListItem.kt b/app/src/main/java/app/revanced/manager/ui/component/bundle/BundleListItem.kt new file mode 100644 index 0000000000..48a1ac17ed --- /dev/null +++ b/app/src/main/java/app/revanced/manager/ui/component/bundle/BundleListItem.kt @@ -0,0 +1,33 @@ +package app.revanced.manager.ui.component.bundle + +import androidx.compose.material3.ListItem +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier + +@Composable +fun BundleListItem( + modifier: Modifier = Modifier, + headlineText: String, + supportingText: String = "", + trailingContent: @Composable (() -> Unit)? = null, +) { + ListItem( + headlineContent = { + Text( + text = headlineText, + style = MaterialTheme.typography.titleLarge + ) + }, + supportingContent = { + Text( + text = supportingText, + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.outline + ) + }, + trailingContent = trailingContent, + modifier = modifier + ) +} \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/ui/component/bundle/BundlePatchesDialog.kt b/app/src/main/java/app/revanced/manager/ui/component/bundle/BundlePatchesDialog.kt new file mode 100644 index 0000000000..9920194913 --- /dev/null +++ b/app/src/main/java/app/revanced/manager/ui/component/bundle/BundlePatchesDialog.kt @@ -0,0 +1,276 @@ +package app.revanced.manager.ui.component.bundle + +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.ArrowBack +import androidx.compose.material3.* +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.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import androidx.compose.ui.window.Dialog +import androidx.compose.ui.window.DialogProperties +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import app.revanced.manager.R +import app.revanced.manager.domain.bundles.PatchBundleSource +import app.revanced.manager.patcher.patch.PatchInfo +import app.revanced.manager.ui.component.ArrowButton +import app.revanced.manager.ui.component.LazyColumnWithScrollbar + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun BundlePatchesDialog( + onDismissRequest: () -> Unit, + bundle: PatchBundleSource, +) { + var showAllVersions by rememberSaveable { mutableStateOf(false) } + var showOptions by rememberSaveable { mutableStateOf(false) } + val state by bundle.state.collectAsStateWithLifecycle() + + Dialog( + onDismissRequest = onDismissRequest, + properties = DialogProperties( + usePlatformDefaultWidth = false, + dismissOnBackPress = true + ) + ) { + Scaffold( + topBar = { + BundleTopBar( + title = stringResource(R.string.bundle_patches), + onBackClick = onDismissRequest, + backIcon = { + Icon( + imageVector = Icons.AutoMirrored.Filled.ArrowBack, + contentDescription = stringResource(R.string.back) + ) + }, + ) + }, + ) { paddingValues -> + LazyColumnWithScrollbar( + modifier = Modifier + .fillMaxWidth() + .padding(paddingValues), + verticalArrangement = Arrangement.spacedBy(12.dp), + contentPadding = PaddingValues(16.dp) + ) { + state.patchBundleOrNull()?.let { bundle -> + items(bundle.patches) { patch -> + PatchItem( + patch, + showAllVersions, + onExpandVersions = { showAllVersions = !showAllVersions }, + showOptions, + onExpandOptions = { showOptions = !showOptions } + ) + } + } + } + } + } +} + +@OptIn(ExperimentalLayoutApi::class) +@Composable +fun PatchItem( + patch: PatchInfo, + expandVersions: Boolean, + onExpandVersions: () -> Unit, + expandOptions: Boolean, + onExpandOptions: () -> Unit +) { + ElevatedCard( + modifier = Modifier + .fillMaxWidth() + .then( + if (patch.options.isNullOrEmpty()) Modifier else Modifier + .clip(RoundedCornerShape(8.dp)) + .clickable(onClick = onExpandOptions), + ) + ) { + Column( + modifier = Modifier.padding(16.dp), + verticalArrangement = Arrangement.spacedBy(6.dp) + ) { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.Absolute.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = patch.name, + color = MaterialTheme.colorScheme.primary, + style = MaterialTheme.typography.titleLarge, + fontWeight = FontWeight.Bold + ) + + if (!patch.options.isNullOrEmpty()) { + ArrowButton(expanded = expandOptions, onClick = null) + } + } + patch.description?.let { + Text( + text = it, + style = MaterialTheme.typography.bodyMedium + ) + } + Column( + verticalArrangement = Arrangement.spacedBy(4.dp) + ) { + if (patch.compatiblePackages.isNullOrEmpty()) { + Row( + horizontalArrangement = Arrangement.spacedBy(4.dp), + verticalAlignment = Alignment.CenterVertically + ) { + PatchInfoChip( + text = "$PACKAGE_ICON ${stringResource(R.string.bundle_view_patches_any_package)}" + ) + PatchInfoChip( + text = "$VERSION_ICON ${stringResource(R.string.bundle_view_patches_any_version)}" + ) + } + } else { + patch.compatiblePackages.forEach { compatiblePackage -> + val packageName = compatiblePackage.packageName + val versions = compatiblePackage.versions.orEmpty().reversed() + + FlowRow( + horizontalArrangement = Arrangement.spacedBy(4.dp), + verticalArrangement = Arrangement.spacedBy(4.dp) + ) { + PatchInfoChip( + modifier = Modifier.align(Alignment.CenterVertically), + text = "$PACKAGE_ICON $packageName" + ) + + if (versions.isNotEmpty()) { + if (expandVersions) { + versions.forEach { version -> + PatchInfoChip( + modifier = Modifier.align(Alignment.CenterVertically), + text = "$VERSION_ICON $version" + ) + } + } else { + PatchInfoChip( + modifier = Modifier.align(Alignment.CenterVertically), + text = "$VERSION_ICON ${versions.first()}" + ) + } + if (versions.size > 1) { + PatchInfoChip( + onClick = onExpandVersions, + text = if (expandVersions) stringResource(R.string.less) else "+${versions.size - 1}" + ) + } + } + } + } + } + } + if (!patch.options.isNullOrEmpty()) { + AnimatedVisibility(visible = expandOptions) { + val options = patch.options + + Column { + options.forEachIndexed { i, option -> + OutlinedCard( + modifier = Modifier.fillMaxWidth(), + colors = CardColors( + containerColor = Color.Transparent, + contentColor = MaterialTheme.colorScheme.onSurface, + disabledContainerColor = Color.Transparent, + disabledContentColor = MaterialTheme.colorScheme.onSurface + ), shape = when { + options.size == 1 -> RoundedCornerShape(8.dp) + i == 0 -> RoundedCornerShape(topStart = 8.dp, topEnd = 8.dp) + i == options.lastIndex -> RoundedCornerShape( + bottomStart = 8.dp, + bottomEnd = 8.dp + ) + + else -> RoundedCornerShape(0.dp) + } + ) { + Column( + modifier = Modifier.padding(12.dp), + verticalArrangement = Arrangement.spacedBy(4.dp), + ) { + Text( + text = option.title, + style = MaterialTheme.typography.titleMedium, + color = MaterialTheme.colorScheme.primary + ) + Text( + text = option.description, + style = MaterialTheme.typography.bodyMedium + ) + } + } + } + } + } + } + } + } +} + +@Composable +fun PatchInfoChip( + modifier: Modifier = Modifier, + onClick: (() -> Unit)? = null, + text: String +) { + val shape = RoundedCornerShape(8.0.dp) + val cardModifier = if (onClick != null) { + Modifier + .clip(shape) + .clickable(onClick = onClick) + } else { + Modifier + } + + OutlinedCard( + modifier = modifier.then(cardModifier), + colors = CardColors( + containerColor = Color.Transparent, + contentColor = MaterialTheme.colorScheme.onSurface, + disabledContainerColor = Color.Transparent, + disabledContentColor = MaterialTheme.colorScheme.onSurface + ), + shape = shape, + border = BorderStroke(1.dp, MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.20f)) + ) { + Row( + modifier = Modifier.padding(8.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.Center + ) { + Text( + text, + overflow = TextOverflow.Ellipsis, + softWrap = false, + style = MaterialTheme.typography.labelLarge, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + } +} + +const val PACKAGE_ICON = "\uD83D\uDCE6" +const val VERSION_ICON = "\uD83C\uDFAF" \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/ui/component/bundle/BundleSelector.kt b/app/src/main/java/app/revanced/manager/ui/component/bundle/BundleSelector.kt new file mode 100644 index 0000000000..8ea55e22f4 --- /dev/null +++ b/app/src/main/java/app/revanced/manager/ui/component/bundle/BundleSelector.kt @@ -0,0 +1,76 @@ +package app.revanced.manager.ui.component.bundle + +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.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.ModalBottomSheet +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import app.revanced.manager.domain.bundles.PatchBundleSource +import app.revanced.manager.domain.bundles.PatchBundleSource.Extensions.nameState + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun BundleSelector(bundles: List<PatchBundleSource>, onFinish: (PatchBundleSource?) -> Unit) { + LaunchedEffect(bundles) { + if (bundles.size == 1) { + onFinish(bundles[0]) + } + } + + if (bundles.size < 2) { + return + } + + ModalBottomSheet( + onDismissRequest = { onFinish(null) } + ) { + Column( + modifier = Modifier.fillMaxWidth(), + verticalArrangement = Arrangement.spacedBy(12.dp) + ) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.Center, + modifier = Modifier + .height(48.dp) + .fillMaxWidth() + ) { + Text( + text = "Select bundle", + style = MaterialTheme.typography.titleLarge, + color = MaterialTheme.colorScheme.onSurface + ) + } + bundles.forEach { + val name by it.nameState + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.Center, + modifier = Modifier + .height(48.dp) + .fillMaxWidth() + .clickable { + onFinish(it) + } + ) { + Text( + name, + style = MaterialTheme.typography.titleMedium, + color = MaterialTheme.colorScheme.onSurface + ) + } + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/ui/component/bundle/BundleTopBar.kt b/app/src/main/java/app/revanced/manager/ui/component/bundle/BundleTopBar.kt new file mode 100644 index 0000000000..543d2d3465 --- /dev/null +++ b/app/src/main/java/app/revanced/manager/ui/component/bundle/BundleTopBar.kt @@ -0,0 +1,46 @@ +package app.revanced.manager.ui.component.bundle + +import androidx.compose.foundation.layout.RowScope +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar +import androidx.compose.material3.TopAppBarDefaults +import androidx.compose.material3.TopAppBarScrollBehavior +import androidx.compose.material3.surfaceColorAtElevation +import androidx.compose.runtime.Composable +import androidx.compose.ui.unit.dp + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun BundleTopBar( + title: String, + onBackClick: (() -> Unit)? = null, + actions: @Composable (RowScope.() -> Unit) = {}, + scrollBehavior: TopAppBarScrollBehavior? = null, + backIcon: @Composable () -> Unit, +) { + val containerColor = MaterialTheme.colorScheme.surfaceColorAtElevation(3.0.dp) + + TopAppBar( + title = { + Text( + text = title, + style = MaterialTheme.typography.titleLarge + ) + }, + scrollBehavior = scrollBehavior, + navigationIcon = { + if (onBackClick != null) { + IconButton(onClick = onBackClick) { + backIcon() + } + } + }, + actions = actions, + colors = TopAppBarDefaults.topAppBarColors( + containerColor = containerColor + ) + ) +} \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/ui/component/bundle/ImportBundleDialog.kt b/app/src/main/java/app/revanced/manager/ui/component/bundle/ImportBundleDialog.kt new file mode 100644 index 0000000000..37d9ed1ad0 --- /dev/null +++ b/app/src/main/java/app/revanced/manager/ui/component/bundle/ImportBundleDialog.kt @@ -0,0 +1,237 @@ +package app.revanced.manager.ui.component.bundle + +import android.net.Uri +import androidx.activity.compose.rememberLauncherForActivityResult +import androidx.activity.result.contract.ActivityResultContracts +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.padding +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Topic +import androidx.compose.material3.* +import androidx.compose.runtime.* +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.semantics.Role +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import app.revanced.manager.R +import app.revanced.manager.ui.component.AlertDialogExtended +import app.revanced.manager.ui.component.TextHorizontalPadding +import app.revanced.manager.ui.component.haptics.HapticCheckbox +import app.revanced.manager.ui.component.haptics.HapticRadioButton +import app.revanced.manager.ui.model.BundleType +import app.revanced.manager.util.BIN_MIMETYPE +import app.revanced.manager.util.transparentListItemColors + +@Composable +fun ImportPatchBundleDialog( + onDismiss: () -> Unit, + onRemoteSubmit: (String, Boolean) -> Unit, + onLocalSubmit: (Uri) -> Unit +) { + var currentStep by rememberSaveable { mutableIntStateOf(0) } + var bundleType by rememberSaveable { mutableStateOf(BundleType.Remote) } + var patchBundle by rememberSaveable { mutableStateOf<Uri?>(null) } + var remoteUrl by rememberSaveable { mutableStateOf("") } + var autoUpdate by rememberSaveable { mutableStateOf(false) } + + val patchActivityLauncher = + rememberLauncherForActivityResult(ActivityResultContracts.GetContent()) { uri -> + uri?.let { patchBundle = it } + } + + fun launchPatchActivity() { + patchActivityLauncher.launch(BIN_MIMETYPE) + } + + val steps = listOf<@Composable () -> Unit>( + { + SelectBundleTypeStep(bundleType) { selectedType -> + bundleType = selectedType + } + }, + { + ImportBundleStep( + bundleType, + patchBundle, + remoteUrl, + autoUpdate, + { launchPatchActivity() }, + { remoteUrl = it }, + { autoUpdate = it } + ) + } + ) + + val inputsAreValid by remember { + derivedStateOf { + (bundleType == BundleType.Local && patchBundle != null) || + (bundleType == BundleType.Remote && remoteUrl.isNotEmpty()) + } + } + + AlertDialogExtended( + onDismissRequest = onDismiss, + title = { + Text(stringResource(if (currentStep == 0) R.string.select else R.string.add_patch_bundle)) + }, + text = { + steps[currentStep]() + }, + confirmButton = { + if (currentStep == steps.lastIndex) { + TextButton( + enabled = inputsAreValid, + onClick = { + when (bundleType) { + BundleType.Local -> patchBundle?.let(onLocalSubmit) + BundleType.Remote -> onRemoteSubmit(remoteUrl, autoUpdate) + } + } + ) { + Text(stringResource(R.string.add)) + } + } else { + TextButton(onClick = { currentStep++ }) { + Text(stringResource(R.string.next)) + } + } + }, + dismissButton = { + if (currentStep > 0) { + TextButton(onClick = { currentStep-- }) { + Text(stringResource(R.string.back)) + } + } else { + TextButton(onClick = onDismiss) { + Text(stringResource(R.string.cancel)) + } + } + }, + textHorizontalPadding = PaddingValues(0.dp) + ) +} + +@Composable +fun SelectBundleTypeStep( + bundleType: BundleType, + onBundleTypeSelected: (BundleType) -> Unit +) { + Column( + verticalArrangement = Arrangement.spacedBy(24.dp) + ) { + Text( + modifier = Modifier.padding(horizontal = 24.dp), + text = stringResource(R.string.select_bundle_type_dialog_description) + ) + Column { + ListItem( + modifier = Modifier.clickable( + role = Role.RadioButton, + onClick = { onBundleTypeSelected(BundleType.Remote) } + ), + headlineContent = { Text(stringResource(R.string.enter_url)) }, + overlineContent = { Text(stringResource(R.string.recommended)) }, + supportingContent = { Text(stringResource(R.string.remote_bundle_description)) }, + leadingContent = { + HapticRadioButton( + selected = bundleType == BundleType.Remote, + onClick = null + ) + }, + colors = transparentListItemColors + ) + HorizontalDivider(modifier = Modifier.padding(horizontal = 16.dp)) + ListItem( + modifier = Modifier.clickable( + role = Role.RadioButton, + onClick = { onBundleTypeSelected(BundleType.Local) } + ), + headlineContent = { Text(stringResource(R.string.select_from_storage)) }, + supportingContent = { Text(stringResource(R.string.local_bundle_description)) }, + overlineContent = { }, + leadingContent = { + HapticRadioButton( + selected = bundleType == BundleType.Local, + onClick = null + ) + }, + colors = transparentListItemColors + ) + } + } +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun ImportBundleStep( + bundleType: BundleType, + patchBundle: Uri?, + remoteUrl: String, + autoUpdate: Boolean, + launchPatchActivity: () -> Unit, + onRemoteUrlChange: (String) -> Unit, + onAutoUpdateChange: (Boolean) -> Unit +) { + Column { + when (bundleType) { + BundleType.Local -> { + Column( + modifier = Modifier.padding(horizontal = 8.dp) + ) { + ListItem( + headlineContent = { + Text(stringResource(R.string.patch_bundle_field)) + }, + supportingContent = { Text(stringResource(if (patchBundle != null) R.string.file_field_set else R.string.file_field_not_set)) }, + trailingContent = { + IconButton(onClick = launchPatchActivity) { + Icon(imageVector = Icons.Default.Topic, contentDescription = null) + } + }, + modifier = Modifier.clickable { launchPatchActivity() }, + colors = transparentListItemColors + ) + } + } + + BundleType.Remote -> { + Column( + modifier = Modifier.padding(TextHorizontalPadding) + ) { + OutlinedTextField( + value = remoteUrl, + onValueChange = onRemoteUrlChange, + label = { Text(stringResource(R.string.bundle_url)) } + ) + } + Column( + modifier = Modifier.padding(horizontal = 8.dp) + ) { + ListItem( + modifier = Modifier.clickable( + role = Role.Checkbox, + onClick = { onAutoUpdateChange(!autoUpdate) } + ), + headlineContent = { Text(stringResource(R.string.auto_update)) }, + leadingContent = { + CompositionLocalProvider(LocalMinimumInteractiveComponentSize provides Dp.Unspecified) { + HapticCheckbox( + checked = autoUpdate, + onCheckedChange = { + onAutoUpdateChange(!autoUpdate) + } + ) + } + }, + colors = transparentListItemColors + ) + } + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/ui/component/haptics/HapticCheckbox.kt b/app/src/main/java/app/revanced/manager/ui/component/haptics/HapticCheckbox.kt new file mode 100644 index 0000000000..fb5453f960 --- /dev/null +++ b/app/src/main/java/app/revanced/manager/ui/component/haptics/HapticCheckbox.kt @@ -0,0 +1,30 @@ +package app.revanced.manager.ui.component.haptics + +import android.view.HapticFeedbackConstants +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.material3.Checkbox +import androidx.compose.material3.CheckboxColors +import androidx.compose.material3.CheckboxDefaults +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import app.revanced.manager.util.withHapticFeedback + +@Composable +fun HapticCheckbox( + checked: Boolean, + onCheckedChange: ((Boolean) -> Unit)?, + modifier: Modifier = Modifier, + enabled: Boolean = true, + colors: CheckboxColors = CheckboxDefaults.colors(), + interactionSource: MutableInteractionSource = remember { MutableInteractionSource() } +) { + Checkbox( + checked = checked, + onCheckedChange = onCheckedChange?.withHapticFeedback(HapticFeedbackConstants.CLOCK_TICK), + modifier = modifier, + enabled = enabled, + colors = colors, + interactionSource = interactionSource + ) +} diff --git a/app/src/main/java/app/revanced/manager/ui/component/haptics/HapticExtendedFloatingActionButton.kt b/app/src/main/java/app/revanced/manager/ui/component/haptics/HapticExtendedFloatingActionButton.kt new file mode 100644 index 0000000000..4fc6ad30a8 --- /dev/null +++ b/app/src/main/java/app/revanced/manager/ui/component/haptics/HapticExtendedFloatingActionButton.kt @@ -0,0 +1,41 @@ +package app.revanced.manager.ui.component.haptics + +import android.view.HapticFeedbackConstants +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.material3.ExtendedFloatingActionButton +import androidx.compose.material3.FloatingActionButtonDefaults +import androidx.compose.material3.FloatingActionButtonElevation +import androidx.compose.material3.contentColorFor +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.Shape +import app.revanced.manager.util.withHapticFeedback + +@Composable +fun HapticExtendedFloatingActionButton ( + text: @Composable () -> Unit, + icon: @Composable () -> Unit, + onClick: () -> Unit, + modifier: Modifier = Modifier, + expanded: Boolean = true, + shape: Shape = FloatingActionButtonDefaults.extendedFabShape, + containerColor: Color = FloatingActionButtonDefaults.containerColor, + contentColor: Color = contentColorFor(containerColor), + elevation: FloatingActionButtonElevation = FloatingActionButtonDefaults.elevation(), + interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, +) { + ExtendedFloatingActionButton( + text = text, + icon = icon, + onClick = onClick.withHapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY), + modifier = modifier, + expanded = expanded, + shape = shape, + containerColor = containerColor, + contentColor = contentColor, + elevation = elevation, + interactionSource = interactionSource + ) +} diff --git a/app/src/main/java/app/revanced/manager/ui/component/haptics/HapticFloatingActionButton.kt b/app/src/main/java/app/revanced/manager/ui/component/haptics/HapticFloatingActionButton.kt new file mode 100644 index 0000000000..f4a2e15333 --- /dev/null +++ b/app/src/main/java/app/revanced/manager/ui/component/haptics/HapticFloatingActionButton.kt @@ -0,0 +1,37 @@ +package app.revanced.manager.ui.component.haptics + +import android.view.HapticFeedbackConstants +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.material3.FloatingActionButton +import androidx.compose.material3.FloatingActionButtonDefaults +import androidx.compose.material3.FloatingActionButtonElevation +import androidx.compose.material3.contentColorFor +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.Shape +import app.revanced.manager.util.withHapticFeedback + +@Composable +fun HapticFloatingActionButton ( + onClick: () -> Unit, + modifier: Modifier = Modifier, + shape: Shape = FloatingActionButtonDefaults.shape, + containerColor: Color = FloatingActionButtonDefaults.containerColor, + contentColor: Color = contentColorFor(containerColor), + elevation: FloatingActionButtonElevation = FloatingActionButtonDefaults.elevation(), + interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, + content: @Composable () -> Unit, +) { + FloatingActionButton( + onClick = onClick.withHapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY), + modifier = modifier, + shape = shape, + containerColor = containerColor, + contentColor = contentColor, + elevation = elevation, + interactionSource = interactionSource, + content = content + ) +} diff --git a/app/src/main/java/app/revanced/manager/ui/component/haptics/HapticRadioButton.kt b/app/src/main/java/app/revanced/manager/ui/component/haptics/HapticRadioButton.kt new file mode 100644 index 0000000000..63a9e58278 --- /dev/null +++ b/app/src/main/java/app/revanced/manager/ui/component/haptics/HapticRadioButton.kt @@ -0,0 +1,38 @@ +package app.revanced.manager.ui.component.haptics + +import android.view.HapticFeedbackConstants +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.material3.RadioButton +import androidx.compose.material3.RadioButtonColors +import androidx.compose.material3.RadioButtonDefaults +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalView + +@Composable +fun HapticRadioButton( + selected: Boolean, + onClick: (() -> Unit)?, + modifier: Modifier = Modifier, + enabled: Boolean = true, + colors: RadioButtonColors = RadioButtonDefaults.colors(), + interactionSource: MutableInteractionSource = remember { MutableInteractionSource() } +) { + val view = LocalView.current + + RadioButton( + selected = selected, + onClick = onClick?.let { + { + // Perform haptic feedback + if (!selected) view.performHapticFeedback(HapticFeedbackConstants.CLOCK_TICK) + it() + } + }, + modifier = modifier, + enabled = enabled, + colors = colors, + interactionSource = interactionSource + ) +} diff --git a/app/src/main/java/app/revanced/manager/ui/component/haptics/HapticSwitch.kt b/app/src/main/java/app/revanced/manager/ui/component/haptics/HapticSwitch.kt new file mode 100644 index 0000000000..c2491397f6 --- /dev/null +++ b/app/src/main/java/app/revanced/manager/ui/component/haptics/HapticSwitch.kt @@ -0,0 +1,41 @@ +package app.revanced.manager.ui.component.haptics + +import android.os.Build +import android.view.HapticFeedbackConstants +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.material3.Switch +import androidx.compose.material3.SwitchColors +import androidx.compose.material3.SwitchDefaults +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier + +@Composable +fun HapticSwitch( + checked: Boolean, + onCheckedChange: (Boolean) -> Unit, + modifier: Modifier = Modifier, + thumbContent: (@Composable () -> Unit)? = null, + enabled: Boolean = true, + colors: SwitchColors = SwitchDefaults.colors(), + interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, +) { + Switch( + checked = checked, + onCheckedChange = { newChecked -> + val useNewConstants = Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE + when { + newChecked && useNewConstants -> HapticFeedbackConstants.TOGGLE_ON + newChecked -> HapticFeedbackConstants.VIRTUAL_KEY + !newChecked && useNewConstants -> HapticFeedbackConstants.TOGGLE_OFF + !newChecked -> HapticFeedbackConstants.CLOCK_TICK + } + onCheckedChange(newChecked) + }, + modifier = modifier, + thumbContent = thumbContent, + enabled = enabled, + colors = colors, + interactionSource = interactionSource, + ) +} diff --git a/app/src/main/java/app/revanced/manager/ui/component/haptics/HapticTab.kt b/app/src/main/java/app/revanced/manager/ui/component/haptics/HapticTab.kt new file mode 100644 index 0000000000..d0676951ea --- /dev/null +++ b/app/src/main/java/app/revanced/manager/ui/component/haptics/HapticTab.kt @@ -0,0 +1,36 @@ +package app.revanced.manager.ui.component.haptics + +import android.view.HapticFeedbackConstants +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.material3.LocalContentColor +import androidx.compose.material3.Tab +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import app.revanced.manager.util.withHapticFeedback + +@Composable +fun HapticTab ( + selected: Boolean, + onClick: () -> Unit, + modifier: Modifier = Modifier, + enabled: Boolean = true, + text: @Composable (() -> Unit)? = null, + icon: @Composable (() -> Unit)? = null, + selectedContentColor: Color = LocalContentColor.current, + unselectedContentColor: Color = selectedContentColor, + interactionSource: MutableInteractionSource = remember { MutableInteractionSource() } +) { + Tab( + selected = selected, + onClick = onClick.withHapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY), + modifier = modifier, + enabled = enabled, + text = text, + icon = icon, + selectedContentColor = selectedContentColor, + unselectedContentColor = unselectedContentColor, + interactionSource = interactionSource + ) +} \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/ui/component/patcher/InstallPickerDialog.kt b/app/src/main/java/app/revanced/manager/ui/component/patcher/InstallPickerDialog.kt new file mode 100644 index 0000000000..b86124d918 --- /dev/null +++ b/app/src/main/java/app/revanced/manager/ui/component/patcher/InstallPickerDialog.kt @@ -0,0 +1,61 @@ +package app.revanced.manager.ui.component.patcher + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Column +import androidx.compose.material3.* +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.Modifier +import androidx.compose.ui.res.stringResource +import app.revanced.manager.R +import app.revanced.manager.data.room.apps.installed.InstallType +import app.revanced.manager.ui.component.haptics.HapticRadioButton +import app.revanced.manager.util.transparentListItemColors + +@Composable +fun InstallPickerDialog( + onDismiss: () -> Unit, + onConfirm: (InstallType) -> Unit +) { + var selectedInstallType by rememberSaveable { mutableStateOf(InstallType.DEFAULT) } + + AlertDialog( + onDismissRequest = onDismiss, + dismissButton = { + TextButton(onClick = onDismiss) { + Text(stringResource(R.string.cancel)) + } + }, + confirmButton = { + Button( + onClick = { + onConfirm(selectedInstallType) + onDismiss() + } + ) { + Text(stringResource(R.string.install_app)) + } + }, + title = { Text(stringResource(R.string.select_install_type)) }, + text = { + Column { + InstallType.entries.forEach { + ListItem( + modifier = Modifier.clickable { selectedInstallType = it }, + leadingContent = { + HapticRadioButton( + selected = selectedInstallType == it, + onClick = null + ) + }, + headlineContent = { Text(stringResource(it.stringResource)) }, + colors = transparentListItemColors + ) + } + } + } + ) +} \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/ui/component/patcher/Steps.kt b/app/src/main/java/app/revanced/manager/ui/component/patcher/Steps.kt new file mode 100644 index 0000000000..d6c78263c2 --- /dev/null +++ b/app/src/main/java/app/revanced/manager/ui/component/patcher/Steps.kt @@ -0,0 +1,251 @@ +package app.revanced.manager.ui.component.patcher + +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.animateColorAsState +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Cancel +import androidx.compose.material.icons.filled.CheckCircle +import androidx.compose.material.icons.outlined.Circle +import androidx.compose.material3.Icon +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.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.semantics.contentDescription +import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import app.revanced.manager.R +import app.revanced.manager.ui.component.ArrowButton +import app.revanced.manager.ui.component.LoadingIndicator +import app.revanced.manager.ui.model.ProgressKey +import app.revanced.manager.ui.model.State +import app.revanced.manager.ui.model.Step +import app.revanced.manager.ui.model.StepCategory +import app.revanced.manager.ui.model.StepProgressProvider +import java.util.Locale +import kotlin.math.floor + +// Credits: https://github.com/Aliucord/AliucordManager/blob/main/app/src/main/kotlin/com/aliucord/manager/ui/component/installer/InstallGroup.kt +@Composable +fun Steps( + category: StepCategory, + steps: List<Step>, + stepCount: Pair<Int, Int>? = null, + stepProgressProvider: StepProgressProvider +) { + var expanded by rememberSaveable { mutableStateOf(true) } + + val categoryColor by animateColorAsState( + if (expanded) MaterialTheme.colorScheme.surfaceContainerHigh else Color.Transparent, + label = "category" + ) + + val cardColor by animateColorAsState( + if (expanded) MaterialTheme.colorScheme.surfaceContainer else Color.Transparent, + label = "card" + ) + + val state = remember(steps) { + when { + steps.all { it.state == State.COMPLETED } -> State.COMPLETED + steps.any { it.state == State.FAILED } -> State.FAILED + steps.any { it.state == State.RUNNING } -> State.RUNNING + else -> State.WAITING + } + } + + Column( + modifier = Modifier + .clip(RoundedCornerShape(16.dp)) + .fillMaxWidth() + .background(cardColor) + ) { + Row( + modifier = Modifier + .clip(RoundedCornerShape(16.dp)) + .clickable { expanded = !expanded } + .background(categoryColor) + ) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(12.dp), + modifier = Modifier.padding(16.dp) + ) { + StepIcon(state = state, size = 24.dp) + + Text(stringResource(category.displayName)) + + Spacer(modifier = Modifier.weight(1f)) + + val stepProgress = remember(stepCount, steps) { + stepCount?.let { (current, total) -> "$current/$total" } + ?: "${steps.count { it.state == State.COMPLETED }}/${steps.size}" + } + + Text( + text = stepProgress, + style = MaterialTheme.typography.labelSmall + ) + + ArrowButton(modifier = Modifier.size(24.dp), expanded = expanded, onClick = null) + } + } + + AnimatedVisibility(visible = expanded) { + Column( + modifier = Modifier.fillMaxWidth() + ) { + steps.forEach { step -> + val (progress, progressText) = when (step.progressKey) { + null -> null + ProgressKey.DOWNLOAD -> stepProgressProvider.downloadProgress?.let { (downloaded, total) -> + if (total != null) downloaded.toFloat() / total.toFloat() to "${downloaded.megaBytes}/${total.megaBytes} MB" + else null to "${downloaded.megaBytes} MB" + } + } ?: (null to null) + + SubStep( + name = step.name, + state = step.state, + message = step.message, + progress = progress, + progressText = progressText + ) + } + } + } + } +} + +@Composable +fun SubStep( + name: String, + state: State, + message: String? = null, + progress: Float? = null, + progressText: String? = null +) { + var messageExpanded by rememberSaveable { mutableStateOf(true) } + + Column( + modifier = Modifier + .run { + if (message != null) + clickable { messageExpanded = !messageExpanded } + else this + } + ) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(12.dp), + modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp) + ) { + Box( + modifier = Modifier.size(24.dp), + contentAlignment = Alignment.Center + ) { + StepIcon(state, progress, size = 20.dp) + } + + Text( + text = name, + style = MaterialTheme.typography.titleSmall, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + modifier = Modifier.weight(1f, true), + ) + + when { + message != null -> Box( + modifier = Modifier.size(24.dp), + contentAlignment = Alignment.Center + ) { + ArrowButton( + modifier = Modifier.size(20.dp), + expanded = messageExpanded, + onClick = null + ) + } + + progressText != null -> Text( + progressText, + style = MaterialTheme.typography.labelSmall + ) + } + } + + AnimatedVisibility(visible = messageExpanded && message != null) { + Text( + text = message.orEmpty(), + style = MaterialTheme.typography.labelMedium, + color = MaterialTheme.colorScheme.secondary, + modifier = Modifier.padding(horizontal = 52.dp, vertical = 8.dp) + ) + } + } +} + +@Composable +fun StepIcon(state: State, progress: Float? = null, size: Dp) { + val strokeWidth = Dp(floor(size.value / 10) + 1) + + when (state) { + State.COMPLETED -> Icon( + Icons.Filled.CheckCircle, + contentDescription = stringResource(R.string.step_completed), + tint = MaterialTheme.colorScheme.surfaceTint, + modifier = Modifier.size(size) + ) + + State.FAILED -> Icon( + Icons.Filled.Cancel, + contentDescription = stringResource(R.string.step_failed), + tint = MaterialTheme.colorScheme.error, + modifier = Modifier.size(size) + ) + + State.WAITING -> Icon( + Icons.Outlined.Circle, + contentDescription = stringResource(R.string.step_waiting), + tint = MaterialTheme.colorScheme.surfaceVariant, + modifier = Modifier.size(size) + ) + + State.RUNNING -> + LoadingIndicator( + modifier = stringResource(R.string.step_running).let { description -> + Modifier + .size(size) + .semantics { + contentDescription = description + } + }, + progress = { progress }, + strokeWidth = strokeWidth + ) + } +} + +private val Long.megaBytes get() = "%.1f".format(locale = Locale.ROOT, toDouble() / 1_000_000) \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/ui/component/patches/OptionFields.kt b/app/src/main/java/app/revanced/manager/ui/component/patches/OptionFields.kt new file mode 100644 index 0000000000..06438176b8 --- /dev/null +++ b/app/src/main/java/app/revanced/manager/ui/component/patches/OptionFields.kt @@ -0,0 +1,657 @@ +package app.revanced.manager.ui.component.patches + +import android.app.Application +import android.os.Parcelable +import androidx.activity.compose.rememberLauncherForActivityResult +import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.LocalIndication +import androidx.compose.foundation.clickable +import androidx.compose.foundation.combinedClickable +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.lazy.itemsIndexed +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.ArrowBack +import androidx.compose.material.icons.filled.Close +import androidx.compose.material.icons.filled.DragHandle +import androidx.compose.material.icons.outlined.* +import androidx.compose.material3.* +import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisallowComposableCalls +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateListOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.runtime.toMutableStateList +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.pluralStringResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontStyle +import androidx.compose.ui.unit.dp +import androidx.compose.ui.window.DialogProperties +import app.revanced.manager.R +import app.revanced.manager.data.platform.Filesystem +import app.revanced.manager.patcher.patch.Option +import app.revanced.manager.ui.component.* +import app.revanced.manager.ui.component.haptics.HapticExtendedFloatingActionButton +import app.revanced.manager.ui.component.haptics.HapticRadioButton +import app.revanced.manager.ui.component.haptics.HapticSwitch +import app.revanced.manager.util.isScrollingUp +import app.revanced.manager.util.mutableStateSetOf +import app.revanced.manager.util.saver.snapshotStateListSaver +import app.revanced.manager.util.saver.snapshotStateSetSaver +import app.revanced.manager.util.toast +import app.revanced.manager.util.transparentListItemColors +import kotlinx.parcelize.Parcelize +import org.koin.compose.koinInject +import org.koin.core.component.KoinComponent +import org.koin.core.component.get +import sh.calvin.reorderable.ReorderableItem +import sh.calvin.reorderable.rememberReorderableLazyColumnState +import java.io.Serializable +import kotlin.random.Random +import kotlin.reflect.typeOf +import androidx.compose.ui.window.Dialog as ComposeDialog + +private class OptionEditorScope<T : Any>( + private val editor: OptionEditor<T>, + val option: Option<T>, + val openDialog: () -> Unit, + val dismissDialog: () -> Unit, + val value: T?, + val setValue: (T?) -> Unit, +) { + fun submitDialog(value: T?) { + setValue(value) + dismissDialog() + } + + fun clickAction() = editor.clickAction(this) + + @Composable + fun ListItemTrailingContent() = editor.ListItemTrailingContent(this) + + @Composable + fun Dialog() = editor.Dialog(this) +} + +private interface OptionEditor<T : Any> { + fun clickAction(scope: OptionEditorScope<T>) = scope.openDialog() + + @Composable + fun ListItemTrailingContent(scope: OptionEditorScope<T>) { + IconButton(onClick = { clickAction(scope) }) { + Icon(Icons.Outlined.Edit, stringResource(R.string.edit)) + } + } + + @Composable + fun Dialog(scope: OptionEditorScope<T>) +} + +private inline fun <reified T : Serializable> OptionEditor<T>.toMapEditorElements() = arrayOf( + typeOf<T>() to this, + typeOf<List<T>>() to ListOptionEditor(this) +) + +private val optionEditors = mapOf( + *BooleanOptionEditor.toMapEditorElements(), + *StringOptionEditor.toMapEditorElements(), + *IntOptionEditor.toMapEditorElements(), + *LongOptionEditor.toMapEditorElements(), + *FloatOptionEditor.toMapEditorElements() +) + +@Composable +private inline fun <T : Any> WithOptionEditor( + editor: OptionEditor<T>, + option: Option<T>, + value: T?, + noinline setValue: (T?) -> Unit, + crossinline onDismissDialog: @DisallowComposableCalls () -> Unit = {}, + block: OptionEditorScope<T>.() -> Unit +) { + var showDialog by rememberSaveable { mutableStateOf(false) } + val scope = remember(editor, option, value, setValue) { + OptionEditorScope( + editor, + option, + openDialog = { showDialog = true }, + dismissDialog = { + showDialog = false + onDismissDialog() + }, + value, + setValue + ) + } + + if (showDialog) scope.Dialog() + + scope.block() +} + +@Composable +fun <T : Any> OptionItem( + option: Option<T>, + value: T?, + setValue: (T?) -> Unit, +) { + val editor = remember(option.type, option.presets) { + @Suppress("UNCHECKED_CAST") + val baseOptionEditor = + optionEditors.getOrDefault(option.type, UnknownTypeEditor) as OptionEditor<T> + + if (option.type != typeOf<Boolean>() && option.presets != null) PresetOptionEditor( + baseOptionEditor + ) + else baseOptionEditor + } + + WithOptionEditor(editor, option, value, setValue) { + ListItem( + modifier = Modifier.clickable(onClick = ::clickAction), + headlineContent = { Text(option.title) }, + supportingContent = { + Column { + Text(option.description) + if (option.required && value == null) Text( + stringResource(R.string.option_required), + color = MaterialTheme.colorScheme.error + ) + } + }, + trailingContent = { ListItemTrailingContent() } + ) + } +} + +private object StringOptionEditor : OptionEditor<String> { + @Composable + override fun Dialog(scope: OptionEditorScope<String>) { + var showFileDialog by rememberSaveable { mutableStateOf(false) } + var fieldValue by rememberSaveable(scope.value) { + mutableStateOf(scope.value.orEmpty()) + } + val validatorFailed by remember { + derivedStateOf { !scope.option.validator(fieldValue) } + } + + val fs: Filesystem = koinInject() + val (contract, permissionName) = fs.permissionContract() + val permissionLauncher = rememberLauncherForActivityResult(contract = contract) { + showFileDialog = it + } + + if (showFileDialog) { + PathSelectorDialog( + root = fs.externalFilesDir() + ) { + showFileDialog = false + it?.let { path -> + fieldValue = path.toString() + } + } + } + + AlertDialog( + onDismissRequest = scope.dismissDialog, + title = { Text(scope.option.title) }, + text = { + OutlinedTextField( + value = fieldValue, + onValueChange = { fieldValue = it }, + placeholder = { + Text(stringResource(R.string.dialog_input_placeholder)) + }, + isError = validatorFailed, + supportingText = { + if (validatorFailed) { + Text( + stringResource(R.string.input_dialog_value_invalid), + modifier = Modifier.fillMaxWidth(), + color = MaterialTheme.colorScheme.error + ) + } + }, + trailingIcon = { + var showDropdownMenu by rememberSaveable { mutableStateOf(false) } + IconButton( + onClick = { showDropdownMenu = true } + ) { + Icon( + Icons.Outlined.MoreVert, + stringResource(R.string.string_option_menu_description) + ) + } + + DropdownMenu( + expanded = showDropdownMenu, + onDismissRequest = { showDropdownMenu = false } + ) { + DropdownMenuItem( + leadingIcon = { + Icon(Icons.Outlined.Folder, null) + }, + text = { + Text(stringResource(R.string.path_selector)) + }, + onClick = { + showDropdownMenu = false + if (fs.hasStoragePermission()) { + showFileDialog = true + } else { + permissionLauncher.launch(permissionName) + } + } + ) + } + } + ) + }, + confirmButton = { + TextButton( + enabled = !validatorFailed, + onClick = { scope.submitDialog(fieldValue) }) { + Text(stringResource(R.string.save)) + } + }, + dismissButton = { + TextButton(onClick = scope.dismissDialog) { + Text(stringResource(R.string.cancel)) + } + }, + ) + } +} + +private abstract class NumberOptionEditor<T : Number> : OptionEditor<T> { + @Composable + protected abstract fun NumberDialog( + title: String, + current: T?, + validator: (T?) -> Boolean, + onSubmit: (T?) -> Unit + ) + + @Composable + override fun Dialog(scope: OptionEditorScope<T>) { + NumberDialog(scope.option.title, scope.value, scope.option.validator) { + if (it == null) return@NumberDialog scope.dismissDialog() + + scope.submitDialog(it) + } + } +} + +private object IntOptionEditor : NumberOptionEditor<Int>() { + @Composable + override fun NumberDialog( + title: String, + current: Int?, + validator: (Int?) -> Boolean, + onSubmit: (Int?) -> Unit + ) = IntInputDialog(current, title, validator, onSubmit) +} + +private object LongOptionEditor : NumberOptionEditor<Long>() { + @Composable + override fun NumberDialog( + title: String, + current: Long?, + validator: (Long?) -> Boolean, + onSubmit: (Long?) -> Unit + ) = LongInputDialog(current, title, validator, onSubmit) +} + +private object FloatOptionEditor : NumberOptionEditor<Float>() { + @Composable + override fun NumberDialog( + title: String, + current: Float?, + validator: (Float?) -> Boolean, + onSubmit: (Float?) -> Unit + ) = FloatInputDialog(current, title, validator, onSubmit) +} + +private object BooleanOptionEditor : OptionEditor<Boolean> { + override fun clickAction(scope: OptionEditorScope<Boolean>) { + scope.setValue(!scope.current) + } + + @Composable + override fun ListItemTrailingContent(scope: OptionEditorScope<Boolean>) { + HapticSwitch(checked = scope.current, onCheckedChange = scope.setValue) + } + + @Composable + override fun Dialog(scope: OptionEditorScope<Boolean>) { + } + + private val OptionEditorScope<Boolean>.current get() = value ?: false +} + +private object UnknownTypeEditor : OptionEditor<Any>, KoinComponent { + override fun clickAction(scope: OptionEditorScope<Any>) = + get<Application>().toast("Unknown type: ${scope.option.type}") + + @Composable + override fun Dialog(scope: OptionEditorScope<Any>) { + } +} + +/** + * A wrapper for [OptionEditor]s that shows selectable presets. + * + * @param innerEditor The [OptionEditor] for [T]. + */ +private class PresetOptionEditor<T : Any>(private val innerEditor: OptionEditor<T>) : + OptionEditor<T> { + @Composable + override fun Dialog(scope: OptionEditorScope<T>) { + var selectedPreset by rememberSaveable(scope.value, scope.option.presets) { + val presets = scope.option.presets!! + + mutableStateOf(presets.entries.find { it.value == scope.value }?.key) + } + + WithOptionEditor( + innerEditor, + scope.option, + scope.value, + scope.setValue, + onDismissDialog = scope.dismissDialog + ) inner@{ + var hidePresetsDialog by rememberSaveable { + mutableStateOf(false) + } + if (hidePresetsDialog) return@inner + + // TODO: add a divider for scrollable content + AlertDialogExtended( + onDismissRequest = scope.dismissDialog, + confirmButton = { + TextButton( + onClick = { + if (selectedPreset != null) scope.submitDialog( + scope.option.presets?.get( + selectedPreset + ) + ) + else { + this@inner.openDialog() + // Hide the presets dialog so it doesn't show up in the background. + hidePresetsDialog = true + } + } + ) { + Text(stringResource(if (selectedPreset != null) R.string.save else R.string.continue_)) + } + }, + dismissButton = { + TextButton(onClick = scope.dismissDialog) { + Text(stringResource(R.string.cancel)) + } + }, + title = { Text(scope.option.title) }, + textHorizontalPadding = PaddingValues(horizontal = 0.dp), + text = { + val presets = remember(scope.option.presets) { + scope.option.presets?.entries?.toList().orEmpty() + } + + LazyColumn { + @Composable + fun Item(title: String, value: Any?, presetKey: String?) { + ListItem( + modifier = Modifier.clickable { selectedPreset = presetKey }, + headlineContent = { Text(title) }, + supportingContent = value?.toString()?.let { { Text(it) } }, + leadingContent = { + HapticRadioButton( + selected = selectedPreset == presetKey, + onClick = { selectedPreset = presetKey } + ) + }, + colors = transparentListItemColors + ) + } + + items(presets, key = { it.key }) { + Item(it.key, it.value, it.key) + } + + item(key = null) { + Item(stringResource(R.string.option_preset_custom_value), null, null) + } + } + } + ) + } + } +} + +private class ListOptionEditor<T : Serializable>(private val elementEditor: OptionEditor<T>) : + OptionEditor<List<T>> { + private fun createElementOption(option: Option<List<T>>) = Option<T>( + option.title, + option.key, + option.description, + option.required, + option.type.arguments.first().type!!, + null, + null + ) { true } + + @OptIn(ExperimentalFoundationApi::class, ExperimentalMaterial3Api::class) + @Composable + override fun Dialog(scope: OptionEditorScope<List<T>>) { + val items = + rememberSaveable(scope.value, saver = snapshotStateListSaver()) { + // We need a key for each element in order to support dragging. + scope.value?.map(::Item)?.toMutableStateList() ?: mutableStateListOf() + } + val listIsDirty by remember { + derivedStateOf { + val current = scope.value.orEmpty() + if (current.size != items.size) return@derivedStateOf true + + current.forEachIndexed { index, value -> + if (value != items[index].value) return@derivedStateOf true + } + + false + } + } + + val lazyListState = rememberLazyListState() + val reorderableLazyColumnState = + rememberReorderableLazyColumnState(lazyListState) { from, to -> + // Update the list + items.add(to.index, items.removeAt(from.index)) + } + + var deleteMode by rememberSaveable { + mutableStateOf(false) + } + val deletionTargets = rememberSaveable(saver = snapshotStateSetSaver()) { + mutableStateSetOf<Int>() + } + + val back = back@{ + if (deleteMode) { + deletionTargets.clear() + deleteMode = false + return@back + } + + if (!listIsDirty) { + scope.dismissDialog() + return@back + } + + scope.submitDialog(items.mapNotNull { it.value }) + } + + ComposeDialog( + onDismissRequest = back, + properties = DialogProperties( + usePlatformDefaultWidth = false, + dismissOnBackPress = true + ), + ) { + Scaffold( + topBar = { + AppTopBar( + title = if (deleteMode) pluralStringResource( + R.plurals.selected_count, + deletionTargets.size, + deletionTargets.size + ) else scope.option.title, + onBackClick = back, + backIcon = { + if (deleteMode) { + return@AppTopBar Icon( + Icons.Filled.Close, + stringResource(R.string.cancel) + ) + } + + Icon(Icons.AutoMirrored.Filled.ArrowBack, stringResource(R.string.back)) + }, + actions = { + if (deleteMode) { + IconButton( + onClick = { + if (items.size == deletionTargets.size) deletionTargets.clear() + else deletionTargets.addAll(items.map { it.key }) + } + ) { + Icon( + Icons.Outlined.SelectAll, + stringResource(R.string.select_deselect_all) + ) + } + IconButton( + onClick = { + items.removeIf { it.key in deletionTargets } + deletionTargets.clear() + deleteMode = false + } + ) { + Icon( + Icons.Outlined.Delete, + stringResource(R.string.delete) + ) + } + } else { + IconButton(onClick = items::clear) { + Icon(Icons.Outlined.Restore, stringResource(R.string.reset)) + } + } + } + ) + }, + floatingActionButton = { + if (deleteMode) return@Scaffold + + HapticExtendedFloatingActionButton( + text = { Text(stringResource(R.string.add)) }, + icon = { + Icon( + Icons.Outlined.Add, + stringResource(R.string.add) + ) + }, + expanded = lazyListState.isScrollingUp, + onClick = { items.add(Item(null)) } + ) + } + ) { paddingValues -> + val elementOption = remember(scope.option) { createElementOption(scope.option) } + + LazyColumn( + state = lazyListState, + modifier = Modifier + .fillMaxHeight() + .padding(paddingValues), + ) { + itemsIndexed(items, key = { _, item -> item.key }) { index, item -> + val interactionSource = remember { MutableInteractionSource() } + + ReorderableItem(reorderableLazyColumnState, key = item.key) { + WithOptionEditor( + elementEditor, + elementOption, + value = item.value, + setValue = { items[index] = item.copy(value = it) } + ) { + ListItem( + modifier = Modifier.combinedClickable( + indication = LocalIndication.current, + interactionSource = interactionSource, + onLongClickLabel = stringResource(R.string.select), + onLongClick = { + deletionTargets.add(item.key) + deleteMode = true + }, + onClick = { + if (!deleteMode) { + clickAction() + return@combinedClickable + } + + if (item.key in deletionTargets) { + deletionTargets.remove( + item.key + ) + deleteMode = deletionTargets.isNotEmpty() + } else deletionTargets.add(item.key) + }, + ), + tonalElevation = if (deleteMode && item.key in deletionTargets) 8.dp else 0.dp, + leadingContent = { + IconButton( + modifier = Modifier.draggableHandle(interactionSource = interactionSource), + onClick = {}, + ) { + Icon( + Icons.Filled.DragHandle, + stringResource(R.string.drag_handle) + ) + } + }, + headlineContent = { + if (item.value == null) return@ListItem Text( + stringResource(R.string.empty), + fontStyle = FontStyle.Italic + ) + + Text(item.value.toString()) + }, + trailingContent = { + ListItemTrailingContent() + } + ) + } + } + } + } + } + } + } + + @Parcelize + private data class Item<T : Serializable>(val value: T?, val key: Int = Random.nextInt()) : + Parcelable +} \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/ui/component/patches/PathSelectorDialog.kt b/app/src/main/java/app/revanced/manager/ui/component/patches/PathSelectorDialog.kt new file mode 100644 index 0000000000..c0bad8f96e --- /dev/null +++ b/app/src/main/java/app/revanced/manager/ui/component/patches/PathSelectorDialog.kt @@ -0,0 +1,135 @@ +package app.revanced.manager.ui.component.patches + +import androidx.activity.compose.BackHandler +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.items +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.outlined.ArrowBack +import androidx.compose.material.icons.automirrored.outlined.InsertDriveFile +import androidx.compose.material.icons.filled.Close +import androidx.compose.material.icons.outlined.Folder +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.ListItem +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.window.Dialog +import androidx.compose.ui.window.DialogProperties +import app.revanced.manager.R +import app.revanced.manager.ui.component.AppTopBar +import app.revanced.manager.ui.component.GroupHeader +import app.revanced.manager.ui.component.LazyColumnWithScrollbar +import app.revanced.manager.util.saver.PathSaver +import java.nio.file.Path +import kotlin.io.path.absolutePathString +import kotlin.io.path.isDirectory +import kotlin.io.path.isReadable +import kotlin.io.path.listDirectoryEntries +import kotlin.io.path.name + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun PathSelectorDialog(root: Path, onSelect: (Path?) -> Unit) { + var currentDirectory by rememberSaveable(root, stateSaver = PathSaver) { mutableStateOf(root) } + val notAtRootDir = remember(currentDirectory) { + currentDirectory != root + } + val (directories, files) = remember(currentDirectory) { + currentDirectory.listDirectoryEntries().filter(Path::isReadable).partition(Path::isDirectory) + } + + Dialog( + onDismissRequest = { onSelect(null) }, + properties = DialogProperties( + usePlatformDefaultWidth = false, + dismissOnBackPress = true + ) + ) { + Scaffold( + topBar = { + AppTopBar( + title = stringResource(R.string.path_selector), + onBackClick = { onSelect(null) }, + backIcon = { + Icon(Icons.Filled.Close, contentDescription = stringResource(R.string.close)) + } + ) + }, + ) { paddingValues -> + BackHandler(enabled = notAtRootDir) { + currentDirectory = currentDirectory.parent + } + + LazyColumnWithScrollbar( + modifier = Modifier.padding(paddingValues) + ) { + item(key = "current") { + PathItem( + onClick = { onSelect(currentDirectory) }, + icon = Icons.Outlined.Folder, + name = currentDirectory.toString() + ) + } + + if (notAtRootDir) { + item(key = "parent") { + PathItem( + onClick = { currentDirectory = currentDirectory.parent }, + icon = Icons.AutoMirrored.Outlined.ArrowBack, + name = stringResource(R.string.path_selector_parent_dir) + ) + } + } + + if (directories.isNotEmpty()) { + item(key = "dirs_header") { + GroupHeader(title = stringResource(R.string.path_selector_dirs)) + } + } + items(directories, key = { it.absolutePathString() }) { + PathItem( + onClick = { currentDirectory = it }, + icon = Icons.Outlined.Folder, + name = it.name + ) + } + + if (files.isNotEmpty()) { + item(key = "files_header") { + GroupHeader(title = stringResource(R.string.path_selector_files)) + } + } + items(files, key = { it.absolutePathString() }) { + PathItem( + onClick = { onSelect(it) }, + icon = Icons.AutoMirrored.Outlined.InsertDriveFile, + name = it.name + ) + } + } + } + } +} + +@Composable +private fun PathItem( + onClick: () -> Unit, + icon: ImageVector, + name: String +) { + ListItem( + modifier = Modifier.clickable(onClick = onClick), + headlineContent = { Text(name) }, + leadingContent = { Icon(icon, contentDescription = null) } + ) +} \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/ui/component/settings/BooleanItem.kt b/app/src/main/java/app/revanced/manager/ui/component/settings/BooleanItem.kt new file mode 100644 index 0000000000..0be1be91d2 --- /dev/null +++ b/app/src/main/java/app/revanced/manager/ui/component/settings/BooleanItem.kt @@ -0,0 +1,53 @@ +package app.revanced.manager.ui.component.settings + +import androidx.annotation.StringRes +import androidx.compose.foundation.clickable +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import app.revanced.manager.domain.manager.base.Preference +import app.revanced.manager.ui.component.haptics.HapticSwitch +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch + +@Composable +fun BooleanItem( + modifier: Modifier = Modifier, + preference: Preference<Boolean>, + coroutineScope: CoroutineScope = rememberCoroutineScope(), + @StringRes headline: Int, + @StringRes description: Int +) { + val value by preference.getAsState() + + BooleanItem( + modifier = modifier, + value = value, + onValueChange = { coroutineScope.launch { preference.update(it) } }, + headline = headline, + description = description + ) +} + +@Composable +fun BooleanItem( + modifier: Modifier = Modifier, + value: Boolean, + onValueChange: (Boolean) -> Unit, + @StringRes headline: Int, + @StringRes description: Int +) = SettingsListItem( + modifier = Modifier + .clickable { onValueChange(!value) } + .then(modifier), + headlineContent = stringResource(headline), + supportingContent = stringResource(description), + trailingContent = { + HapticSwitch( + checked = value, + onCheckedChange = onValueChange, + ) + } +) \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/ui/component/settings/Changelog.kt b/app/src/main/java/app/revanced/manager/ui/component/settings/Changelog.kt new file mode 100644 index 0000000000..af26e23222 --- /dev/null +++ b/app/src/main/java/app/revanced/manager/ui/component/settings/Changelog.kt @@ -0,0 +1,86 @@ +package app.revanced.manager.ui.component.settings + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.CalendarToday +import androidx.compose.material.icons.outlined.Campaign +import androidx.compose.material.icons.outlined.FileDownload +import androidx.compose.material.icons.outlined.Sell +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import app.revanced.manager.ui.component.Markdown + +@Composable +fun Changelog( + markdown: String, + version: String, + publishDate: String +) { + Column(verticalArrangement = Arrangement.spacedBy(4.dp)) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(bottom = 0.dp), + horizontalArrangement = Arrangement.spacedBy(8.dp, Alignment.Start), + verticalAlignment = Alignment.CenterVertically + ) { + Icon( + imageVector = Icons.Outlined.Campaign, + contentDescription = null, + tint = MaterialTheme.colorScheme.primary, + modifier = Modifier + .size(32.dp) + ) + Text( + version.removePrefix("v"), + style = MaterialTheme.typography.titleLarge.copy(fontWeight = FontWeight(800)), + color = MaterialTheme.colorScheme.primary, + ) + } + Row( + horizontalArrangement = Arrangement.spacedBy(16.dp), + modifier = Modifier + .fillMaxWidth() + ) { + Tag( + Icons.Outlined.CalendarToday, + publishDate + ) + } + } + Markdown( + markdown, + ) +} + +@Composable +private fun Tag(icon: ImageVector, text: String) { + Row( + horizontalArrangement = Arrangement.spacedBy(6.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Icon( + imageVector = icon, + contentDescription = null, + modifier = Modifier.size(16.dp), + tint = MaterialTheme.colorScheme.outline, + ) + Text( + text, + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.outline, + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/ui/component/settings/IntegerItem.kt b/app/src/main/java/app/revanced/manager/ui/component/settings/IntegerItem.kt new file mode 100644 index 0000000000..56553b28cb --- /dev/null +++ b/app/src/main/java/app/revanced/manager/ui/component/settings/IntegerItem.kt @@ -0,0 +1,76 @@ +package app.revanced.manager.ui.component.settings + +import androidx.annotation.StringRes +import androidx.compose.foundation.clickable +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.Edit +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import app.revanced.manager.R +import app.revanced.manager.domain.manager.base.Preference +import app.revanced.manager.ui.component.IntInputDialog +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch + +@Composable +fun IntegerItem( + modifier: Modifier = Modifier, + preference: Preference<Int>, + coroutineScope: CoroutineScope = rememberCoroutineScope(), + @StringRes headline: Int, + @StringRes description: Int +) { + val value by preference.getAsState() + + IntegerItem( + modifier = modifier, + value = value, + onValueChange = { coroutineScope.launch { preference.update(it) } }, + headline = headline, + description = description + ) +} + +@Composable +fun IntegerItem( + modifier: Modifier = Modifier, + value: Int, + onValueChange: (Int) -> Unit, + @StringRes headline: Int, + @StringRes description: Int +) { + var dialogOpen by rememberSaveable { + mutableStateOf(false) + } + + if (dialogOpen) { + IntInputDialog(current = value, name = stringResource(headline)) { new -> + dialogOpen = false + new?.let(onValueChange) + } + } + + SettingsListItem( + modifier = Modifier + .clickable { dialogOpen = true } + .then(modifier), + headlineContent = stringResource(headline), + supportingContent = stringResource(description), + trailingContent = { + IconButton(onClick = { dialogOpen = true }) { + Icon( + Icons.Outlined.Edit, + contentDescription = stringResource(R.string.edit) + ) + } + } + ) +} \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/ui/component/settings/SafeguardBooleanItem.kt b/app/src/main/java/app/revanced/manager/ui/component/settings/SafeguardBooleanItem.kt new file mode 100644 index 0000000000..b48d80d1e8 --- /dev/null +++ b/app/src/main/java/app/revanced/manager/ui/component/settings/SafeguardBooleanItem.kt @@ -0,0 +1,54 @@ +package app.revanced.manager.ui.component.settings + +import androidx.annotation.StringRes +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import app.revanced.manager.domain.manager.base.Preference +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch + +@Composable +fun SafeguardBooleanItem( + modifier: Modifier = Modifier, + preference: Preference<Boolean>, + coroutineScope: CoroutineScope = rememberCoroutineScope(), + @StringRes headline: Int, + @StringRes description: Int, + @StringRes confirmationText: Int +) { + val value by preference.getAsState() + var showSafeguardWarning by rememberSaveable { + mutableStateOf(false) + } + + if (showSafeguardWarning) { + SafeguardConfirmationDialog( + onDismiss = { showSafeguardWarning = false }, + onConfirm = { + coroutineScope.launch { preference.update(!value) } + showSafeguardWarning = false + }, + body = stringResource(confirmationText) + ) + } + + BooleanItem( + modifier = modifier, + value = value, + onValueChange = { + if (it != preference.default) { + showSafeguardWarning = true + } else { + coroutineScope.launch { preference.update(it) } + } + }, + headline = headline, + description = description + ) +} \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/ui/component/settings/SafeguardConfirmationDialog.kt b/app/src/main/java/app/revanced/manager/ui/component/settings/SafeguardConfirmationDialog.kt new file mode 100644 index 0000000000..d3b6d0e818 --- /dev/null +++ b/app/src/main/java/app/revanced/manager/ui/component/settings/SafeguardConfirmationDialog.kt @@ -0,0 +1,46 @@ +package app.revanced.manager.ui.component.settings + +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.WarningAmber +import androidx.compose.material3.AlertDialog +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextAlign +import app.revanced.manager.R + +@Composable +fun SafeguardConfirmationDialog( + onDismiss: () -> Unit, + onConfirm: () -> Unit, + body: String, +) { + AlertDialog( + onDismissRequest = onDismiss, + confirmButton = { + TextButton(onClick = onConfirm) { + Text(stringResource(R.string.yes)) + } + }, + dismissButton = { + TextButton(onClick = onDismiss) { + Text(stringResource(R.string.no)) + } + }, + icon = { + Icon(Icons.Outlined.WarningAmber, null) + }, + title = { + Text( + text = stringResource(id = R.string.warning), + style = MaterialTheme.typography.headlineSmall.copy(textAlign = TextAlign.Center) + ) + }, + text = { + Text(body) + } + ) +} diff --git a/app/src/main/java/app/revanced/manager/ui/component/settings/SettingsListItem.kt b/app/src/main/java/app/revanced/manager/ui/component/settings/SettingsListItem.kt new file mode 100644 index 0000000000..7c6804776a --- /dev/null +++ b/app/src/main/java/app/revanced/manager/ui/component/settings/SettingsListItem.kt @@ -0,0 +1,70 @@ +package app.revanced.manager.ui.component.settings + +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.ListItemColors +import androidx.compose.material3.ListItemDefaults +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import androidx.compose.material3.ListItem + +@Composable +fun SettingsListItem( + headlineContent: String, + modifier: Modifier = Modifier, + overlineContent: @Composable (() -> Unit)? = null, + supportingContent: String? = null, + leadingContent: @Composable (() -> Unit)? = null, + trailingContent: @Composable (() -> Unit)? = null, + colors: ListItemColors = ListItemDefaults.colors(), + tonalElevation: Dp = ListItemDefaults.Elevation, + shadowElevation: Dp = ListItemDefaults.Elevation, +) = SettingsListItem( + headlineContent = { + Text( + text = headlineContent, + style = MaterialTheme.typography.titleLarge + ) + }, + modifier = modifier, + overlineContent = overlineContent, + supportingContent = supportingContent, + leadingContent = leadingContent, + trailingContent = trailingContent, + colors = colors, + tonalElevation = tonalElevation, + shadowElevation = shadowElevation +) + +@Composable +fun SettingsListItem( + headlineContent: @Composable () -> Unit, + modifier: Modifier = Modifier, + overlineContent: @Composable (() -> Unit)? = null, + supportingContent: String? = null, + leadingContent: @Composable (() -> Unit)? = null, + trailingContent: @Composable (() -> Unit)? = null, + colors: ListItemColors = ListItemDefaults.colors(), + tonalElevation: Dp = ListItemDefaults.Elevation, + shadowElevation: Dp = ListItemDefaults.Elevation, +) = ListItem( + headlineContent = headlineContent, + modifier = modifier.then(Modifier.padding(horizontal = 8.dp)), + overlineContent = overlineContent, + supportingContent = { + if (supportingContent != null) + Text( + text = supportingContent, + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.outline + ) + }, + leadingContent = leadingContent, + trailingContent = trailingContent, + colors = colors, + tonalElevation = tonalElevation, + shadowElevation = shadowElevation +) \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/ui/model/BundleInfo.kt b/app/src/main/java/app/revanced/manager/ui/model/BundleInfo.kt new file mode 100644 index 0000000000..9dd9d1b053 --- /dev/null +++ b/app/src/main/java/app/revanced/manager/ui/model/BundleInfo.kt @@ -0,0 +1,112 @@ +package app.revanced.manager.ui.model + +import app.revanced.manager.domain.repository.PatchBundleRepository +import app.revanced.manager.patcher.patch.PatchInfo +import app.revanced.manager.util.PatchSelection +import app.revanced.manager.util.flatMapLatestAndCombine +import kotlinx.coroutines.flow.map + +/** + * A data class that contains patch bundle metadata for use by UI code. + */ +data class BundleInfo( + val name: String, + val uid: Int, + val supported: List<PatchInfo>, + val unsupported: List<PatchInfo>, + val universal: List<PatchInfo> +) { + val all = sequence { + yieldAll(supported) + yieldAll(unsupported) + yieldAll(universal) + } + + val patchCount get() = supported.size + unsupported.size + universal.size + + fun patchSequence(allowUnsupported: Boolean) = if (allowUnsupported) { + all + } else { + sequence { + yieldAll(supported) + yieldAll(universal) + } + } + + companion object Extensions { + inline fun Iterable<BundleInfo>.toPatchSelection( + allowUnsupported: Boolean, + condition: (Int, PatchInfo) -> Boolean + ): PatchSelection = this.associate { bundle -> + val patches = + bundle.patchSequence(allowUnsupported) + .mapNotNullTo(mutableSetOf()) { patch -> + patch.name.takeIf { + condition( + bundle.uid, + patch + ) + } + } + + bundle.uid to patches + } + + fun PatchBundleRepository.bundleInfoFlow(packageName: String, version: String?) = + sources.flatMapLatestAndCombine( + combiner = { it.filterNotNull() } + ) { source -> + // Regenerate bundle information whenever this source updates. + source.state.map { state -> + val bundle = state.patchBundleOrNull() ?: return@map null + + val supported = mutableListOf<PatchInfo>() + val unsupported = mutableListOf<PatchInfo>() + val universal = mutableListOf<PatchInfo>() + + bundle.patches.filter { it.compatibleWith(packageName) }.forEach { + val targetList = when { + it.compatiblePackages == null -> universal + it.supports( + packageName, + version + ) -> supported + + else -> unsupported + } + + targetList.add(it) + } + + BundleInfo(source.getName(), source.uid, supported, unsupported, universal) + } + } + + /** + * Algorithm for determining whether all required options have been set. + */ + inline fun Iterable<BundleInfo>.requiredOptionsSet( + crossinline isSelected: (BundleInfo, PatchInfo) -> Boolean, + crossinline optionsForPatch: (BundleInfo, PatchInfo) -> Map<String, Any?>? + ) = all bundle@{ bundle -> + bundle + .all + .filter { isSelected(bundle, it) } + .all patch@{ + if (it.options.isNullOrEmpty()) return@patch true + val opts by lazy { optionsForPatch(bundle, it).orEmpty() } + + it.options.all option@{ option -> + if (!option.required || option.default != null) return@option true + + option.key in opts + } + } + } + } +} + +enum class BundleType { + Local, + Remote +} \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/ui/model/InstallerModel.kt b/app/src/main/java/app/revanced/manager/ui/model/InstallerModel.kt new file mode 100644 index 0000000000..410b64c1bd --- /dev/null +++ b/app/src/main/java/app/revanced/manager/ui/model/InstallerModel.kt @@ -0,0 +1,6 @@ +package app.revanced.manager.ui.model + +interface InstallerModel { + fun reinstall() + fun install() +} \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/ui/model/PatcherStep.kt b/app/src/main/java/app/revanced/manager/ui/model/PatcherStep.kt new file mode 100644 index 0000000000..3dbb390e1d --- /dev/null +++ b/app/src/main/java/app/revanced/manager/ui/model/PatcherStep.kt @@ -0,0 +1,33 @@ +package app.revanced.manager.ui.model + +import android.os.Parcelable +import androidx.annotation.StringRes +import app.revanced.manager.R +import kotlinx.parcelize.Parcelize + +enum class StepCategory(@StringRes val displayName: Int) { + PREPARING(R.string.patcher_step_group_preparing), + PATCHING(R.string.patcher_step_group_patching), + SAVING(R.string.patcher_step_group_saving) +} + +enum class State { + WAITING, RUNNING, FAILED, COMPLETED +} + +enum class ProgressKey { + DOWNLOAD +} + +interface StepProgressProvider { + val downloadProgress: Pair<Long, Long?>? +} + +@Parcelize +data class Step( + val name: String, + val category: StepCategory, + val state: State = State.WAITING, + val message: String? = null, + val progressKey: ProgressKey? = null +) : Parcelable \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/ui/model/SelectedApp.kt b/app/src/main/java/app/revanced/manager/ui/model/SelectedApp.kt new file mode 100644 index 0000000000..5d05c4ea90 --- /dev/null +++ b/app/src/main/java/app/revanced/manager/ui/model/SelectedApp.kt @@ -0,0 +1,35 @@ +package app.revanced.manager.ui.model + +import android.os.Parcelable +import app.revanced.manager.network.downloader.ParceledDownloaderData +import kotlinx.parcelize.Parcelize +import java.io.File + +sealed interface SelectedApp : Parcelable { + val packageName: String + val version: String? + + @Parcelize + data class Download( + override val packageName: String, + override val version: String?, + val data: ParceledDownloaderData + ) : SelectedApp + + @Parcelize + data class Search(override val packageName: String, override val version: String?) : SelectedApp + + @Parcelize + data class Local( + override val packageName: String, + override val version: String, + val file: File, + val temporary: Boolean + ) : SelectedApp + + @Parcelize + data class Installed( + override val packageName: String, + override val version: String + ) : SelectedApp +} diff --git a/app/src/main/java/app/revanced/manager/ui/model/navigation/Nav.kt b/app/src/main/java/app/revanced/manager/ui/model/navigation/Nav.kt new file mode 100644 index 0000000000..c4063ebb26 --- /dev/null +++ b/app/src/main/java/app/revanced/manager/ui/model/navigation/Nav.kt @@ -0,0 +1,96 @@ +package app.revanced.manager.ui.model.navigation + +import android.os.Parcelable +import app.revanced.manager.ui.model.SelectedApp +import app.revanced.manager.util.Options +import app.revanced.manager.util.PatchSelection +import kotlinx.parcelize.Parcelize +import kotlinx.parcelize.RawValue +import kotlinx.serialization.Serializable + +interface ComplexParameter<T : Parcelable> + +@Serializable +object Dashboard + +@Serializable +object AppSelector + +@Serializable +data class InstalledApplicationInfo(val packageName: String) + +@Serializable +data class Update(val downloadOnScreenEntry: Boolean = false) + +@Serializable +data object SelectedApplicationInfo : ComplexParameter<SelectedApplicationInfo.ViewModelParams> { + @Parcelize + data class ViewModelParams( + val app: SelectedApp, + val patches: PatchSelection? = null + ) : Parcelable + + @Serializable + object Main + + @Serializable + data object PatchesSelector : ComplexParameter<PatchesSelector.ViewModelParams> { + @Parcelize + data class ViewModelParams( + val app: SelectedApp, + val currentSelection: PatchSelection?, + val options: @RawValue Options, + ) : Parcelable + } + + @Serializable + data object RequiredOptions : ComplexParameter<PatchesSelector.ViewModelParams> +} + +@Serializable +data object Patcher : ComplexParameter<Patcher.ViewModelParams> { + @Parcelize + data class ViewModelParams( + val selectedApp: SelectedApp, + val selectedPatches: PatchSelection, + val options: @RawValue Options + ) : Parcelable +} + +@Serializable +object Settings { + sealed interface Destination + + @Serializable + data object Main : Destination + + @Serializable + data object General : Destination + + @Serializable + data object Advanced : Destination + + @Serializable + data object Updates : Destination + + @Serializable + data object Downloads : Destination + + @Serializable + data object ImportExport : Destination + + @Serializable + data object About : Destination + + @Serializable + data object Changelogs : Destination + + @Serializable + data object Contributors : Destination + + @Serializable + data object Licenses : Destination + + @Serializable + data object DeveloperOptions : Destination +} \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/ui/screen/AppSelectorScreen.kt b/app/src/main/java/app/revanced/manager/ui/screen/AppSelectorScreen.kt new file mode 100644 index 0000000000..299463dfbb --- /dev/null +++ b/app/src/main/java/app/revanced/manager/ui/screen/AppSelectorScreen.kt @@ -0,0 +1,241 @@ +package app.revanced.manager.ui.screen + +import androidx.activity.compose.rememberLauncherForActivityResult +import androidx.activity.result.contract.ActivityResultContracts +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.lazy.items +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Storage +import androidx.compose.material.icons.outlined.Search +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.ListItem +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBarDefaults +import androidx.compose.material3.rememberTopAppBarState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +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.input.nestedscroll.nestedScroll +import androidx.compose.ui.res.pluralStringResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import app.revanced.manager.R +import app.revanced.manager.ui.component.AppIcon +import app.revanced.manager.ui.component.AppLabel +import app.revanced.manager.ui.component.AppTopBar +import app.revanced.manager.ui.component.LazyColumnWithScrollbar +import app.revanced.manager.ui.component.LoadingIndicator +import app.revanced.manager.ui.component.NonSuggestedVersionDialog +import app.revanced.manager.ui.component.SearchView +import app.revanced.manager.ui.model.SelectedApp +import app.revanced.manager.ui.viewmodel.AppSelectorViewModel +import app.revanced.manager.util.APK_MIMETYPE +import app.revanced.manager.util.EventEffect +import app.revanced.manager.util.transparentListItemColors +import org.koin.androidx.compose.koinViewModel + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun AppSelectorScreen( + onSelect: (String) -> Unit, + onStorageSelect: (SelectedApp.Local) -> Unit, + onBackClick: () -> Unit, + vm: AppSelectorViewModel = koinViewModel() +) { + EventEffect(flow = vm.storageSelectionFlow) { + onStorageSelect(it) + } + + val pickApkLauncher = + rememberLauncherForActivityResult(ActivityResultContracts.GetContent()) { uri -> + uri?.let(vm::handleStorageResult) + } + + val suggestedVersions by vm.suggestedAppVersions.collectAsStateWithLifecycle(emptyMap()) + + var filterText by rememberSaveable { mutableStateOf("") } + var search by rememberSaveable { mutableStateOf(false) } + + val appList by vm.appList.collectAsStateWithLifecycle(initialValue = emptyList()) + val filteredAppList = remember(appList, filterText) { + appList.filter { app -> + (vm.loadLabel(app.packageInfo)).contains( + filterText, + true + ) or app.packageName.contains(filterText, true) + } + } + + vm.nonSuggestedVersionDialogSubject?.let { + NonSuggestedVersionDialog( + suggestedVersion = suggestedVersions[it.packageName].orEmpty(), + onDismiss = vm::dismissNonSuggestedVersionDialog + ) + } + + if (search) + SearchView( + query = filterText, + onQueryChange = { filterText = it }, + onActiveChange = { search = it }, + placeholder = { Text(stringResource(R.string.search_apps)) } + ) { + if (appList.isNotEmpty() && filterText.isNotEmpty()) { + LazyColumnWithScrollbar(modifier = Modifier.fillMaxSize()) { + items( + items = filteredAppList, + key = { it.packageName } + ) { app -> + ListItem( + modifier = Modifier.clickable { + onSelect(app.packageName) + }, + leadingContent = { + AppIcon( + app.packageInfo, + null, + Modifier.size(36.dp) + ) + }, + headlineContent = { AppLabel(app.packageInfo) }, + supportingContent = { Text(app.packageName) }, + trailingContent = app.patches?.let { + { + Text( + pluralStringResource( + R.plurals.patch_count, + it, + it + ) + ) + } + }, + colors = transparentListItemColors + ) + } + } + } else { + Column( + modifier = Modifier.fillMaxSize(), + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally + ) { + Icon( + imageVector = Icons.Outlined.Search, + contentDescription = stringResource(R.string.search), + modifier = Modifier.size(64.dp), + tint = MaterialTheme.colorScheme.onSurfaceVariant + ) + + Text( + text = stringResource(R.string.type_anything), + style = MaterialTheme.typography.bodyLarge, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + } + } + + val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState()) + + Scaffold( + topBar = { + AppTopBar( + title = stringResource(R.string.select_app), + scrollBehavior = scrollBehavior, + onBackClick = onBackClick, + actions = { + IconButton(onClick = { search = true }) { + Icon(Icons.Outlined.Search, stringResource(R.string.search)) + } + } + ) + }, + modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection), + ) { paddingValues -> + LazyColumnWithScrollbar( + modifier = Modifier + .fillMaxSize() + .padding(paddingValues), + horizontalAlignment = Alignment.CenterHorizontally + ) { + item { + ListItem( + modifier = Modifier.clickable { + pickApkLauncher.launch(APK_MIMETYPE) + }, + leadingContent = { + Box(Modifier.size(36.dp), Alignment.Center) { + Icon( + Icons.Default.Storage, + null, + modifier = Modifier.size(24.dp) + ) + } + }, + headlineContent = { Text(stringResource(R.string.select_from_storage)) }, + supportingContent = { + Text(stringResource(R.string.select_from_storage_description)) + } + ) + HorizontalDivider() + } + + if (appList.isNotEmpty()) { + items( + items = appList, + key = { it.packageName } + ) { app -> + ListItem( + modifier = Modifier.clickable { + onSelect(app.packageName) + }, + leadingContent = { AppIcon(app.packageInfo, null, Modifier.size(36.dp)) }, + headlineContent = { + AppLabel( + app.packageInfo, + defaultText = app.packageName + ) + }, + supportingContent = { + suggestedVersions[app.packageName]?.let { + Text(stringResource(R.string.suggested_version_info, it)) + } + }, + trailingContent = app.patches?.let { + { + Text( + pluralStringResource( + R.plurals.patch_count, + it, + it + ) + ) + } + } + ) + + } + } else { + item { LoadingIndicator() } + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/ui/screen/BundleListScreen.kt b/app/src/main/java/app/revanced/manager/ui/screen/BundleListScreen.kt new file mode 100644 index 0000000000..c2758e710c --- /dev/null +++ b/app/src/main/java/app/revanced/manager/ui/screen/BundleListScreen.kt @@ -0,0 +1,54 @@ +package app.revanced.manager.ui.screen + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.lazy.items +import androidx.compose.runtime.Composable +import androidx.compose.runtime.snapshots.SnapshotStateList +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import app.revanced.manager.domain.bundles.PatchBundleSource +import app.revanced.manager.ui.component.LazyColumnWithScrollbar +import app.revanced.manager.ui.component.bundle.BundleItem + +@Composable +fun BundleListScreen( + onDelete: (PatchBundleSource) -> Unit, + onUpdate: (PatchBundleSource) -> Unit, + sources: List<PatchBundleSource>, + selectedSources: SnapshotStateList<PatchBundleSource>, + bundlesSelectable: Boolean, +) { + LazyColumnWithScrollbar( + modifier = Modifier.fillMaxSize(), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Top, + ) { + items( + sources, + key = { it.uid } + ) { source -> + BundleItem( + bundle = source, + onDelete = { + onDelete(source) + }, + onUpdate = { + onUpdate(source) + }, + selectable = bundlesSelectable, + onSelect = { + selectedSources.add(source) + }, + isBundleSelected = selectedSources.contains(source), + toggleSelection = { bundleIsNotSelected -> + if (bundleIsNotSelected) { + selectedSources.add(source) + } else { + selectedSources.remove(source) + } + } + ) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/ui/screen/DashboardScreen.kt b/app/src/main/java/app/revanced/manager/ui/screen/DashboardScreen.kt new file mode 100644 index 0000000000..2beb93bb9d --- /dev/null +++ b/app/src/main/java/app/revanced/manager/ui/screen/DashboardScreen.kt @@ -0,0 +1,390 @@ +package app.revanced.manager.ui.screen + +import android.annotation.SuppressLint +import android.content.Intent +import android.net.Uri +import android.provider.Settings +import androidx.activity.compose.BackHandler +import androidx.activity.compose.rememberLauncherForActivityResult +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.pager.HorizontalPager +import androidx.compose.foundation.pager.rememberPagerState +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Add +import androidx.compose.material.icons.filled.BatteryAlert +import androidx.compose.material.icons.filled.Close +import androidx.compose.material.icons.outlined.Apps +import androidx.compose.material.icons.outlined.BugReport +import androidx.compose.material.icons.outlined.DeleteOutline +import androidx.compose.material.icons.outlined.Download +import androidx.compose.material.icons.outlined.Refresh +import androidx.compose.material.icons.outlined.Settings +import androidx.compose.material.icons.outlined.Source +import androidx.compose.material.icons.outlined.Update +import androidx.compose.material.icons.outlined.WarningAmber +import androidx.compose.material3.Badge +import androidx.compose.material3.BadgedBox +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.TabRow +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.material3.surfaceColorAtElevation +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import app.revanced.manager.R +import app.revanced.manager.domain.bundles.PatchBundleSource.Extensions.isDefault +import app.revanced.manager.patcher.aapt.Aapt +import app.revanced.manager.ui.component.AlertDialogExtended +import app.revanced.manager.ui.component.AppTopBar +import app.revanced.manager.ui.component.AutoUpdatesDialog +import app.revanced.manager.ui.component.AvailableUpdateDialog +import app.revanced.manager.ui.component.NotificationCard +import app.revanced.manager.ui.component.bundle.BundleTopBar +import app.revanced.manager.ui.component.bundle.ImportPatchBundleDialog +import app.revanced.manager.ui.component.haptics.HapticFloatingActionButton +import app.revanced.manager.ui.component.haptics.HapticTab +import app.revanced.manager.ui.viewmodel.DashboardViewModel +import app.revanced.manager.util.RequestInstallAppsContract +import app.revanced.manager.util.toast +import kotlinx.coroutines.launch +import org.koin.androidx.compose.koinViewModel + +enum class DashboardPage( + val titleResId: Int, + val icon: ImageVector +) { + DASHBOARD(R.string.tab_apps, Icons.Outlined.Apps), + BUNDLES(R.string.tab_bundles, Icons.Outlined.Source), +} + +@SuppressLint("BatteryLife") +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun DashboardScreen( + vm: DashboardViewModel = koinViewModel(), + onAppSelectorClick: () -> Unit, + onSettingsClick: () -> Unit, + onUpdateClick: () -> Unit, + onDownloaderPluginClick: () -> Unit, + onAppClick: (String) -> Unit +) { + val bundlesSelectable by remember { derivedStateOf { vm.selectedSources.size > 0 } } + val availablePatches by vm.availablePatches.collectAsStateWithLifecycle(0) + val showNewDownloaderPluginsNotification by vm.newDownloaderPluginsAvailable.collectAsStateWithLifecycle( + false + ) + val androidContext = LocalContext.current + val composableScope = rememberCoroutineScope() + val pagerState = rememberPagerState( + initialPage = DashboardPage.DASHBOARD.ordinal, + initialPageOffsetFraction = 0f + ) { DashboardPage.entries.size } + + LaunchedEffect(pagerState.currentPage) { + if (pagerState.currentPage != DashboardPage.BUNDLES.ordinal) vm.cancelSourceSelection() + } + + val firstLaunch by vm.prefs.firstLaunch.getAsState() + if (firstLaunch) AutoUpdatesDialog(vm::applyAutoUpdatePrefs) + + var showAddBundleDialog by rememberSaveable { mutableStateOf(false) } + if (showAddBundleDialog) { + ImportPatchBundleDialog( + onDismiss = { showAddBundleDialog = false }, + onLocalSubmit = { patches -> + showAddBundleDialog = false + vm.createLocalSource(patches) + }, + onRemoteSubmit = { url, autoUpdate -> + showAddBundleDialog = false + vm.createRemoteSource(url, autoUpdate) + } + ) + } + + var showUpdateDialog by rememberSaveable { mutableStateOf(vm.prefs.showManagerUpdateDialogOnLaunch.getBlocking()) } + val availableUpdate by remember { + derivedStateOf { vm.updatedManagerVersion.takeIf { showUpdateDialog } } + } + + availableUpdate?.let { version -> + AvailableUpdateDialog( + onDismiss = { showUpdateDialog = false }, + setShowManagerUpdateDialogOnLaunch = vm::setShowManagerUpdateDialogOnLaunch, + onConfirm = onUpdateClick, + newVersion = version + ) + } + + var showAndroid11Dialog by rememberSaveable { mutableStateOf(false) } + val installAppsPermissionLauncher = + rememberLauncherForActivityResult(RequestInstallAppsContract) { granted -> + showAndroid11Dialog = false + if (granted) onAppSelectorClick() + } + if (showAndroid11Dialog) Android11Dialog( + onDismissRequest = { + showAndroid11Dialog = false + }, + onContinue = { + installAppsPermissionLauncher.launch(androidContext.packageName) + } + ) + + Scaffold( + topBar = { + if (bundlesSelectable) { + BundleTopBar( + title = stringResource(R.string.bundles_selected, vm.selectedSources.size), + onBackClick = vm::cancelSourceSelection, + backIcon = { + Icon( + imageVector = Icons.Default.Close, + contentDescription = stringResource(R.string.back) + ) + }, + actions = { + IconButton( + onClick = { + vm.selectedSources.forEach { if (!it.isDefault) vm.delete(it) } + vm.cancelSourceSelection() + } + ) { + Icon( + Icons.Outlined.DeleteOutline, + stringResource(R.string.delete) + ) + } + IconButton( + onClick = { + vm.selectedSources.forEach { vm.update(it) } + vm.cancelSourceSelection() + } + ) { + Icon( + Icons.Outlined.Refresh, + stringResource(R.string.refresh) + ) + } + } + ) + } else { + AppTopBar( + title = stringResource(R.string.app_name), + actions = { + if (!vm.updatedManagerVersion.isNullOrEmpty()) { + IconButton( + onClick = onUpdateClick, + ) { + BadgedBox( + badge = { + Badge(modifier = Modifier.size(6.dp)) + } + ) { + Icon(Icons.Outlined.Update, stringResource(R.string.update)) + } + } + } + IconButton(onClick = onSettingsClick) { + Icon(Icons.Outlined.Settings, stringResource(R.string.settings)) + } + }, + applyContainerColor = true + ) + } + }, + floatingActionButton = { + HapticFloatingActionButton( + onClick = { + vm.cancelSourceSelection() + + when (pagerState.currentPage) { + DashboardPage.DASHBOARD.ordinal -> { + if (availablePatches < 1) { + androidContext.toast(androidContext.getString(R.string.patches_unavailable)) + composableScope.launch { + pagerState.animateScrollToPage( + DashboardPage.BUNDLES.ordinal + ) + } + return@HapticFloatingActionButton + } + if (vm.android11BugActive) { + showAndroid11Dialog = true + return@HapticFloatingActionButton + } + + onAppSelectorClick() + } + + DashboardPage.BUNDLES.ordinal -> { + showAddBundleDialog = true + } + } + } + ) { Icon(Icons.Default.Add, stringResource(R.string.add)) } + } + ) { paddingValues -> + Column(Modifier.padding(paddingValues)) { + TabRow( + selectedTabIndex = pagerState.currentPage, + containerColor = MaterialTheme.colorScheme.surfaceColorAtElevation(3.0.dp) + ) { + DashboardPage.entries.forEachIndexed { index, page -> + HapticTab( + selected = pagerState.currentPage == index, + onClick = { composableScope.launch { pagerState.animateScrollToPage(index) } }, + text = { Text(stringResource(page.titleResId)) }, + icon = { Icon(page.icon, null) }, + selectedContentColor = MaterialTheme.colorScheme.primary, + unselectedContentColor = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + } + + val showBatteryOptimizationsWarning by vm.showBatteryOptimizationsWarningFlow.collectAsStateWithLifecycle( + false + ) + Notifications( + if (!Aapt.supportsDevice()) { + { + NotificationCard( + isWarning = true, + icon = Icons.Outlined.WarningAmber, + text = stringResource(R.string.unsupported_architecture_warning), + onDismiss = null + ) + } + } else null, + if (showBatteryOptimizationsWarning) { + { + NotificationCard( + isWarning = true, + icon = Icons.Default.BatteryAlert, + text = stringResource(R.string.battery_optimization_notification), + onClick = { + androidContext.startActivity(Intent(Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS).apply { + data = Uri.parse("package:${androidContext.packageName}") + }) + } + ) + } + } else null, + if (showNewDownloaderPluginsNotification) { + { + NotificationCard( + text = stringResource(R.string.new_downloader_plugins_notification), + icon = Icons.Outlined.Download, + modifier = Modifier.clickable(onClick = onDownloaderPluginClick), + actions = { + TextButton(onClick = vm::ignoreNewDownloaderPlugins) { + Text(stringResource(R.string.dismiss)) + } + } + ) + } + } else null + ) + + HorizontalPager( + state = pagerState, + userScrollEnabled = true, + modifier = Modifier.fillMaxSize(), + pageContent = { index -> + when (DashboardPage.entries[index]) { + DashboardPage.DASHBOARD -> { + InstalledAppsScreen( + onAppClick = { onAppClick(it.currentPackageName) } + ) + } + + DashboardPage.BUNDLES -> { + BackHandler { + if (bundlesSelectable) vm.cancelSourceSelection() else composableScope.launch { + pagerState.animateScrollToPage( + DashboardPage.DASHBOARD.ordinal + ) + } + } + + val sources by vm.sources.collectAsStateWithLifecycle(initialValue = emptyList()) + + BundleListScreen( + onDelete = { + vm.delete(it) + }, + onUpdate = { + vm.update(it) + }, + sources = sources, + selectedSources = vm.selectedSources, + bundlesSelectable = bundlesSelectable + ) + } + } + } + ) + } + } +} + +@Composable +fun Notifications( + vararg notifications: (@Composable () -> Unit)?, +) { + val activeNotifications = notifications.filterNotNull() + + if (activeNotifications.isNotEmpty()) { + Column( + modifier = Modifier.padding(16.dp), + verticalArrangement = Arrangement.spacedBy(16.dp) + ) { + activeNotifications.forEach { notification -> + notification() + } + } + } +} + +@Composable +fun Android11Dialog(onDismissRequest: () -> Unit, onContinue: () -> Unit) { + AlertDialogExtended( + onDismissRequest = onDismissRequest, + confirmButton = { + TextButton(onClick = onContinue) { + Text(stringResource(R.string.continue_)) + } + }, + title = { + Text(stringResource(R.string.android_11_bug_dialog_title)) + }, + icon = { + Icon(Icons.Outlined.BugReport, null) + }, + text = { + Text(stringResource(R.string.android_11_bug_dialog_description)) + } + ) +} \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/ui/screen/InstalledAppInfoScreen.kt b/app/src/main/java/app/revanced/manager/ui/screen/InstalledAppInfoScreen.kt new file mode 100644 index 0000000000..851e4d2464 --- /dev/null +++ b/app/src/main/java/app/revanced/manager/ui/screen/InstalledAppInfoScreen.kt @@ -0,0 +1,214 @@ +package app.revanced.manager.ui.screen + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.ArrowRight +import androidx.compose.material.icons.automirrored.outlined.OpenInNew +import androidx.compose.material.icons.outlined.Circle +import androidx.compose.material.icons.outlined.Delete +import androidx.compose.material.icons.outlined.SettingsBackupRestore +import androidx.compose.material.icons.outlined.Update +import androidx.compose.material3.AlertDialog +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.material3.TopAppBarDefaults +import androidx.compose.material3.rememberTopAppBarState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.SideEffect +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.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.input.nestedscroll.nestedScroll +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.pluralStringResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import app.revanced.manager.R +import app.revanced.manager.data.room.apps.installed.InstallType +import app.revanced.manager.ui.component.AppInfo +import app.revanced.manager.ui.component.AppTopBar +import app.revanced.manager.ui.component.ColumnWithScrollbar +import app.revanced.manager.ui.component.SegmentedButton +import app.revanced.manager.ui.component.settings.SettingsListItem +import app.revanced.manager.ui.viewmodel.InstalledAppInfoViewModel +import app.revanced.manager.util.toast + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun InstalledAppInfoScreen( + onPatchClick: (packageName: String) -> Unit, + onBackClick: () -> Unit, + viewModel: InstalledAppInfoViewModel +) { + val context = LocalContext.current + + SideEffect { + viewModel.onBackClick = onBackClick + } + + var showUninstallDialog by rememberSaveable { mutableStateOf(false) } + + if (showUninstallDialog) + UninstallDialog( + onDismiss = { showUninstallDialog = false }, + onConfirm = { viewModel.uninstall() } + ) + + val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState()) + + Scaffold( + topBar = { + AppTopBar( + title = stringResource(R.string.app_info), + scrollBehavior = scrollBehavior, + onBackClick = onBackClick + ) + }, + modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection), + ) { paddingValues -> + ColumnWithScrollbar( + modifier = Modifier + .fillMaxSize() + .padding(paddingValues) + ) { + val installedApp = viewModel.installedApp ?: return@ColumnWithScrollbar + + AppInfo(viewModel.appInfo) { + Text(installedApp.version, color = MaterialTheme.colorScheme.onSurfaceVariant, style = MaterialTheme.typography.bodyMedium) + + if (installedApp.installType == InstallType.MOUNT) { + Text( + text = if (viewModel.isMounted) { + stringResource(R.string.mounted) + } else { + stringResource(R.string.not_mounted) + }, + style = MaterialTheme.typography.bodySmall + ) + } + } + + Row( + horizontalArrangement = Arrangement.spacedBy(2.dp), + modifier = Modifier + .padding(horizontal = 16.dp) + .clip(RoundedCornerShape(24.dp)) + ) { + SegmentedButton( + icon = Icons.AutoMirrored.Outlined.OpenInNew, + text = stringResource(R.string.open_app), + onClick = viewModel::launch + ) + + when (installedApp.installType) { + InstallType.DEFAULT -> SegmentedButton( + icon = Icons.Outlined.Delete, + text = stringResource(R.string.uninstall), + onClick = viewModel::uninstall + ) + + InstallType.MOUNT -> { + SegmentedButton( + icon = Icons.Outlined.SettingsBackupRestore, + text = stringResource(R.string.unpatch), + onClick = { showUninstallDialog = true }, + enabled = viewModel.rootInstaller.hasRootAccess() + ) + + SegmentedButton( + icon = Icons.Outlined.Circle, + text = if (viewModel.isMounted) stringResource(R.string.unmount) else stringResource(R.string.mount), + onClick = viewModel::mountOrUnmount, + enabled = viewModel.rootInstaller.hasRootAccess() + ) + } + + } + + SegmentedButton( + icon = Icons.Outlined.Update, + text = stringResource(R.string.repatch), + onClick = { + onPatchClick(installedApp.originalPackageName) + }, + enabled = installedApp.installType != InstallType.MOUNT || viewModel.rootInstaller.hasRootAccess() + ) + } + + Column( + modifier = Modifier.padding(vertical = 16.dp) + ) { + SettingsListItem( + modifier = Modifier.clickable { context.toast("Not implemented yet!") }, + headlineContent = stringResource(R.string.applied_patches), + supportingContent = + (viewModel.appliedPatches?.values?.sumOf { it.size } ?: 0).let { + pluralStringResource( + id = R.plurals.patch_count, + it, + it + ) + }, + trailingContent = { Icon(Icons.AutoMirrored.Filled.ArrowRight, contentDescription = stringResource(R.string.view_applied_patches)) } + ) + + SettingsListItem( + headlineContent = stringResource(R.string.package_name), + supportingContent = installedApp.currentPackageName + ) + + if (installedApp.originalPackageName != installedApp.currentPackageName) { + SettingsListItem( + headlineContent = stringResource(R.string.original_package_name), + supportingContent = installedApp.originalPackageName + ) + } + + SettingsListItem( + headlineContent = stringResource(R.string.install_type), + supportingContent = stringResource(installedApp.installType.stringResource) + ) + } + } + } +} + +@Composable +fun UninstallDialog( + onDismiss: () -> Unit, + onConfirm: () -> Unit +) = AlertDialog( + onDismissRequest = onDismiss, + title = { Text(stringResource(R.string.unpatch_app)) }, + text = { Text(stringResource(R.string.unpatch_description)) }, + confirmButton = { + TextButton( + onClick = { + onConfirm() + onDismiss() + } + ) { + Text(stringResource(R.string.ok)) + } + }, + dismissButton = { + TextButton( + onClick = onDismiss + ) { + Text(stringResource(R.string.cancel)) + } + } +) \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/ui/screen/InstalledAppsScreen.kt b/app/src/main/java/app/revanced/manager/ui/screen/InstalledAppsScreen.kt new file mode 100644 index 0000000000..264cf587b0 --- /dev/null +++ b/app/src/main/java/app/revanced/manager/ui/screen/InstalledAppsScreen.kt @@ -0,0 +1,75 @@ +package app.revanced.manager.ui.screen + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.lazy.items +import androidx.compose.material3.ListItem +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import app.revanced.manager.R +import app.revanced.manager.data.room.apps.installed.InstalledApp +import app.revanced.manager.ui.component.AppIcon +import app.revanced.manager.ui.component.AppLabel +import app.revanced.manager.ui.component.LazyColumnWithScrollbar +import app.revanced.manager.ui.component.LoadingIndicator +import app.revanced.manager.ui.viewmodel.InstalledAppsViewModel +import org.koin.androidx.compose.koinViewModel + +@Composable +fun InstalledAppsScreen( + onAppClick: (InstalledApp) -> Unit, + viewModel: InstalledAppsViewModel = koinViewModel() +) { + val installedApps by viewModel.apps.collectAsStateWithLifecycle(initialValue = null) + + Column { + LazyColumnWithScrollbar( + modifier = Modifier.fillMaxSize(), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = if (installedApps.isNullOrEmpty()) Arrangement.Center else Arrangement.Top, + ) { + installedApps?.let { installedApps -> + if (installedApps.isNotEmpty()) { + items( + installedApps, + key = { it.currentPackageName } + ) { installedApp -> + viewModel.packageInfoMap[installedApp.currentPackageName].let { packageInfo -> + ListItem( + modifier = Modifier.clickable { onAppClick(installedApp) }, + leadingContent = { + AppIcon( + packageInfo, + contentDescription = null, + Modifier.size(36.dp) + ) + }, + headlineContent = { AppLabel(packageInfo, defaultText = null) }, + supportingContent = { Text(installedApp.currentPackageName) } + ) + + } + } + } else { + item { + Text( + text = stringResource(R.string.no_patched_apps_found), + style = MaterialTheme.typography.titleLarge + ) + } + } + + } ?: item { LoadingIndicator() } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/ui/screen/PatcherScreen.kt b/app/src/main/java/app/revanced/manager/ui/screen/PatcherScreen.kt new file mode 100644 index 0000000000..44217abce9 --- /dev/null +++ b/app/src/main/java/app/revanced/manager/ui/screen/PatcherScreen.kt @@ -0,0 +1,207 @@ +package app.revanced.manager.ui.screen + +import android.app.Activity +import android.view.WindowManager +import androidx.activity.compose.BackHandler +import androidx.activity.compose.rememberLauncherForActivityResult +import androidx.activity.result.contract.ActivityResultContracts +import androidx.activity.result.contract.ActivityResultContracts.CreateDocument +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.outlined.OpenInNew +import androidx.compose.material.icons.outlined.FileDownload +import androidx.compose.material.icons.outlined.PostAdd +import androidx.compose.material.icons.outlined.Save +import androidx.compose.material3.* +import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.getValue +import androidx.compose.runtime.livedata.observeAsState +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import app.revanced.manager.R +import app.revanced.manager.data.room.apps.installed.InstallType +import app.revanced.manager.ui.component.AppScaffold +import app.revanced.manager.ui.component.AppTopBar +import app.revanced.manager.ui.component.InstallerStatusDialog +import app.revanced.manager.ui.component.haptics.HapticExtendedFloatingActionButton +import app.revanced.manager.ui.component.patcher.InstallPickerDialog +import app.revanced.manager.ui.component.patcher.Steps +import app.revanced.manager.ui.model.StepCategory +import app.revanced.manager.ui.viewmodel.PatcherViewModel +import app.revanced.manager.util.APK_MIMETYPE +import app.revanced.manager.util.EventEffect + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun PatcherScreen( + onBackClick: () -> Unit, + vm: PatcherViewModel +) { + fun leaveScreen() { + vm.onBack() + onBackClick() + } + BackHandler(onBack = ::leaveScreen) + + val context = LocalContext.current + val exportApkLauncher = + rememberLauncherForActivityResult(CreateDocument(APK_MIMETYPE), vm::export) + + val patcherSucceeded by vm.patcherSucceeded.observeAsState(null) + val canInstall by remember { derivedStateOf { patcherSucceeded == true && (vm.installedPackageName != null || !vm.isInstalling) } } + var showInstallPicker by rememberSaveable { mutableStateOf(false) } + + val steps by remember { + derivedStateOf { + vm.steps.groupBy { it.category } + } + } + + if (patcherSucceeded == null) { + DisposableEffect(Unit) { + val window = (context as Activity).window + window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) + onDispose { + window.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) + } + } + } + + if (showInstallPicker) + InstallPickerDialog( + onDismiss = { showInstallPicker = false }, + onConfirm = vm::install + ) + + vm.packageInstallerStatus?.let { + InstallerStatusDialog(it, vm, vm::dismissPackageInstallerDialog) + } + + val activityLauncher = rememberLauncherForActivityResult( + contract = ActivityResultContracts.StartActivityForResult(), + onResult = vm::handleActivityResult + ) + EventEffect(flow = vm.launchActivityFlow) { intent -> + activityLauncher.launch(intent) + } + + vm.activityPromptDialog?.let { title -> + AlertDialog( + onDismissRequest = vm::rejectInteraction, + confirmButton = { + TextButton( + onClick = vm::allowInteraction + ) { + Text(stringResource(R.string.continue_)) + } + }, + dismissButton = { + TextButton( + onClick = vm::rejectInteraction + ) { + Text(stringResource(R.string.cancel)) + } + }, + title = { Text(title) }, + text = { + Text(stringResource(R.string.plugin_activity_dialog_body)) + } + ) + } + + AppScaffold( + topBar = { scrollBehavior -> + AppTopBar( + title = stringResource(R.string.patcher), + scrollBehavior = scrollBehavior, + onBackClick = ::leaveScreen + ) + }, + bottomBar = { + BottomAppBar( + actions = { + IconButton( + onClick = { exportApkLauncher.launch("${vm.packageName}.apk") }, + enabled = patcherSucceeded == true + ) { + Icon(Icons.Outlined.Save, stringResource(id = R.string.save_apk)) + } + IconButton( + onClick = { vm.exportLogs(context) }, + enabled = patcherSucceeded != null + ) { + Icon(Icons.Outlined.PostAdd, stringResource(id = R.string.save_logs)) + } + }, + floatingActionButton = { + AnimatedVisibility(visible = canInstall) { + HapticExtendedFloatingActionButton( + text = { + Text( + stringResource(if (vm.installedPackageName == null) R.string.install_app else R.string.open_app) + ) + }, + icon = { + vm.installedPackageName?.let { + Icon( + Icons.AutoMirrored.Outlined.OpenInNew, + stringResource(R.string.open_app) + ) + } ?: Icon( + Icons.Outlined.FileDownload, + stringResource(R.string.install_app) + ) + }, + onClick = { + if (vm.installedPackageName == null) + if (vm.isDeviceRooted()) showInstallPicker = true + else vm.install(InstallType.DEFAULT) + else vm.open() + } + ) + } + } + ) + } + ) { paddingValues -> + Column( + modifier = Modifier + .padding(paddingValues) + .fillMaxSize() + ) { + LinearProgressIndicator( + progress = { vm.progress }, + modifier = Modifier.fillMaxWidth() + ) + + LazyColumn( + modifier = Modifier.fillMaxSize(), + verticalArrangement = Arrangement.spacedBy(16.dp), + contentPadding = PaddingValues(16.dp) + ) { + items( + items = steps.toList(), + key = { it.first } + ) { (category, steps) -> + Steps( + category = category, + steps = steps, + stepCount = if (category == StepCategory.PATCHING) vm.patchesProgress else null, + stepProgressProvider = vm + ) + } + } + } + } +} diff --git a/app/src/main/java/app/revanced/manager/ui/screen/PatchesSelectorScreen.kt b/app/src/main/java/app/revanced/manager/ui/screen/PatchesSelectorScreen.kt new file mode 100644 index 0000000000..2614195925 --- /dev/null +++ b/app/src/main/java/app/revanced/manager/ui/screen/PatchesSelectorScreen.kt @@ -0,0 +1,662 @@ +package app.revanced.manager.ui.screen + +import androidx.compose.animation.AnimatedContent +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.core.EaseInOut +import androidx.compose.animation.core.animateFloatAsState +import androidx.compose.animation.core.tween +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.animation.slideInHorizontally +import androidx.compose.animation.slideOutHorizontally +import androidx.compose.animation.togetherWith +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.ExperimentalLayoutApi +import androidx.compose.foundation.layout.FlowRow +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyListScope +import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.pager.HorizontalPager +import androidx.compose.foundation.pager.rememberPagerState +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.ArrowBack +import androidx.compose.material.icons.automirrored.outlined.HelpOutline +import androidx.compose.material.icons.filled.Close +import androidx.compose.material.icons.outlined.FilterList +import androidx.compose.material.icons.outlined.Restore +import androidx.compose.material.icons.outlined.Save +import androidx.compose.material.icons.outlined.Settings +import androidx.compose.material.icons.outlined.WarningAmber +import androidx.compose.material3.AlertDialog +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.ListItem +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.ModalBottomSheet +import androidx.compose.material3.Scaffold +import androidx.compose.material3.ScrollableTabRow +import androidx.compose.material3.SmallFloatingActionButton +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.material3.surfaceColorAtElevation +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.saveable.rememberSaveable +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.draw.rotate +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import androidx.compose.ui.window.Dialog +import androidx.compose.ui.window.DialogProperties +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import app.revanced.manager.R +import app.revanced.manager.patcher.patch.Option +import app.revanced.manager.patcher.patch.PatchInfo +import app.revanced.manager.ui.component.AppTopBar +import app.revanced.manager.ui.component.CheckedFilterChip +import app.revanced.manager.ui.component.LazyColumnWithScrollbar +import app.revanced.manager.ui.component.SafeguardDialog +import app.revanced.manager.ui.component.SearchBar +import app.revanced.manager.ui.component.haptics.HapticCheckbox +import app.revanced.manager.ui.component.haptics.HapticExtendedFloatingActionButton +import app.revanced.manager.ui.component.haptics.HapticTab +import app.revanced.manager.ui.component.patches.OptionItem +import app.revanced.manager.ui.viewmodel.PatchesSelectorViewModel +import app.revanced.manager.ui.viewmodel.PatchesSelectorViewModel.Companion.SHOW_UNIVERSAL +import app.revanced.manager.ui.viewmodel.PatchesSelectorViewModel.Companion.SHOW_UNSUPPORTED +import app.revanced.manager.util.Options +import app.revanced.manager.util.PatchSelection +import app.revanced.manager.util.isScrollingUp +import app.revanced.manager.util.transparentListItemColors +import kotlinx.coroutines.launch + +@OptIn(ExperimentalMaterial3Api::class, ExperimentalLayoutApi::class) +@Composable +fun PatchesSelectorScreen( + onSave: (PatchSelection?, Options) -> Unit, + onBackClick: () -> Unit, + vm: PatchesSelectorViewModel +) { + val bundles by vm.bundlesFlow.collectAsStateWithLifecycle(initialValue = emptyList()) + val pagerState = rememberPagerState( + initialPage = 0, + initialPageOffsetFraction = 0f + ) { + bundles.size + } + val composableScope = rememberCoroutineScope() + val (query, setQuery) = rememberSaveable { + mutableStateOf("") + } + val (searchExpanded, setSearchExpanded) = rememberSaveable { + mutableStateOf(false) + } + var showBottomSheet by rememberSaveable { mutableStateOf(false) } + val showSaveButton by remember { + derivedStateOf { vm.selectionIsValid(bundles) } + } + + val defaultPatchSelectionCount by vm.defaultSelectionCount + .collectAsStateWithLifecycle(initialValue = 0) + + val selectedPatchCount by remember { + derivedStateOf { + vm.customPatchSelection?.values?.sumOf { it.size } ?: defaultPatchSelectionCount + } + } + + val patchLazyListStates = remember(bundles) { List(bundles.size) { LazyListState() } } + + if (showBottomSheet) { + ModalBottomSheet( + onDismissRequest = { + showBottomSheet = false + } + ) { + Column( + modifier = Modifier.padding(horizontal = 24.dp) + ) { + Text( + text = stringResource(R.string.patch_selector_sheet_filter_title), + style = MaterialTheme.typography.headlineSmall, + modifier = Modifier.padding(bottom = 16.dp) + ) + + Text( + text = stringResource(R.string.patch_selector_sheet_filter_compat_title), + style = MaterialTheme.typography.titleMedium + ) + + FlowRow( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(8.dp), + verticalArrangement = Arrangement.spacedBy(12.dp) + ) { + CheckedFilterChip( + selected = vm.filter and SHOW_UNSUPPORTED == 0, + onClick = { vm.toggleFlag(SHOW_UNSUPPORTED) }, + label = { Text(stringResource(R.string.supported)) } + ) + + CheckedFilterChip( + selected = vm.filter and SHOW_UNIVERSAL != 0, + onClick = { vm.toggleFlag(SHOW_UNIVERSAL) }, + label = { Text(stringResource(R.string.universal)) }, + ) + } + } + } + } + + if (vm.compatibleVersions.isNotEmpty()) + UnsupportedPatchDialog( + appVersion = vm.appVersion ?: stringResource(R.string.any_version), + supportedVersions = vm.compatibleVersions, + onDismissRequest = vm::dismissDialogs + ) + var showUnsupportedPatchesDialog by rememberSaveable { + mutableStateOf(false) + } + if (showUnsupportedPatchesDialog) + UnsupportedPatchesDialog( + appVersion = vm.appVersion ?: stringResource(R.string.any_version), + onDismissRequest = { showUnsupportedPatchesDialog = false } + ) + + vm.optionsDialog?.let { (bundle, patch) -> + OptionsDialog( + onDismissRequest = vm::dismissDialogs, + patch = patch, + values = vm.getOptions(bundle, patch), + reset = { vm.resetOptions(bundle, patch) }, + set = { key, value -> vm.setOption(bundle, patch, key, value) } + ) + } + + var showSelectionWarning by rememberSaveable { + mutableStateOf(false) + } + if (showSelectionWarning) { + SelectionWarningDialog(onDismiss = { showSelectionWarning = false }) + } + vm.pendingUniversalPatchAction?.let { + UniversalPatchWarningDialog( + onCancel = vm::dismissUniversalPatchWarning, + onConfirm = vm::confirmUniversalPatchWarning + ) + } + + fun LazyListScope.patchList( + uid: Int, + patches: List<PatchInfo>, + visible: Boolean, + supported: Boolean, + header: (@Composable () -> Unit)? = null + ) { + if (patches.isNotEmpty() && visible) { + header?.let { + item(contentType = 0) { + it() + } + } + + items( + items = patches, + key = { it.name }, + contentType = { 1 } + ) { patch -> + PatchItem( + patch = patch, + onOptionsDialog = { + vm.optionsDialog = uid to patch + }, + selected = supported && vm.isSelected( + uid, + patch + ), + onToggle = { + when { + // Open unsupported dialog if the patch is not supported + !supported -> vm.openUnsupportedDialog(patch) + + // Show selection warning if enabled + vm.selectionWarningEnabled -> showSelectionWarning = true + + // Set pending universal patch action if the universal patch warning is enabled and there are no compatible packages + vm.universalPatchWarningEnabled && patch.compatiblePackages == null -> { + vm.pendingUniversalPatchAction = { vm.togglePatch(uid, patch) } + } + + // Toggle the patch otherwise + else -> vm.togglePatch(uid, patch) + } + }, + supported = supported + ) + } + } + } + + Scaffold( + topBar = { + SearchBar( + query = query, + onQueryChange = setQuery, + expanded = searchExpanded, + onExpandedChange = setSearchExpanded, + placeholder = { + Text(stringResource(R.string.search_patches)) + }, + leadingIcon = { + val rotation by animateFloatAsState( + targetValue = if (searchExpanded) 360f else 0f, + animationSpec = tween(durationMillis = 400, easing = EaseInOut), + label = "SearchBar back button" + ) + IconButton( + onClick = { + if (searchExpanded) { + setSearchExpanded(false) + } else { + onBackClick() + } + } + ) { + Icon( + modifier = Modifier.rotate(rotation), + imageVector = Icons.AutoMirrored.Filled.ArrowBack, + contentDescription = stringResource(R.string.back) + ) + } + }, + trailingIcon = { + AnimatedContent( + targetState = searchExpanded, + label = "Filter/Clear", + transitionSpec = { fadeIn() togetherWith fadeOut() } + ) { searchExpanded -> + if (searchExpanded) { + IconButton( + onClick = { setQuery("") }, + enabled = query.isNotEmpty() + ) { + Icon( + imageVector = Icons.Filled.Close, + contentDescription = stringResource(R.string.clear) + ) + } + } else { + IconButton(onClick = { showBottomSheet = true }) { + Icon( + imageVector = Icons.Outlined.FilterList, + contentDescription = stringResource(R.string.more) + ) + } + } + } + } + ) { + val bundle = bundles[pagerState.currentPage] + + LazyColumnWithScrollbar( + modifier = Modifier.fillMaxSize() + ) { + fun List<PatchInfo>.searched() = filter { + it.name.contains(query, true) + } + + patchList( + uid = bundle.uid, + patches = bundle.supported.searched(), + visible = true, + supported = true + ) + patchList( + uid = bundle.uid, + patches = bundle.universal.searched(), + visible = vm.filter and SHOW_UNIVERSAL != 0, + supported = true + ) { + ListHeader( + title = stringResource(R.string.universal_patches), + ) + } + + patchList( + uid = bundle.uid, + patches = bundle.unsupported.searched(), + visible = vm.filter and SHOW_UNSUPPORTED != 0, + supported = vm.allowIncompatiblePatches + ) { + ListHeader( + title = stringResource(R.string.unsupported_patches), + onHelpClick = { showUnsupportedPatchesDialog = true } + ) + } + } + } + }, + floatingActionButton = { + if (!showSaveButton) return@Scaffold + + AnimatedVisibility( + visible = !searchExpanded, + enter = slideInHorizontally { it } + fadeIn(), + exit = slideOutHorizontally { it } + fadeOut() + ) { + Column( + horizontalAlignment = Alignment.End, + verticalArrangement = Arrangement.spacedBy(4.dp) + ) { + SmallFloatingActionButton( + onClick = vm::reset, + containerColor = MaterialTheme.colorScheme.tertiaryContainer + ) { + Icon(Icons.Outlined.Restore, stringResource(R.string.reset)) + } + HapticExtendedFloatingActionButton( + text = { Text(stringResource(R.string.save_with_count, selectedPatchCount)) }, + icon = { + Icon( + imageVector = Icons.Outlined.Save, + contentDescription = stringResource(R.string.save) + ) + }, + expanded = patchLazyListStates.getOrNull(pagerState.currentPage)?.isScrollingUp ?: true, + onClick = { + onSave(vm.getCustomSelection(), vm.getOptions()) + } + ) + } + } + } + ) { paddingValues -> + Column( + modifier = Modifier + .fillMaxSize() + .padding(paddingValues) + ) { + if (bundles.size > 1) { + ScrollableTabRow( + selectedTabIndex = pagerState.currentPage, + containerColor = MaterialTheme.colorScheme.surfaceColorAtElevation(3.0.dp) + ) { + bundles.forEachIndexed { index, bundle -> + HapticTab( + selected = pagerState.currentPage == index, + onClick = { + composableScope.launch { + pagerState.animateScrollToPage( + index + ) + } + }, + text = { Text(bundle.name) }, + selectedContentColor = MaterialTheme.colorScheme.primary, + unselectedContentColor = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + } + } + + HorizontalPager( + state = pagerState, + userScrollEnabled = true, + pageContent = { index -> + // Avoid crashing if the lists have not been fully initialized yet. + if (index > bundles.lastIndex || bundles.size != patchLazyListStates.size) return@HorizontalPager + val bundle = bundles[index] + + LazyColumnWithScrollbar( + modifier = Modifier.fillMaxSize(), + state = patchLazyListStates[index] + ) { + patchList( + uid = bundle.uid, + patches = bundle.supported, + visible = true, + supported = true + ) + patchList( + uid = bundle.uid, + patches = bundle.universal, + visible = vm.filter and SHOW_UNIVERSAL != 0, + supported = true + ) { + ListHeader( + title = stringResource(R.string.universal_patches), + ) + } + patchList( + uid = bundle.uid, + patches = bundle.unsupported, + visible = vm.filter and SHOW_UNSUPPORTED != 0, + supported = vm.allowIncompatiblePatches + ) { + ListHeader( + title = stringResource(R.string.unsupported_patches), + onHelpClick = { showUnsupportedPatchesDialog = true } + ) + } + } + } + ) + } + } +} + +@Composable +private fun SelectionWarningDialog(onDismiss: () -> Unit) { + SafeguardDialog( + onDismiss = onDismiss, + title = R.string.warning, + body = stringResource(R.string.selection_warning_description), + ) +} + +@Composable +private fun UniversalPatchWarningDialog( + onCancel: () -> Unit, + onConfirm: () -> Unit +) { + AlertDialog( + onDismissRequest = onCancel, + confirmButton = { + TextButton(onClick = onConfirm) { + Text(stringResource(R.string.continue_)) + } + }, + dismissButton = { + TextButton(onClick = onCancel) { + Text(stringResource(R.string.cancel)) + } + }, + icon = { + Icon(Icons.Outlined.WarningAmber, null) + }, + title = { + Text( + text = stringResource(R.string.warning), + style = MaterialTheme.typography.headlineSmall.copy(textAlign = TextAlign.Center) + ) + }, + text = { + Text(stringResource(R.string.universal_patch_warning_description)) + } + ) +} + +@Composable +private fun PatchItem( + patch: PatchInfo, + onOptionsDialog: () -> Unit, + selected: Boolean, + onToggle: () -> Unit, + supported: Boolean = true +) = ListItem( + modifier = Modifier + .let { if (!supported) it.alpha(0.5f) else it } + .clickable(onClick = onToggle) + .fillMaxSize(), + leadingContent = { + HapticCheckbox( + checked = selected, + onCheckedChange = { onToggle() }, + enabled = supported + ) + }, + headlineContent = { Text(patch.name) }, + supportingContent = patch.description?.let { { Text(it) } }, + trailingContent = { + if (patch.options?.isNotEmpty() == true) { + IconButton(onClick = onOptionsDialog, enabled = supported) { + Icon(Icons.Outlined.Settings, null) + } + } + }, + colors = transparentListItemColors +) + +@Composable +fun ListHeader( + title: String, + onHelpClick: (() -> Unit)? = null +) { + ListItem( + headlineContent = { + Text( + text = title, + color = MaterialTheme.colorScheme.primary, + style = MaterialTheme.typography.labelLarge + ) + }, + trailingContent = onHelpClick?.let { + { + IconButton(onClick = it) { + Icon( + Icons.AutoMirrored.Outlined.HelpOutline, + stringResource(R.string.help) + ) + } + } + }, + colors = transparentListItemColors + ) +} + +@Composable +private fun UnsupportedPatchesDialog( + appVersion: String, + onDismissRequest: () -> Unit +) = AlertDialog( + icon = { + Icon(Icons.Outlined.WarningAmber, null) + }, + onDismissRequest = onDismissRequest, + confirmButton = { + TextButton(onClick = onDismissRequest) { + Text(stringResource(R.string.ok)) + } + }, + title = { Text(stringResource(R.string.unsupported_patches)) }, + text = { + Text( + stringResource( + R.string.unsupported_patches_dialog, + appVersion + ) + ) + } +) + +@Composable +private fun UnsupportedPatchDialog( + appVersion: String, + supportedVersions: List<String>, + onDismissRequest: () -> Unit +) = AlertDialog( + icon = { + Icon(Icons.Outlined.WarningAmber, null) + }, + onDismissRequest = onDismissRequest, + confirmButton = { + TextButton(onClick = onDismissRequest) { + Text(stringResource(R.string.ok)) + } + }, + title = { Text(stringResource(R.string.unsupported_patch)) }, + text = { + Text( + stringResource( + R.string.app_not_supported, + appVersion, + supportedVersions.joinToString(", ") + ) + ) + } +) + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +private fun OptionsDialog( + patch: PatchInfo, + values: Map<String, Any?>?, + reset: () -> Unit, + set: (String, Any?) -> Unit, + onDismissRequest: () -> Unit, +) = Dialog( + onDismissRequest = onDismissRequest, + properties = DialogProperties( + usePlatformDefaultWidth = false, + dismissOnBackPress = true + ) +) { + Scaffold( + topBar = { + AppTopBar( + title = patch.name, + onBackClick = onDismissRequest, + actions = { + IconButton(onClick = reset) { + Icon(Icons.Outlined.Restore, stringResource(R.string.reset)) + } + } + ) + } + ) { paddingValues -> + LazyColumnWithScrollbar( + modifier = Modifier.padding(paddingValues) + ) { + if (patch.options == null) return@LazyColumnWithScrollbar + + items(patch.options, key = { it.key }) { option -> + val key = option.key + val value = + if (values == null || !values.contains(key)) option.default else values[key] + + @Suppress("UNCHECKED_CAST") + OptionItem( + option = option as Option<Any>, + value = value, + setValue = { + set(key, it) + } + ) + } + } + } +} diff --git a/app/src/main/java/app/revanced/manager/ui/screen/RequiredOptionsScreen.kt b/app/src/main/java/app/revanced/manager/ui/screen/RequiredOptionsScreen.kt new file mode 100644 index 0000000000..1daa4c5fac --- /dev/null +++ b/app/src/main/java/app/revanced/manager/ui/screen/RequiredOptionsScreen.kt @@ -0,0 +1,165 @@ +package app.revanced.manager.ui.screen + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.pager.HorizontalPager +import androidx.compose.foundation.pager.rememberPagerState +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.AutoFixHigh +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.ScrollableTabRow +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBarDefaults +import androidx.compose.material3.rememberTopAppBarState +import androidx.compose.material3.surfaceColorAtElevation +import androidx.compose.runtime.Composable +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.input.nestedscroll.nestedScroll +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import app.revanced.manager.R +import app.revanced.manager.patcher.patch.Option +import app.revanced.manager.ui.component.AppTopBar +import app.revanced.manager.ui.component.LazyColumnWithScrollbar +import app.revanced.manager.ui.component.haptics.HapticExtendedFloatingActionButton +import app.revanced.manager.ui.component.haptics.HapticTab +import app.revanced.manager.ui.component.patches.OptionItem +import app.revanced.manager.ui.model.BundleInfo.Extensions.requiredOptionsSet +import app.revanced.manager.ui.viewmodel.PatchesSelectorViewModel +import app.revanced.manager.util.Options +import app.revanced.manager.util.PatchSelection +import app.revanced.manager.util.isScrollingUp +import kotlinx.coroutines.launch + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun RequiredOptionsScreen( + onContinue: (PatchSelection?, Options) -> Unit, + onBackClick: () -> Unit, + vm: PatchesSelectorViewModel +) { + val list by vm.requiredOptsPatches.collectAsStateWithLifecycle(emptyList()) + + val pagerState = rememberPagerState( + initialPage = 0, + initialPageOffsetFraction = 0f + ) { + list.size + } + val patchLazyListStates = remember(list) { List(list.size, ::LazyListState) } + val bundles by vm.bundlesFlow.collectAsStateWithLifecycle(emptyList()) + val showContinueButton by remember { + derivedStateOf { + bundles.requiredOptionsSet( + isSelected = { bundle, patch -> vm.isSelected(bundle.uid, patch) }, + optionsForPatch = { bundle, patch -> vm.getOptions(bundle.uid, patch) } + ) + } + } + val composableScope = rememberCoroutineScope() + + val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState()) + + Scaffold( + topBar = { + AppTopBar( + title = stringResource(R.string.required_options_screen), + scrollBehavior = scrollBehavior, + onBackClick = onBackClick + ) + }, + floatingActionButton = { + if (!showContinueButton) return@Scaffold + + HapticExtendedFloatingActionButton( + text = { Text(stringResource(R.string.patch)) }, + icon = { + Icon( + Icons.Default.AutoFixHigh, + stringResource(R.string.patch) + ) + }, + expanded = patchLazyListStates.getOrNull(pagerState.currentPage)?.isScrollingUp + ?: true, + onClick = { + onContinue(vm.getCustomSelection(), vm.getOptions()) + } + ) + }, + modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection), + ) { paddingValues -> + Column( + Modifier + .fillMaxSize() + .padding(paddingValues) + ) { + if (list.isEmpty()) return@Column + else if (list.size > 1) ScrollableTabRow( + selectedTabIndex = pagerState.currentPage, + containerColor = MaterialTheme.colorScheme.surfaceColorAtElevation(3.0.dp) + ) { + list.forEachIndexed { index, (bundle, _) -> + HapticTab( + selected = pagerState.currentPage == index, + onClick = { + composableScope.launch { + pagerState.animateScrollToPage( + index + ) + } + }, + text = { Text(bundle.name) }, + selectedContentColor = MaterialTheme.colorScheme.primary, + unselectedContentColor = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + } + + HorizontalPager( + state = pagerState, + userScrollEnabled = true, + pageContent = { index -> + // Avoid crashing if the lists have not been fully initialized yet. + if (index > list.lastIndex || list.size != patchLazyListStates.size) return@HorizontalPager + val (bundle, patches) = list[index] + + LazyColumnWithScrollbar( + modifier = Modifier.fillMaxSize(), + state = patchLazyListStates[index] + ) { + items(patches, key = { it.name }) { + ListHeader(it.name) + + val values = vm.getOptions(bundle.uid, it) + it.options?.forEach { option -> + val key = option.key + val value = + if (values == null || key !in values) option.default else values[key] + + @Suppress("UNCHECKED_CAST") + OptionItem( + option = option as Option<Any>, + value = value, + setValue = { new -> + vm.setOption(bundle.uid, it, key, new) + } + ) + } + } + } + } + ) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/ui/screen/SelectedAppInfoScreen.kt b/app/src/main/java/app/revanced/manager/ui/screen/SelectedAppInfoScreen.kt new file mode 100644 index 0000000000..35f2546f29 --- /dev/null +++ b/app/src/main/java/app/revanced/manager/ui/screen/SelectedAppInfoScreen.kt @@ -0,0 +1,324 @@ +package app.revanced.manager.ui.screen + +import androidx.activity.compose.rememberLauncherForActivityResult +import androidx.activity.result.contract.ActivityResultContracts +import androidx.annotation.StringRes +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.outlined.ArrowRight +import androidx.compose.material.icons.filled.AutoFixHigh +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.ListItem +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.material3.TopAppBarDefaults +import androidx.compose.material3.rememberTopAppBarState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +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.compose.ui.unit.dp +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import app.revanced.manager.R +import app.revanced.manager.data.room.apps.installed.InstallType +import app.revanced.manager.data.room.apps.installed.InstalledApp +import app.revanced.manager.network.downloader.LoadedDownloaderPlugin +import app.revanced.manager.ui.component.AlertDialogExtended +import app.revanced.manager.ui.component.AppInfo +import app.revanced.manager.ui.component.AppTopBar +import app.revanced.manager.ui.component.ColumnWithScrollbar +import app.revanced.manager.ui.component.LoadingIndicator +import app.revanced.manager.ui.component.haptics.HapticExtendedFloatingActionButton +import app.revanced.manager.ui.model.SelectedApp +import app.revanced.manager.ui.viewmodel.SelectedAppInfoViewModel +import app.revanced.manager.util.EventEffect +import app.revanced.manager.util.Options +import app.revanced.manager.util.PatchSelection +import app.revanced.manager.util.enabled +import app.revanced.manager.util.toast +import app.revanced.manager.util.transparentListItemColors +import kotlinx.coroutines.launch + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun SelectedAppInfoScreen( + onPatchSelectorClick: (SelectedApp, PatchSelection?, Options) -> Unit, + onRequiredOptions: (SelectedApp, PatchSelection?, Options) -> Unit, + onPatchClick: () -> Unit, + onBackClick: () -> Unit, + vm: SelectedAppInfoViewModel +) { + val context = LocalContext.current + + val packageName = vm.selectedApp.packageName + val version = vm.selectedApp.version + val bundles by vm.bundleInfoFlow.collectAsStateWithLifecycle(emptyList()) + + val allowIncompatiblePatches by vm.prefs.disablePatchVersionCompatCheck.getAsState() + val patches = remember(bundles, allowIncompatiblePatches) { + vm.getPatches(bundles, allowIncompatiblePatches) + } + val selectedPatchCount = remember(patches) { + patches.values.sumOf { it.size } + } + + val launcher = rememberLauncherForActivityResult( + contract = ActivityResultContracts.StartActivityForResult(), + onResult = vm::handlePluginActivityResult + ) + EventEffect(flow = vm.launchActivityFlow) { intent -> + launcher.launch(intent) + } + val composableScope = rememberCoroutineScope() + + val error by vm.errorFlow.collectAsStateWithLifecycle(null) + + val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState()) + + Scaffold( + topBar = { + AppTopBar( + title = stringResource(R.string.app_info), + scrollBehavior = scrollBehavior, + onBackClick = onBackClick + ) + }, + floatingActionButton = { + if (error != null) return@Scaffold + + HapticExtendedFloatingActionButton( + text = { Text(stringResource(R.string.patch)) }, + icon = { + Icon( + Icons.Default.AutoFixHigh, + stringResource(R.string.patch) + ) + }, + onClick = patchClick@{ + if (selectedPatchCount == 0) { + context.toast(context.getString(R.string.no_patches_selected)) + + return@patchClick + } + + composableScope.launch { + if (!vm.hasSetRequiredOptions(patches)) { + onRequiredOptions( + vm.selectedApp, + vm.getCustomPatches(bundles, allowIncompatiblePatches), + vm.options + ) + return@launch + } + + onPatchClick() + } + } + ) + }, + modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection), + ) { paddingValues -> + val plugins by vm.plugins.collectAsStateWithLifecycle(emptyList()) + + if (vm.showSourceSelector) { + val requiredVersion by vm.requiredVersion.collectAsStateWithLifecycle(null) + + AppSourceSelectorDialog( + plugins = plugins, + installedApp = vm.installedAppData, + searchApp = SelectedApp.Search( + vm.packageName, + vm.desiredVersion + ), + activeSearchJob = vm.activePluginAction, + hasRoot = vm.hasRoot, + onDismissRequest = vm::dismissSourceSelector, + onSelectPlugin = vm::searchUsingPlugin, + requiredVersion = requiredVersion, + onSelect = { + vm.selectedApp = it + vm.dismissSourceSelector() + } + ) + } + + ColumnWithScrollbar( + modifier = Modifier + .fillMaxSize() + .padding(paddingValues) + ) { + AppInfo(vm.selectedAppInfo, placeholderLabel = packageName) { + Text( + version ?: stringResource(R.string.selected_app_meta_any_version), + color = MaterialTheme.colorScheme.onSurfaceVariant, + style = MaterialTheme.typography.bodyMedium, + ) + } + + PageItem( + R.string.patch_selector_item, + stringResource( + R.string.patch_selector_item_description, + selectedPatchCount + ), + onClick = { + onPatchSelectorClick( + vm.selectedApp, + vm.getCustomPatches( + bundles, + allowIncompatiblePatches + ), + vm.options + ) + } + ) + PageItem( + R.string.apk_source_selector_item, + when (val app = vm.selectedApp) { + is SelectedApp.Search -> stringResource(R.string.apk_source_auto) + is SelectedApp.Installed -> stringResource(R.string.apk_source_installed) + is SelectedApp.Download -> stringResource( + R.string.apk_source_downloader, + plugins.find { it.packageName == app.data.pluginPackageName }?.name + ?: app.data.pluginPackageName + ) + + is SelectedApp.Local -> stringResource(R.string.apk_source_local) + }, + onClick = { + vm.showSourceSelector() + } + ) + error?.let { + Text( + stringResource(it.resourceId), + color = MaterialTheme.colorScheme.error, + modifier = Modifier.padding(horizontal = 24.dp) + ) + } + } + } +} + +@Composable +private fun PageItem(@StringRes title: Int, description: String, onClick: () -> Unit) { + ListItem( + modifier = Modifier + .clickable(onClick = onClick) + .padding(start = 8.dp), + headlineContent = { + Text( + stringResource(title), + color = MaterialTheme.colorScheme.onSurface, + style = MaterialTheme.typography.titleLarge + ) + }, + supportingContent = { + Text( + description, + color = MaterialTheme.colorScheme.outline, + style = MaterialTheme.typography.bodyMedium + ) + }, + trailingContent = { + Icon(Icons.AutoMirrored.Outlined.ArrowRight, null) + } + ) +} + +@Composable +private fun AppSourceSelectorDialog( + plugins: List<LoadedDownloaderPlugin>, + installedApp: Pair<SelectedApp.Installed, InstalledApp?>?, + searchApp: SelectedApp.Search, + activeSearchJob: String?, + hasRoot: Boolean, + requiredVersion: String?, + onDismissRequest: () -> Unit, + onSelectPlugin: (LoadedDownloaderPlugin) -> Unit, + onSelect: (SelectedApp) -> Unit, +) { + val canSelect = activeSearchJob == null + + AlertDialogExtended( + onDismissRequest = onDismissRequest, + confirmButton = { + TextButton(onClick = onDismissRequest) { + Text(stringResource(R.string.cancel)) + } + }, + title = { Text(stringResource(R.string.app_source_dialog_title)) }, + textHorizontalPadding = PaddingValues(horizontal = 0.dp), + text = { + LazyColumn { + item(key = "auto") { + val hasPlugins = plugins.isNotEmpty() + ListItem( + modifier = Modifier + .clickable(enabled = canSelect && hasPlugins) { onSelect(searchApp) } + .enabled(hasPlugins), + headlineContent = { Text(stringResource(R.string.app_source_dialog_option_auto)) }, + supportingContent = { + Text( + if (hasPlugins) + stringResource(R.string.app_source_dialog_option_auto_description) + else + stringResource(R.string.app_source_dialog_option_auto_unavailable) + ) + }, + colors = transparentListItemColors + ) + } + + installedApp?.let { (app, meta) -> + item(key = "installed") { + val (usable, text) = when { + // Mounted apps must be unpatched before patching, which cannot be done without root access. + meta?.installType == InstallType.MOUNT && !hasRoot -> false to stringResource( + R.string.app_source_dialog_option_installed_no_root + ) + // Patching already patched apps is not allowed because patches expect unpatched apps. + meta?.installType == InstallType.DEFAULT -> false to stringResource(R.string.already_patched) + // Version does not match suggested version. + requiredVersion != null && app.version != requiredVersion -> false to stringResource( + R.string.app_source_dialog_option_installed_version_not_suggested, + app.version + ) + + else -> true to app.version + } + ListItem( + modifier = Modifier + .clickable(enabled = canSelect && usable) { onSelect(app) } + .enabled(usable), + headlineContent = { Text(stringResource(R.string.installed)) }, + supportingContent = { Text(text) }, + colors = transparentListItemColors + ) + } + } + + items(plugins, key = { "plugin_${it.packageName}" }) { plugin -> + ListItem( + modifier = Modifier.clickable(enabled = canSelect) { onSelectPlugin(plugin) }, + headlineContent = { Text(plugin.name) }, + trailingContent = (@Composable { LoadingIndicator() }).takeIf { activeSearchJob == plugin.packageName }, + colors = transparentListItemColors + ) + } + } + } + ) +} \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/ui/screen/SettingsScreen.kt b/app/src/main/java/app/revanced/manager/ui/screen/SettingsScreen.kt new file mode 100644 index 0000000000..46a25b1036 --- /dev/null +++ b/app/src/main/java/app/revanced/manager/ui/screen/SettingsScreen.kt @@ -0,0 +1,77 @@ +package app.revanced.manager.ui.screen + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.* +import androidx.compose.material3.* +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import app.revanced.manager.R +import app.revanced.manager.ui.component.AppTopBar +import app.revanced.manager.ui.component.ColumnWithScrollbar +import app.revanced.manager.ui.component.settings.SettingsListItem +import app.revanced.manager.ui.model.navigation.Settings + +private val settingsSections = listOf( + Triple( + R.string.general, + R.string.general_description, + Icons.Outlined.Settings + ) to Settings.General, + Triple( + R.string.updates, + R.string.updates_description, + Icons.Outlined.Update + ) to Settings.Updates, + Triple( + R.string.downloads, + R.string.downloads_description, + Icons.Outlined.Download + ) to Settings.Downloads, + Triple( + R.string.import_export, + R.string.import_export_description, + Icons.Outlined.SwapVert + ) to Settings.ImportExport, + Triple( + R.string.advanced, + R.string.advanced_description, + Icons.Outlined.Tune + ) to Settings.Advanced, + Triple( + R.string.about, + R.string.app_name, + Icons.Outlined.Info + ) to Settings.About, +) + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun SettingsScreen(onBackClick: () -> Unit, navigate: (Settings.Destination) -> Unit) { + Scaffold( + topBar = { + AppTopBar( + title = stringResource(R.string.settings), + onBackClick = onBackClick, + ) + } + ) { paddingValues -> + ColumnWithScrollbar( + modifier = Modifier + .padding(paddingValues) + .fillMaxSize() + ) { + settingsSections.forEach { (titleDescIcon, destination) -> + SettingsListItem( + modifier = Modifier.clickable { navigate(destination) }, + headlineContent = stringResource(titleDescIcon.first), + supportingContent = stringResource(titleDescIcon.second), + leadingContent = { Icon(titleDescIcon.third, null) } + ) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/ui/screen/UpdateScreen.kt b/app/src/main/java/app/revanced/manager/ui/screen/UpdateScreen.kt new file mode 100644 index 0000000000..21e5d3fc91 --- /dev/null +++ b/app/src/main/java/app/revanced/manager/ui/screen/UpdateScreen.kt @@ -0,0 +1,244 @@ +package app.revanced.manager.ui.screen + +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.core.spring +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.ColumnScope +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.Update +import androidx.compose.material3.AlertDialog +import androidx.compose.material3.Button +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.Icon +import androidx.compose.material3.LinearProgressIndicator +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.material3.TopAppBarDefaults +import androidx.compose.material3.rememberTopAppBarState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.Stable +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.stringResource +import androidx.compose.ui.unit.dp +import app.revanced.manager.BuildConfig +import app.revanced.manager.R +import app.revanced.manager.network.dto.ReVancedAsset +import app.revanced.manager.ui.component.AppTopBar +import app.revanced.manager.ui.component.settings.Changelog +import app.revanced.manager.ui.viewmodel.UpdateViewModel +import app.revanced.manager.ui.viewmodel.UpdateViewModel.State +import app.revanced.manager.util.relativeTime +import com.gigamole.composefadingedges.content.FadingEdgesContentType +import com.gigamole.composefadingedges.content.scrollconfig.FadingEdgesScrollConfig +import com.gigamole.composefadingedges.fill.FadingEdgesFillType +import com.gigamole.composefadingedges.verticalFadingEdges +import org.koin.androidx.compose.koinViewModel + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +@Stable +fun UpdateScreen( + onBackClick: () -> Unit, + vm: UpdateViewModel = koinViewModel() +) { + val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState()) + + Scaffold( + topBar = { + AppTopBar( + title = stringResource(R.string.update), + scrollBehavior = scrollBehavior, + onBackClick = onBackClick + ) + }, + modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection), + ) { paddingValues -> + AnimatedVisibility(visible = vm.showInternetCheckDialog) { + MeteredDownloadConfirmationDialog( + onDismiss = { vm.showInternetCheckDialog = false }, + onDownloadAnyways = { vm.downloadUpdate(true) } + ) + } + Column( + modifier = Modifier + .fillMaxSize() + .padding(paddingValues) + .padding(vertical = 16.dp, horizontal = 24.dp) + .verticalScroll(rememberScrollState()), + verticalArrangement = Arrangement.spacedBy(32.dp) + ) { + Header( + vm.state, + vm.releaseInfo, + DownloadData(vm.downloadProgress, vm.downloadedSize, vm.totalSize) + ) + vm.releaseInfo?.let { changelog -> + HorizontalDivider() + Changelog(changelog) + } ?: Spacer(modifier = Modifier.weight(1f)) + Buttons(vm.state, vm::downloadUpdate, vm::installUpdate, onBackClick) + } + } +} + +@Composable +private fun MeteredDownloadConfirmationDialog( + onDismiss: () -> Unit, + onDownloadAnyways: () -> Unit +) { + AlertDialog( + onDismissRequest = onDismiss, + dismissButton = { + TextButton(onDismiss) { + Text(stringResource(R.string.cancel)) + } + }, + confirmButton = { + TextButton( + onClick = { + onDismiss() + onDownloadAnyways() + } + ) { + Text(stringResource(R.string.download)) + } + }, + title = { Text(stringResource(R.string.download_update_confirmation)) }, + icon = { Icon(Icons.Outlined.Update, null) }, + text = { Text(stringResource(R.string.download_confirmation_metered)) } + ) +} + +@Composable +private fun Header(state: State, releaseInfo: ReVancedAsset?, downloadData: DownloadData) { + Column(verticalArrangement = Arrangement.spacedBy(16.dp)) { + Text( + text = stringResource(state.title), + style = MaterialTheme.typography.headlineMedium + ) + if (state == State.CAN_DOWNLOAD) { + Column { + Text( + text = stringResource( + id = R.string.current_version, + BuildConfig.VERSION_NAME + ), + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + releaseInfo?.version?.let { + Text( + text = stringResource( + R.string.new_version, + it.replace("v", "") + ), + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + } + } else if (state == State.DOWNLOADING) { + LinearProgressIndicator( + progress = { downloadData.downloadProgress }, + modifier = Modifier.fillMaxWidth(), + ) + Text( + text = + "${downloadData.downloadedSize.div(1000000)} MB / ${ + downloadData.totalSize.div( + 1000000 + ) + } MB (${ + downloadData.downloadProgress.times( + 100 + ).toInt() + }%)", + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.outline, + modifier = Modifier.align(Alignment.CenterHorizontally) + ) + } + } +} + +@Composable +private fun ColumnScope.Changelog(releaseInfo: ReVancedAsset) { + val scrollState = rememberScrollState() + Column( + modifier = Modifier + .weight(1f) + .verticalScroll(scrollState) + .verticalFadingEdges( + fillType = FadingEdgesFillType.FadeColor( + color = MaterialTheme.colorScheme.background, + fillStops = Triple(0F, 0.55F, 1F), + secondStopAlpha = 1F + ), + contentType = FadingEdgesContentType.Dynamic.Scroll( + state = scrollState, + scrollConfig = FadingEdgesScrollConfig.Dynamic( + animationSpec = spring(), + isLerpByDifferenceForPartialContent = true, + scrollFactor = 1.25F + ) + ), + length = 350.dp + ) + ) { + Changelog( + markdown = releaseInfo.description.replace("`", ""), + version = releaseInfo.version, + publishDate = releaseInfo.createdAt.relativeTime(LocalContext.current) + ) + } +} + +@Composable +private fun Buttons( + state: State, + onDownloadClick: () -> Unit, + onInstallClick: () -> Unit, + onBackClick: () -> Unit +) { + Row(modifier = Modifier.fillMaxWidth()) { + if (state.showCancel) { + TextButton( + onClick = onBackClick, + ) { + Text(text = stringResource(R.string.cancel)) + } + } + Spacer(modifier = Modifier.weight(1f)) + if (state == State.CAN_DOWNLOAD) { + Button(onClick = onDownloadClick) { + Text(text = stringResource(R.string.update)) + } + } else if (state == State.CAN_INSTALL) { + Button( + onClick = onInstallClick + ) { + Text(text = stringResource(R.string.install_app)) + } + } + } +} + +data class DownloadData( + val downloadProgress: Float, + val downloadedSize: Long, + val totalSize: Long +) \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/ui/screen/settings/AboutSettingsScreen.kt b/app/src/main/java/app/revanced/manager/ui/screen/settings/AboutSettingsScreen.kt new file mode 100644 index 0000000000..3b5e0cbcfd --- /dev/null +++ b/app/src/main/java/app/revanced/manager/ui/screen/settings/AboutSettingsScreen.kt @@ -0,0 +1,247 @@ +package app.revanced.manager.ui.screen.settings + +import androidx.appcompat.content.res.AppCompatResources +import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.Image +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.ExperimentalLayoutApi +import androidx.compose.foundation.layout.FlowRow +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.FavoriteBorder +import androidx.compose.material.icons.outlined.MailOutline +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.FilledTonalButton +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +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.rememberTopAppBarState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +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.stringResource +import androidx.compose.ui.unit.dp +import app.revanced.manager.BuildConfig +import app.revanced.manager.R +import app.revanced.manager.network.dto.ReVancedSocial +import app.revanced.manager.ui.component.AppTopBar +import app.revanced.manager.ui.component.ColumnWithScrollbar +import app.revanced.manager.ui.component.settings.SettingsListItem +import app.revanced.manager.ui.model.navigation.Settings +import app.revanced.manager.ui.viewmodel.AboutViewModel +import app.revanced.manager.ui.viewmodel.AboutViewModel.Companion.getSocialIcon +import app.revanced.manager.util.openUrl +import com.google.accompanist.drawablepainter.rememberDrawablePainter +import org.koin.androidx.compose.koinViewModel + +@OptIn(ExperimentalMaterial3Api::class, ExperimentalLayoutApi::class) +@Composable +fun AboutSettingsScreen( + onBackClick: () -> Unit, + navigate: (Settings.Destination) -> Unit, + viewModel: AboutViewModel = koinViewModel() +) { + val context = LocalContext.current + // painterResource() is broken on release builds for some reason. + val icon = rememberDrawablePainter(drawable = remember { + AppCompatResources.getDrawable(context, R.drawable.ic_logo_ring) + }) + + val (preferredSocials, socials) = remember(viewModel.socials) { + viewModel.socials.partition(ReVancedSocial::preferred) + } + + val preferredSocialButtons = remember(preferredSocials, viewModel.donate, viewModel.contact) { + preferredSocials.map { + Triple( + getSocialIcon(it.name), + it.name, + third = { + context.openUrl(it.url) + } + ) + } + listOfNotNull( + viewModel.donate?.let { + Triple( + Icons.Outlined.FavoriteBorder, + context.getString(R.string.donate), + third = { + context.openUrl(it) + } + ) + }, + viewModel.contact?.let { + Triple( + Icons.Outlined.MailOutline, + context.getString(R.string.contact), + third = { + context.openUrl("mailto:$it") + } + ) + } + ) + } + + val socialButtons = remember(socials) { + socials.map { + Triple( + getSocialIcon(it.name), + it.name, + third = { + context.openUrl(it.url) + } + ) + } + } + + val listItems = listOfNotNull( + Triple(stringResource(R.string.submit_feedback), + stringResource(R.string.submit_feedback_description), + third = { + context.openUrl("https://github.com/ReVanced/revanced-manager/issues/new/choose") + }), + Triple( + stringResource(R.string.contributors), + stringResource(R.string.contributors_description), + third = { navigate(Settings.Contributors) } + ), + Triple( + stringResource(R.string.developer_options), + stringResource(R.string.developer_options_description), + third = { navigate(Settings.DeveloperOptions) } + ), + Triple( + stringResource(R.string.opensource_licenses), + stringResource(R.string.opensource_licenses_description), + third = { navigate(Settings.Licenses) } + ) + ) + + val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState()) + + Scaffold( + topBar = { + AppTopBar( + title = stringResource(R.string.about), + scrollBehavior = scrollBehavior, + onBackClick = onBackClick + ) + }, + modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection), + ) { paddingValues -> + ColumnWithScrollbar( + modifier = Modifier + .fillMaxSize() + .padding(paddingValues), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(16.dp) + ) { + Image( + modifier = Modifier.padding(top = 16.dp), + painter = icon, + contentDescription = null + ) + Column( + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(4.dp) + ) { + Text( + stringResource(R.string.app_name), + style = MaterialTheme.typography.headlineSmall + ) + Text( + text = stringResource(R.string.version) + " " + BuildConfig.VERSION_NAME + " (" + BuildConfig.VERSION_CODE + ")", + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.outline + ) + } + FlowRow( + maxItemsInEachRow = 2, + horizontalArrangement = Arrangement.spacedBy(8.dp, Alignment.CenterHorizontally), + modifier = Modifier.padding(horizontal = 16.dp) + ) { + preferredSocialButtons.forEach { (icon, text, onClick) -> + FilledTonalButton( + onClick = onClick, + modifier = Modifier.weight(1f), + ) { + Row( + horizontalArrangement = Arrangement.spacedBy(8.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Icon( + icon, + contentDescription = null, + modifier = Modifier.size(18.dp) + ) + Text( + text, + style = MaterialTheme.typography.labelLarge + ) + } + } + } + } + FlowRow( + horizontalArrangement = Arrangement.spacedBy(8.dp, Alignment.CenterHorizontally) + ) { + socialButtons.forEach { (icon, text, onClick) -> + IconButton( + onClick = onClick, + modifier = Modifier.padding(end = 8.dp), + ) { + Icon( + icon, + contentDescription = text, + modifier = Modifier.size(28.dp), + tint = MaterialTheme.colorScheme.secondary + ) + } + } + } + OutlinedCard( + modifier = Modifier.padding(horizontal = 16.dp), + border = BorderStroke(1.dp, MaterialTheme.colorScheme.outlineVariant) + ) { + Column( + modifier = Modifier.padding(16.dp), + verticalArrangement = Arrangement.spacedBy(8.dp) + ) { + Text( + text = stringResource(R.string.about_revanced_manager), + style = MaterialTheme.typography.titleMedium + ) + Text( + text = stringResource(R.string.revanced_manager_description), + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + } + Column { + listItems.forEach { (title, description, onClick) -> + SettingsListItem( + modifier = Modifier + .fillMaxWidth() + .clickable { onClick() }, + headlineContent = title, + supportingContent = description + ) + } + } + } + } +} diff --git a/app/src/main/java/app/revanced/manager/ui/screen/settings/AdvancedSettingsScreen.kt b/app/src/main/java/app/revanced/manager/ui/screen/settings/AdvancedSettingsScreen.kt new file mode 100644 index 0000000000..ac3dfc7d17 --- /dev/null +++ b/app/src/main/java/app/revanced/manager/ui/screen/settings/AdvancedSettingsScreen.kt @@ -0,0 +1,254 @@ +package app.revanced.manager.ui.screen.settings + +import android.app.ActivityManager +import android.content.ClipData +import android.content.ClipboardManager +import android.os.Build +import android.view.HapticFeedbackConstants +import androidx.activity.compose.rememberLauncherForActivityResult +import androidx.activity.result.contract.ActivityResultContracts +import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.clickable +import androidx.compose.foundation.combinedClickable +import androidx.compose.foundation.layout.Arrangement +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.material.icons.Icons +import androidx.compose.material.icons.outlined.Api +import androidx.compose.material.icons.outlined.Restore +import androidx.compose.material3.AlertDialog +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +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.rememberTopAppBarState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.saveable.rememberSaveable +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.stringResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import androidx.core.content.getSystemService +import androidx.lifecycle.viewModelScope +import app.revanced.manager.BuildConfig +import app.revanced.manager.R +import app.revanced.manager.ui.component.AppTopBar +import app.revanced.manager.ui.component.ColumnWithScrollbar +import app.revanced.manager.ui.component.GroupHeader +import app.revanced.manager.ui.component.settings.BooleanItem +import app.revanced.manager.ui.component.settings.IntegerItem +import app.revanced.manager.ui.component.settings.SafeguardBooleanItem +import app.revanced.manager.ui.component.settings.SettingsListItem +import app.revanced.manager.ui.viewmodel.AdvancedSettingsViewModel +import app.revanced.manager.util.toast +import app.revanced.manager.util.withHapticFeedback +import org.koin.androidx.compose.koinViewModel + +@OptIn(ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class) +@Composable +fun AdvancedSettingsScreen( + onBackClick: () -> Unit, + vm: AdvancedSettingsViewModel = koinViewModel() +) { + val context = LocalContext.current + val memoryLimit = remember { + val activityManager = context.getSystemService<ActivityManager>()!! + context.getString( + R.string.device_memory_limit_format, + activityManager.memoryClass, + activityManager.largeMemoryClass + ) + } + val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState()) + + Scaffold( + topBar = { + AppTopBar( + title = stringResource(R.string.advanced), + scrollBehavior = scrollBehavior, + onBackClick = onBackClick + ) + }, + modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection), + ) { paddingValues -> + ColumnWithScrollbar( + modifier = Modifier + .fillMaxSize() + .padding(paddingValues) + ) { + GroupHeader(stringResource(R.string.manager)) + + val apiUrl by vm.prefs.api.getAsState() + var showApiUrlDialog by rememberSaveable { mutableStateOf(false) } + + if (showApiUrlDialog) { + APIUrlDialog( + currentUrl = apiUrl, + defaultUrl = vm.prefs.api.default, + onSubmit = { + showApiUrlDialog = false + it?.let(vm::setApiUrl) + } + ) + } + SettingsListItem( + headlineContent = stringResource(R.string.api_url), + supportingContent = stringResource(R.string.api_url_description), + modifier = Modifier.clickable { + showApiUrlDialog = true + } + ) + + GroupHeader(stringResource(R.string.patcher)) + BooleanItem( + preference = vm.prefs.useProcessRuntime, + coroutineScope = vm.viewModelScope, + headline = R.string.process_runtime, + description = R.string.process_runtime_description, + ) + IntegerItem( + preference = vm.prefs.patcherProcessMemoryLimit, + coroutineScope = vm.viewModelScope, + headline = R.string.process_runtime_memory_limit, + description = R.string.process_runtime_memory_limit_description, + ) + + GroupHeader(stringResource(R.string.safeguards)) + SafeguardBooleanItem( + preference = vm.prefs.disablePatchVersionCompatCheck, + coroutineScope = vm.viewModelScope, + headline = R.string.patch_compat_check, + description = R.string.patch_compat_check_description, + confirmationText = R.string.patch_compat_check_confirmation + ) + SafeguardBooleanItem( + preference = vm.prefs.disableUniversalPatchWarning, + coroutineScope = vm.viewModelScope, + headline = R.string.universal_patches_safeguard, + description = R.string.universal_patches_safeguard_description, + confirmationText = R.string.universal_patches_safeguard_confirmation + ) + SafeguardBooleanItem( + preference = vm.prefs.suggestedVersionSafeguard, + coroutineScope = vm.viewModelScope, + headline = R.string.suggested_version_safeguard, + description = R.string.suggested_version_safeguard_description, + confirmationText = R.string.suggested_version_safeguard_confirmation + ) + SafeguardBooleanItem( + preference = vm.prefs.disableSelectionWarning, + coroutineScope = vm.viewModelScope, + headline = R.string.patch_selection_safeguard, + description = R.string.patch_selection_safeguard_description, + confirmationText = R.string.patch_selection_safeguard_confirmation + ) + + GroupHeader(stringResource(R.string.debugging)) + val exportDebugLogsLauncher = + rememberLauncherForActivityResult(ActivityResultContracts.CreateDocument("text/plain")) { + it?.let(vm::exportDebugLogs) + } + SettingsListItem( + headlineContent = stringResource(R.string.debug_logs_export), + modifier = Modifier.clickable { exportDebugLogsLauncher.launch(vm.debugLogFileName) } + ) + val clipboard = remember { context.getSystemService<ClipboardManager>()!! } + val deviceContent = """ + Version: ${BuildConfig.VERSION_NAME} (${BuildConfig.VERSION_CODE}) + Build type: ${BuildConfig.BUILD_TYPE} + Model: ${Build.MODEL} + Android version: ${Build.VERSION.RELEASE} (${Build.VERSION.SDK_INT}) + Supported Archs: ${Build.SUPPORTED_ABIS.joinToString(", ")} + Memory limit: $memoryLimit + """.trimIndent() + SettingsListItem( + modifier = Modifier.combinedClickable( + onClick = { }, + onLongClickLabel = stringResource(R.string.copy_to_clipboard), + onLongClick = { + clipboard.setPrimaryClip( + ClipData.newPlainText("Device Information", deviceContent) + ) + + context.toast(context.getString(R.string.toast_copied_to_clipboard)) + }.withHapticFeedback(HapticFeedbackConstants.LONG_PRESS) + ), + headlineContent = stringResource(R.string.about_device), + supportingContent = deviceContent + ) + } + } +} + +@Composable +private fun APIUrlDialog(currentUrl: String, defaultUrl: String, onSubmit: (String?) -> Unit) { + var url by rememberSaveable(currentUrl) { mutableStateOf(currentUrl) } + + AlertDialog( + onDismissRequest = { onSubmit(null) }, + confirmButton = { + TextButton( + onClick = { + onSubmit(url) + } + ) { + Text(stringResource(R.string.api_url_dialog_save)) + } + }, + dismissButton = { + TextButton(onClick = { onSubmit(null) }) { + Text(stringResource(R.string.cancel)) + } + }, + icon = { + Icon(Icons.Outlined.Api, null) + }, + title = { + Text( + text = stringResource(R.string.api_url_dialog_title), + style = MaterialTheme.typography.headlineSmall.copy(textAlign = TextAlign.Center), + color = MaterialTheme.colorScheme.onSurface, + ) + }, + text = { + Column( + verticalArrangement = Arrangement.spacedBy(16.dp) + ) { + Text( + text = stringResource(R.string.api_url_dialog_description), + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + Text( + text = stringResource(R.string.api_url_dialog_warning), + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.error + ) + OutlinedTextField( + modifier = Modifier.fillMaxWidth(), + value = url, + onValueChange = { url = it }, + label = { Text(stringResource(R.string.api_url)) }, + trailingIcon = { + IconButton(onClick = { url = defaultUrl }) { + Icon(Icons.Outlined.Restore, stringResource(R.string.api_url_dialog_reset)) + } + } + ) + } + } + ) +} \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/ui/screen/settings/ContributorScreen.kt b/app/src/main/java/app/revanced/manager/ui/screen/settings/ContributorScreen.kt new file mode 100644 index 0000000000..a6be70bd3b --- /dev/null +++ b/app/src/main/java/app/revanced/manager/ui/screen/settings/ContributorScreen.kt @@ -0,0 +1,208 @@ +package app.revanced.manager.ui.screen.settings + +import androidx.compose.foundation.border +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.BoxWithConstraints +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.ExperimentalLayoutApi +import androidx.compose.foundation.layout.FlowRow +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +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.layout.width +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.pager.HorizontalPager +import androidx.compose.foundation.pager.rememberPagerState +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBarDefaults +import androidx.compose.material3.rememberTopAppBarState +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.input.nestedscroll.nestedScroll +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.coerceAtMost +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.times +import app.revanced.manager.R +import app.revanced.manager.network.dto.ReVancedContributor +import app.revanced.manager.ui.component.AppTopBar +import app.revanced.manager.ui.component.LazyColumnWithScrollbar +import app.revanced.manager.ui.component.LoadingIndicator +import app.revanced.manager.ui.viewmodel.ContributorViewModel +import coil.compose.AsyncImage +import org.koin.androidx.compose.koinViewModel + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun ContributorScreen( + onBackClick: () -> Unit, + viewModel: ContributorViewModel = koinViewModel() +) { + val repositories = viewModel.repositories + val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState()) + + Scaffold( + topBar = { + AppTopBar( + title = stringResource(R.string.contributors), + scrollBehavior = scrollBehavior, + onBackClick = onBackClick + ) + }, + modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection), + ) { paddingValues -> + LazyColumnWithScrollbar( + modifier = Modifier + .fillMaxHeight() + .padding(paddingValues) + .fillMaxWidth(), + contentPadding = PaddingValues(16.dp), + verticalArrangement = if (repositories.isNullOrEmpty()) Arrangement.Center else Arrangement.spacedBy( + 24.dp + ) + ) { + repositories?.let { repositories -> + if (repositories.isEmpty()) { + item { + Text( + text = stringResource(id = R.string.no_contributors_found), + style = MaterialTheme.typography.titleLarge + ) + } + } else { + items( + items = repositories, + key = { it.name } + ) { + ContributorsCard( + title = it.name, + contributors = it.contributors + ) + } + } + } ?: item { LoadingIndicator() } + } + } +} + +@OptIn(ExperimentalLayoutApi::class) +@Composable +fun ContributorsCard( + title: String, + contributors: List<ReVancedContributor>, + itemsPerPage: Int = 12, + numberOfRows: Int = 2 +) { + val itemsPerRow = (itemsPerPage / numberOfRows) + + // Create a list of contributors grouped by itemsPerPage + val contributorsByPage = remember(itemsPerPage, contributors) { + contributors.chunked(itemsPerPage) + } + val pagerState = rememberPagerState { contributorsByPage.size } + + Card( + modifier = Modifier + .fillMaxWidth() + .border( + width = 1.dp, + color = MaterialTheme.colorScheme.surfaceContainerHigh, + shape = MaterialTheme.shapes.medium + ), + colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surfaceContainer) + ) { + Column( + modifier = Modifier.padding(16.dp), + verticalArrangement = Arrangement.spacedBy(16.dp) + ) { + Row( + horizontalArrangement = Arrangement.spacedBy(8.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = title, + style = MaterialTheme.typography.titleMedium.copy(fontWeight = FontWeight.Medium) + ) + Text( + text = "(${(pagerState.currentPage + 1)}/${pagerState.pageCount})", + color = MaterialTheme.colorScheme.onSurfaceVariant, + style = MaterialTheme.typography.labelMedium.copy(fontWeight = FontWeight.SemiBold) + ) + } + HorizontalPager( + state = pagerState, + userScrollEnabled = true, + modifier = Modifier.fillMaxSize(), + ) { page -> + BoxWithConstraints { + val spaceBetween = 16.dp + val maxWidth = this.maxWidth + val itemSize = (maxWidth - (itemsPerRow - 1) * spaceBetween) / itemsPerRow + val itemSpacing = (maxWidth - itemSize * 6) / (itemsPerRow - 1) + FlowRow( + maxItemsInEachRow = itemsPerRow, + horizontalArrangement = Arrangement.spacedBy(itemSpacing), + verticalArrangement = Arrangement.spacedBy(16.dp), + modifier = Modifier.fillMaxWidth() + ) { + contributorsByPage[page].forEach { + if (itemSize > 100.dp) { + Row( + modifier = Modifier.width(itemSize - 1.dp), // we delete 1.dp to account for not-so divisible numbers + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(12.dp) + ) { + AsyncImage( + model = it.avatarUrl, + contentDescription = it.avatarUrl, + contentScale = ContentScale.Crop, + modifier = Modifier + .size((itemSize / 3).coerceAtMost(40.dp)) + .clip(CircleShape) + ) + Text( + text = it.username, + style = MaterialTheme.typography.bodyLarge, + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + } + } else { + Box( + modifier = Modifier.width(itemSize - 1.dp), + contentAlignment = Alignment.Center + ) { + AsyncImage( + model = it.avatarUrl, + contentDescription = it.avatarUrl, + contentScale = ContentScale.Crop, + modifier = Modifier + .size(size = (itemSize - 1.dp).coerceAtMost(50.dp)) // we delete 1.dp to account for not-so divisible numbers + .clip(CircleShape) + ) + } + } + } + } + } + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/ui/screen/settings/DeveloperOptionsScreen.kt b/app/src/main/java/app/revanced/manager/ui/screen/settings/DeveloperOptionsScreen.kt new file mode 100644 index 0000000000..40a1e937a4 --- /dev/null +++ b/app/src/main/java/app/revanced/manager/ui/screen/settings/DeveloperOptionsScreen.kt @@ -0,0 +1,51 @@ +package app.revanced.manager.ui.screen.settings + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Scaffold +import androidx.compose.material3.TopAppBarDefaults +import androidx.compose.material3.rememberTopAppBarState +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.input.nestedscroll.nestedScroll +import androidx.compose.ui.res.stringResource +import app.revanced.manager.R +import app.revanced.manager.ui.component.AppTopBar +import app.revanced.manager.ui.component.GroupHeader +import app.revanced.manager.ui.component.settings.SettingsListItem +import app.revanced.manager.ui.viewmodel.DeveloperOptionsViewModel +import org.koin.androidx.compose.koinViewModel + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun DeveloperOptionsScreen( + onBackClick: () -> Unit, + vm: DeveloperOptionsViewModel = koinViewModel() +) { + val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState()) + + Scaffold( + topBar = { + AppTopBar( + title = stringResource(R.string.developer_options), + scrollBehavior = scrollBehavior, + onBackClick = onBackClick + ) + }, + modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection), + ) { paddingValues -> + Column(modifier = Modifier.padding(paddingValues)) { + GroupHeader(stringResource(R.string.patch_bundles_section)) + SettingsListItem( + headlineContent = stringResource(R.string.patch_bundles_force_download), + modifier = Modifier.clickable(onClick = vm::redownloadBundles) + ) + SettingsListItem( + headlineContent = stringResource(R.string.patch_bundles_reset), + modifier = Modifier.clickable(onClick = vm::redownloadBundles) + ) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/ui/screen/settings/DownloadsSettingsScreen.kt b/app/src/main/java/app/revanced/manager/ui/screen/settings/DownloadsSettingsScreen.kt new file mode 100644 index 0000000000..96247a0316 --- /dev/null +++ b/app/src/main/java/app/revanced/manager/ui/screen/settings/DownloadsSettingsScreen.kt @@ -0,0 +1,254 @@ +package app.revanced.manager.ui.screen.settings + +import androidx.annotation.StringRes +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.items +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Delete +import androidx.compose.material3.AlertDialog +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.material3.TopAppBarDefaults +import androidx.compose.material3.pulltorefresh.PullToRefreshDefaults +import androidx.compose.material3.pulltorefresh.pullToRefresh +import androidx.compose.material3.pulltorefresh.rememberPullToRefreshState +import androidx.compose.material3.rememberTopAppBarState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +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.input.nestedscroll.nestedScroll +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import androidx.compose.ui.zIndex +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import app.revanced.manager.R +import app.revanced.manager.network.downloader.DownloaderPluginState +import app.revanced.manager.ui.component.AppLabel +import app.revanced.manager.ui.component.AppTopBar +import app.revanced.manager.ui.component.ExceptionViewerDialog +import app.revanced.manager.ui.component.GroupHeader +import app.revanced.manager.ui.component.LazyColumnWithScrollbar +import app.revanced.manager.ui.component.haptics.HapticCheckbox +import app.revanced.manager.ui.component.settings.SettingsListItem +import app.revanced.manager.ui.viewmodel.DownloadsViewModel +import org.koin.androidx.compose.koinViewModel +import java.security.MessageDigest + +@OptIn(ExperimentalMaterial3Api::class, ExperimentalStdlibApi::class) +@Composable +fun DownloadsSettingsScreen( + onBackClick: () -> Unit, + viewModel: DownloadsViewModel = koinViewModel() +) { + val pullRefreshState = rememberPullToRefreshState() + val downloadedApps by viewModel.downloadedApps.collectAsStateWithLifecycle(emptyList()) + val pluginStates by viewModel.downloaderPluginStates.collectAsStateWithLifecycle() + val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState()) + + Scaffold( + topBar = { + AppTopBar( + title = stringResource(R.string.downloads), + scrollBehavior = scrollBehavior, + onBackClick = onBackClick, + actions = { + if (viewModel.appSelection.isNotEmpty()) { + IconButton(onClick = { viewModel.deleteApps() }) { + Icon(Icons.Default.Delete, stringResource(R.string.delete)) + } + } + } + ) + }, + modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection), + ) { paddingValues -> + Box( + contentAlignment = Alignment.TopCenter, + modifier = Modifier + .padding(paddingValues) + .fillMaxWidth() + .zIndex(1f) + ) { + PullToRefreshDefaults.Indicator( + state = pullRefreshState, + isRefreshing = viewModel.isRefreshingPlugins + ) + } + + LazyColumnWithScrollbar( + modifier = Modifier + .fillMaxSize() + .padding(paddingValues) + .pullToRefresh( + isRefreshing = viewModel.isRefreshingPlugins, + state = pullRefreshState, + onRefresh = viewModel::refreshPlugins + ) + ) { + item { + GroupHeader(stringResource(R.string.downloader_plugins)) + } + pluginStates.forEach { (packageName, state) -> + item(key = packageName) { + var showDialog by rememberSaveable { + mutableStateOf(false) + } + + fun dismiss() { + showDialog = false + } + + val packageInfo = + remember(packageName) { + viewModel.pm.getPackageInfo( + packageName + ) + } ?: return@item + + if (showDialog) { + val signature = + remember(packageName) { + val androidSignature = + viewModel.pm.getSignature(packageName) + val hash = MessageDigest.getInstance("SHA-256") + .digest(androidSignature.toByteArray()) + hash.toHexString(format = HexFormat.UpperCase) + } + + when (state) { + is DownloaderPluginState.Loaded -> TrustDialog( + title = R.string.downloader_plugin_revoke_trust_dialog_title, + body = stringResource( + R.string.downloader_plugin_trust_dialog_body, + packageName, + signature + ), + onDismiss = ::dismiss, + onConfirm = { + viewModel.revokePluginTrust(packageName) + dismiss() + } + ) + + is DownloaderPluginState.Failed -> ExceptionViewerDialog( + text = remember(state.throwable) { + state.throwable.stackTraceToString() + }, + onDismiss = ::dismiss + ) + + is DownloaderPluginState.Untrusted -> TrustDialog( + title = R.string.downloader_plugin_trust_dialog_title, + body = stringResource( + R.string.downloader_plugin_trust_dialog_body, + packageName, + signature + ), + onDismiss = ::dismiss, + onConfirm = { + viewModel.trustPlugin(packageName) + dismiss() + } + ) + } + } + + SettingsListItem( + modifier = Modifier.clickable { showDialog = true }, + headlineContent = { + AppLabel( + packageInfo = packageInfo, + style = MaterialTheme.typography.titleLarge + ) + }, + supportingContent = stringResource( + when (state) { + is DownloaderPluginState.Loaded -> R.string.downloader_plugin_state_trusted + is DownloaderPluginState.Failed -> R.string.downloader_plugin_state_failed + is DownloaderPluginState.Untrusted -> R.string.downloader_plugin_state_untrusted + } + ), + trailingContent = { Text(packageInfo.versionName!!) } + ) + } + } + if (pluginStates.isEmpty()) { + item { + Text( + stringResource(R.string.downloader_no_plugins_installed), + modifier = Modifier.fillMaxWidth(), + textAlign = TextAlign.Center + ) + } + } + + item { + GroupHeader(stringResource(R.string.downloaded_apps)) + } + items(downloadedApps, key = { it.packageName to it.version }) { app -> + val selected = app in viewModel.appSelection + + SettingsListItem( + modifier = Modifier.clickable { viewModel.toggleApp(app) }, + headlineContent = app.packageName, + leadingContent = (@Composable { + HapticCheckbox( + checked = selected, + onCheckedChange = { viewModel.toggleApp(app) } + ) + }).takeIf { viewModel.appSelection.isNotEmpty() }, + supportingContent = app.version, + tonalElevation = if (selected) 8.dp else 0.dp + ) + } + if (downloadedApps.isEmpty()) { + item { + Text( + stringResource(R.string.downloader_settings_no_apps), + modifier = Modifier.fillMaxWidth(), + textAlign = TextAlign.Center + ) + } + } + } + } +} + +@Composable +private fun TrustDialog( + @StringRes title: Int, + body: String, + onDismiss: () -> Unit, + onConfirm: () -> Unit +) { + AlertDialog( + onDismissRequest = onDismiss, + confirmButton = { + TextButton(onClick = onConfirm) { + Text(stringResource(R.string.continue_)) + } + }, + dismissButton = { + TextButton(onClick = onDismiss) { + Text(stringResource(R.string.dismiss)) + } + }, + title = { Text(stringResource(title)) }, + text = { Text(body) } + ) +} \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/ui/screen/settings/GeneralSettingsScreen.kt b/app/src/main/java/app/revanced/manager/ui/screen/settings/GeneralSettingsScreen.kt new file mode 100644 index 0000000000..16dc0a6548 --- /dev/null +++ b/app/src/main/java/app/revanced/manager/ui/screen/settings/GeneralSettingsScreen.kt @@ -0,0 +1,142 @@ +package app.revanced.manager.ui.screen.settings + +import android.os.Build +import androidx.compose.foundation.clickable +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.material3.AlertDialog +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.FilledTonalButton +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.material3.TopAppBarDefaults +import androidx.compose.material3.rememberTopAppBarState +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.input.nestedscroll.nestedScroll +import androidx.compose.ui.res.stringResource +import androidx.lifecycle.viewModelScope +import app.revanced.manager.R +import app.revanced.manager.domain.manager.PreferencesManager +import app.revanced.manager.ui.component.AppTopBar +import app.revanced.manager.ui.component.ColumnWithScrollbar +import app.revanced.manager.ui.component.GroupHeader +import app.revanced.manager.ui.component.haptics.HapticRadioButton +import app.revanced.manager.ui.component.settings.BooleanItem +import app.revanced.manager.ui.component.settings.SettingsListItem +import app.revanced.manager.ui.theme.Theme +import app.revanced.manager.ui.viewmodel.GeneralSettingsViewModel +import org.koin.androidx.compose.koinViewModel +import org.koin.compose.koinInject + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun GeneralSettingsScreen( + onBackClick: () -> Unit, + viewModel: GeneralSettingsViewModel = koinViewModel() +) { + val prefs = viewModel.prefs + val coroutineScope = viewModel.viewModelScope + var showThemePicker by rememberSaveable { mutableStateOf(false) } + + if (showThemePicker) { + ThemePicker( + onDismiss = { showThemePicker = false }, + onConfirm = { viewModel.setTheme(it) } + ) + } + val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState()) + + Scaffold( + topBar = { + AppTopBar( + title = stringResource(R.string.general), + scrollBehavior = scrollBehavior, + onBackClick = onBackClick + ) + }, + modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection), + ) { paddingValues -> + ColumnWithScrollbar( + modifier = Modifier + .fillMaxSize() + .padding(paddingValues) + ) { + GroupHeader(stringResource(R.string.appearance)) + + val theme by prefs.theme.getAsState() + SettingsListItem( + modifier = Modifier.clickable { showThemePicker = true }, + headlineContent = stringResource(R.string.theme), + supportingContent = stringResource(R.string.theme_description), + trailingContent = { + FilledTonalButton( + onClick = { + showThemePicker = true + } + ) { + Text(stringResource(theme.displayName)) + } + } + ) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + BooleanItem( + preference = prefs.dynamicColor, + coroutineScope = coroutineScope, + headline = R.string.dynamic_color, + description = R.string.dynamic_color_description + ) + } + } + } +} + +@Composable +private fun ThemePicker( + onDismiss: () -> Unit, + onConfirm: (Theme) -> Unit, + prefs: PreferencesManager = koinInject() +) { + var selectedTheme by rememberSaveable { mutableStateOf(prefs.theme.getBlocking()) } + + AlertDialog( + onDismissRequest = onDismiss, + title = { Text(stringResource(R.string.theme)) }, + text = { + Column { + Theme.entries.forEach { + Row( + modifier = Modifier + .fillMaxWidth() + .clickable { selectedTheme = it }, + verticalAlignment = Alignment.CenterVertically + ) { + HapticRadioButton( + selected = selectedTheme == it, + onClick = { selectedTheme = it }) + Text(stringResource(it.displayName)) + } + } + } + }, + confirmButton = { + TextButton( + onClick = { + onConfirm(selectedTheme) + onDismiss() + } + ) { + Text(stringResource(R.string.apply)) + } + } + ) +} \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/ui/screen/settings/ImportExportSettingsScreen.kt b/app/src/main/java/app/revanced/manager/ui/screen/settings/ImportExportSettingsScreen.kt new file mode 100644 index 0000000000..7f8083f37c --- /dev/null +++ b/app/src/main/java/app/revanced/manager/ui/screen/settings/ImportExportSettingsScreen.kt @@ -0,0 +1,342 @@ +package app.revanced.manager.ui.screen.settings + +import androidx.activity.compose.rememberLauncherForActivityResult +import androidx.activity.result.contract.ActivityResultContracts +import androidx.annotation.StringRes +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.Key +import androidx.compose.material3.AlertDialog +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.ModalBottomSheet +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.rememberTopAppBarState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +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.stringResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import androidx.lifecycle.viewModelScope +import app.revanced.manager.R +import app.revanced.manager.ui.component.AppTopBar +import app.revanced.manager.ui.component.ColumnWithScrollbar +import app.revanced.manager.ui.component.GroupHeader +import app.revanced.manager.ui.component.PasswordField +import app.revanced.manager.ui.component.bundle.BundleSelector +import app.revanced.manager.ui.component.settings.SettingsListItem +import app.revanced.manager.ui.viewmodel.ImportExportViewModel +import app.revanced.manager.util.toast +import app.revanced.manager.util.uiSafe +import kotlinx.coroutines.launch +import org.koin.androidx.compose.koinViewModel + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun ImportExportSettingsScreen( + onBackClick: () -> Unit, + vm: ImportExportViewModel = koinViewModel() +) { + val context = LocalContext.current + + val importKeystoreLauncher = + rememberLauncherForActivityResult(contract = ActivityResultContracts.GetContent()) { + it?.let { uri -> vm.startKeystoreImport(uri) } + } + val exportKeystoreLauncher = + rememberLauncherForActivityResult(ActivityResultContracts.CreateDocument("*/*")) { + it?.let(vm::exportKeystore) + } + + val patchBundles by vm.patchBundles.collectAsStateWithLifecycle(initialValue = emptyList()) + val packagesWithOptions by vm.packagesWithOptions.collectAsStateWithLifecycle(initialValue = emptySet()) + + vm.selectionAction?.let { action -> + val launcher = rememberLauncherForActivityResult(action.activityContract) { uri -> + if (uri == null) { + vm.clearSelectionAction() + } else { + vm.executeSelectionAction(uri) + } + } + + if (vm.selectedBundle == null) { + BundleSelector(patchBundles) { + if (it == null) { + vm.clearSelectionAction() + } else { + vm.selectBundle(it) + launcher.launch(action.activityArg) + } + } + } + } + + if (vm.showCredentialsDialog) { + KeystoreCredentialsDialog( + onDismissRequest = vm::cancelKeystoreImport, + onSubmit = { cn, pass -> + vm.viewModelScope.launch { + uiSafe(context, R.string.failed_to_import_keystore, "Failed to import keystore") { + val result = vm.tryKeystoreImport(cn, pass) + if (!result) context.toast(context.getString(R.string.import_keystore_wrong_credentials)) + } + } + } + ) + } + + val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState()) + + Scaffold( + topBar = { + AppTopBar( + title = stringResource(R.string.import_export), + scrollBehavior = scrollBehavior, + onBackClick = onBackClick + ) + }, + modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection), + ) { paddingValues -> + ColumnWithScrollbar( + modifier = Modifier + .fillMaxSize() + .padding(paddingValues) + ) { + var showPackageSelector by rememberSaveable { + mutableStateOf(false) + } + var showBundleSelector by rememberSaveable { + mutableStateOf(false) + } + + if (showPackageSelector) { + PackageSelector(packages = packagesWithOptions) { selected -> + selected?.let(vm::resetOptionsForPackage) + + showPackageSelector = false + } + } + + if (showBundleSelector) { + BundleSelector(bundles = patchBundles) { bundle -> + bundle?.let(vm::clearOptionsForBundle) + + showBundleSelector = false + } + } + + GroupHeader(stringResource(R.string.import_)) + GroupItem( + onClick = { + importKeystoreLauncher.launch("*/*") + }, + headline = R.string.import_keystore, + description = R.string.import_keystore_description + ) + GroupItem( + onClick = vm::importSelection, + headline = R.string.import_patch_selection, + description = R.string.import_patch_selection_description + ) + + GroupHeader(stringResource(R.string.export)) + GroupItem( + onClick = { + if (!vm.canExport()) { + context.toast(context.getString(R.string.export_keystore_unavailable)) + return@GroupItem + } + exportKeystoreLauncher.launch("Manager.keystore") + }, + headline = R.string.export_keystore, + description = R.string.export_keystore_description + ) + GroupItem( + onClick = vm::exportSelection, + headline = R.string.export_patch_selection, + description = R.string.export_patch_selection_description + ) + + GroupHeader(stringResource(R.string.reset)) + GroupItem( + onClick = vm::regenerateKeystore, + headline = R.string.regenerate_keystore, + description = R.string.regenerate_keystore_description + ) + GroupItem( + onClick = vm::resetSelection, // TODO: allow resetting selection for specific bundle or package name. + headline = R.string.reset_patch_selection, + description = R.string.reset_patch_selection_description + ) + GroupItem( + onClick = vm::resetOptions, // TODO: patch options import/export. + headline = R.string.patch_options_reset_all, + description = R.string.patch_options_reset_all_description, + ) + GroupItem( + onClick = { showPackageSelector = true }, + headline = R.string.patch_options_reset_package, + description = R.string.patch_options_reset_package_description + ) + if (patchBundles.size > 1) { + GroupItem( + onClick = { showBundleSelector = true }, + headline = R.string.patch_options_reset_bundle, + description = R.string.patch_options_reset_bundle_description, + ) + } + } + } +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +private fun PackageSelector(packages: Set<String>, onFinish: (String?) -> Unit) { + val context = LocalContext.current + + val noPackages = packages.isEmpty() + + LaunchedEffect(noPackages) { + if (noPackages) { + context.toast("No packages available.") + onFinish(null) + } + } + + if (noPackages) return + + ModalBottomSheet( + onDismissRequest = { onFinish(null) } + ) { + Column( + modifier = Modifier.fillMaxWidth(), + verticalArrangement = Arrangement.spacedBy(12.dp) + ) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.Center, + modifier = Modifier + .height(48.dp) + .fillMaxWidth() + ) { + Text( + text = "Select package", + style = MaterialTheme.typography.titleLarge, + color = MaterialTheme.colorScheme.onSurface + ) + } + packages.forEach { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.Center, + modifier = Modifier + .height(48.dp) + .fillMaxWidth() + .clickable { + onFinish(it) + } + ) { + Text( + text = it, + style = MaterialTheme.typography.titleMedium, + color = MaterialTheme.colorScheme.onSurface + ) + } + } + } + } +} + +@Composable +private fun GroupItem( + onClick: () -> Unit, + @StringRes headline: Int, + @StringRes description: Int? = null +) { + SettingsListItem( + modifier = Modifier.clickable { onClick() }, + headlineContent = stringResource(headline), + supportingContent = description?.let { stringResource(it) } + ) +} + +@Composable +fun KeystoreCredentialsDialog( + onDismissRequest: () -> Unit, + onSubmit: (String, String) -> Unit +) { + var cn by rememberSaveable { mutableStateOf("") } + var pass by rememberSaveable { mutableStateOf("") } + + AlertDialog( + onDismissRequest = onDismissRequest, + confirmButton = { + TextButton( + onClick = { + onSubmit(cn, pass) + } + ) { + Text(stringResource(R.string.import_keystore_dialog_button)) + } + }, + dismissButton = { + TextButton(onClick = onDismissRequest) { + Text(stringResource(R.string.cancel)) + } + }, + icon = { + Icon(Icons.Outlined.Key, null) + }, + title = { + Text( + text = stringResource(R.string.import_keystore_dialog_title), + style = MaterialTheme.typography.headlineSmall.copy(textAlign = TextAlign.Center), + color = MaterialTheme.colorScheme.onSurface, + ) + }, + text = { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(16.dp) + ) { + Text( + text = stringResource(R.string.import_keystore_dialog_description), + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + OutlinedTextField( + value = cn, + onValueChange = { cn = it }, + label = { Text(stringResource(R.string.import_keystore_dialog_alias_field)) } + ) + PasswordField( + value = pass, + onValueChange = { pass = it }, + label = { Text(stringResource(R.string.import_keystore_dialog_password_field)) } + ) + } + } + ) +} \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/ui/screen/settings/LicensesScreen.kt b/app/src/main/java/app/revanced/manager/ui/screen/settings/LicensesScreen.kt new file mode 100644 index 0000000000..76e2e964a5 --- /dev/null +++ b/app/src/main/java/app/revanced/manager/ui/screen/settings/LicensesScreen.kt @@ -0,0 +1,50 @@ +package app.revanced.manager.ui.screen.settings + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import app.revanced.manager.R +import app.revanced.manager.ui.component.AppScaffold +import app.revanced.manager.ui.component.AppTopBar +import app.revanced.manager.ui.component.Scrollbar +import com.mikepenz.aboutlibraries.ui.compose.LibrariesContainer +import com.mikepenz.aboutlibraries.ui.compose.LibraryDefaults + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun LicensesScreen( + onBackClick: () -> Unit, +) { + AppScaffold( + topBar = { scrollBehavior -> + AppTopBar( + title = stringResource(R.string.opensource_licenses), + scrollBehavior = scrollBehavior, + onBackClick = onBackClick + ) + } + ) { paddingValues -> + Column(modifier = Modifier.padding(paddingValues)) { + val lazyListState = rememberLazyListState() + + LibrariesContainer( + modifier = Modifier + .fillMaxSize(), + lazyListState = lazyListState, + colors = LibraryDefaults.libraryColors( + backgroundColor = MaterialTheme.colorScheme.background, + contentColor = MaterialTheme.colorScheme.onBackground, + badgeBackgroundColor = MaterialTheme.colorScheme.primary, + badgeContentColor = MaterialTheme.colorScheme.onPrimary, + ) + ) + Scrollbar(lazyListState = lazyListState, modifier = Modifier.padding(paddingValues)) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/ui/screen/settings/update/ChangelogsScreen.kt b/app/src/main/java/app/revanced/manager/ui/screen/settings/update/ChangelogsScreen.kt new file mode 100644 index 0000000000..6808d2310d --- /dev/null +++ b/app/src/main/java/app/revanced/manager/ui/screen/settings/update/ChangelogsScreen.kt @@ -0,0 +1,64 @@ +package app.revanced.manager.ui.screen.settings.update + + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Scaffold +import androidx.compose.material3.TopAppBarDefaults +import androidx.compose.material3.rememberTopAppBarState +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.stringResource +import androidx.compose.ui.unit.dp +import app.revanced.manager.R +import app.revanced.manager.ui.component.AppTopBar +import app.revanced.manager.ui.component.ColumnWithScrollbar +import app.revanced.manager.ui.component.LoadingIndicator +import app.revanced.manager.ui.component.settings.Changelog +import app.revanced.manager.ui.viewmodel.ChangelogsViewModel +import app.revanced.manager.util.relativeTime +import org.koin.androidx.compose.koinViewModel + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun ChangelogsScreen( + onBackClick: () -> Unit, + vm: ChangelogsViewModel = koinViewModel() +) { + val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState()) + + Scaffold( + topBar = { + AppTopBar( + title = stringResource(R.string.changelog), + scrollBehavior = scrollBehavior, + onBackClick = onBackClick + ) + }, + modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection), + ) { paddingValues -> + ColumnWithScrollbar( + modifier = Modifier + .padding(paddingValues) + .fillMaxSize(), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = if (vm.releaseInfo == null) Arrangement.Center else Arrangement.Top + ) { + vm.releaseInfo?.let { info -> + Column(modifier = Modifier.padding(16.dp)) { + Changelog( + markdown = info.description.replace("`", ""), + version = info.version, + publishDate = info.createdAt.relativeTime(LocalContext.current) + ) + } + } ?: LoadingIndicator() + } + } +} \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/ui/screen/settings/update/UpdatesSettingsScreen.kt b/app/src/main/java/app/revanced/manager/ui/screen/settings/update/UpdatesSettingsScreen.kt new file mode 100644 index 0000000000..af43ff93fa --- /dev/null +++ b/app/src/main/java/app/revanced/manager/ui/screen/settings/update/UpdatesSettingsScreen.kt @@ -0,0 +1,81 @@ +package app.revanced.manager.ui.screen.settings.update + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Scaffold +import androidx.compose.material3.TopAppBarDefaults +import androidx.compose.material3.rememberTopAppBarState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.Modifier +import androidx.compose.ui.input.nestedscroll.nestedScroll +import androidx.compose.ui.res.stringResource +import app.revanced.manager.R +import app.revanced.manager.ui.component.AppTopBar +import app.revanced.manager.ui.component.ColumnWithScrollbar +import app.revanced.manager.ui.component.settings.BooleanItem +import app.revanced.manager.ui.component.settings.SettingsListItem +import app.revanced.manager.ui.viewmodel.UpdatesSettingsViewModel +import kotlinx.coroutines.launch +import org.koin.androidx.compose.koinViewModel + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun UpdatesSettingsScreen( + onBackClick: () -> Unit, + onChangelogClick: () -> Unit, + onUpdateClick: () -> Unit, + vm: UpdatesSettingsViewModel = koinViewModel(), +) { + val coroutineScope = rememberCoroutineScope() + val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState()) + + Scaffold( + topBar = { + AppTopBar( + title = stringResource(R.string.updates), + scrollBehavior = scrollBehavior, + onBackClick = onBackClick + ) + }, + modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection), + ) { paddingValues -> + ColumnWithScrollbar( + modifier = Modifier + .fillMaxSize() + .padding(paddingValues) + ) { + SettingsListItem( + modifier = Modifier.clickable { + coroutineScope.launch { + if (vm.checkForUpdates()) onUpdateClick() + } + }, + headlineContent = stringResource(R.string.manual_update_check), + supportingContent = stringResource(R.string.manual_update_check_description) + ) + + SettingsListItem( + modifier = Modifier.clickable(onClick = onChangelogClick), + headlineContent = stringResource(R.string.changelog), + supportingContent = stringResource( + R.string.changelog_description + ) + ) + + BooleanItem( + preference = vm.managerAutoUpdates, + headline = R.string.update_checking_manager, + description = R.string.update_checking_manager_description + ) + + BooleanItem( + preference = vm.showManagerUpdateDialogOnLaunch, + headline = R.string.show_manager_update_dialog_on_launch, + description = R.string.update_checking_manager_description + ) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/ui/theme/Color.kt b/app/src/main/java/app/revanced/manager/ui/theme/Color.kt new file mode 100644 index 0000000000..9626a96207 --- /dev/null +++ b/app/src/main/java/app/revanced/manager/ui/theme/Color.kt @@ -0,0 +1,65 @@ +package app.revanced.manager.ui.theme + +import androidx.compose.ui.graphics.Color + +val rv_theme_light_primary = Color(0xFF005FAC) +val rv_theme_light_onPrimary = Color(0xFFFFFFFF) +val rv_theme_light_primaryContainer = Color(0xFFD4E3FF) +val rv_theme_light_onPrimaryContainer = Color(0xFF001C39) +val rv_theme_light_secondary = Color(0xFF545F71) +val rv_theme_light_onSecondary = Color(0xFFFFFFFF) +val rv_theme_light_secondaryContainer = Color(0xFFD8E3F8) +val rv_theme_light_onSecondaryContainer = Color(0xFF111C2B) +val rv_theme_light_tertiary = Color(0xFF6D5677) +val rv_theme_light_onTertiary = Color(0xFFFFFFFF) +val rv_theme_light_tertiaryContainer = Color(0xFFF6D9FF) +val rv_theme_light_onTertiaryContainer = Color(0xFF271430) +val rv_theme_light_error = Color(0xFFBA1A1A) +val rv_theme_light_errorContainer = Color(0xFFFFDAD6) +val rv_theme_light_onError = Color(0xFFFFFFFF) +val rv_theme_light_onErrorContainer = Color(0xFF410002) +val rv_theme_light_background = Color(0xFFFDFCFF) +val rv_theme_light_onBackground = Color(0xFF1A1C1E) +val rv_theme_light_surface = Color(0xFFFDFCFF) +val rv_theme_light_onSurface = Color(0xFF1A1C1E) +val rv_theme_light_surfaceVariant = Color(0xFFDFE2EB) +val rv_theme_light_onSurfaceVariant = Color(0xFF43474E) +val rv_theme_light_outline = Color(0xFF73777F) +val rv_theme_light_inverseOnSurface = Color(0xFFF1F0F4) +val rv_theme_light_inverseSurface = Color(0xFF2F3033) +val rv_theme_light_inversePrimary = Color(0xFFA4C9FF) +//val rv_theme_light_shadow = Color(0xFF000000) +val rv_theme_light_surfaceTint = Color(0xFF005FAC) +val rv_theme_light_outlineVariant = Color(0xFFC3C6CF) +val rv_theme_light_scrim = Color(0xFF000000) + +val rv_theme_dark_primary = Color(0xFFA4C9FF) +val rv_theme_dark_onPrimary = Color(0xFF00315D) +val rv_theme_dark_primaryContainer = Color(0xFF004884) +val rv_theme_dark_onPrimaryContainer = Color(0xFFD4E3FF) +val rv_theme_dark_secondary = Color(0xFFBCC7DB) +val rv_theme_dark_onSecondary = Color(0xFF263141) +val rv_theme_dark_secondaryContainer = Color(0xFF3D4758) +val rv_theme_dark_onSecondaryContainer = Color(0xFFD8E3F8) +val rv_theme_dark_tertiary = Color(0xFFD9BDE3) +val rv_theme_dark_onTertiary = Color(0xFF3D2946) +val rv_theme_dark_tertiaryContainer = Color(0xFF543F5E) +val rv_theme_dark_onTertiaryContainer = Color(0xFFF6D9FF) +val rv_theme_dark_error = Color(0xFFFFB4AB) +val rv_theme_dark_errorContainer = Color(0xFF93000A) +val rv_theme_dark_onError = Color(0xFF690005) +val rv_theme_dark_onErrorContainer = Color(0xFFFFDAD6) +val rv_theme_dark_background = Color(0xFF1A1C1E) +val rv_theme_dark_onBackground = Color(0xFFE3E2E6) +val rv_theme_dark_surface = Color(0xFF1A1C1E) +val rv_theme_dark_onSurface = Color(0xFFE3E2E6) +val rv_theme_dark_surfaceVariant = Color(0xFF43474E) +val rv_theme_dark_onSurfaceVariant = Color(0xFFC3C6CF) +val rv_theme_dark_outline = Color(0xFF8D9199) +val rv_theme_dark_inverseOnSurface = Color(0xFF1A1C1E) +val rv_theme_dark_inverseSurface = Color(0xFFE3E2E6) +val rv_theme_dark_inversePrimary = Color(0xFF005FAC) +//val rv_theme_dark_shadow = Color(0xFF000000) +val rv_theme_dark_surfaceTint = Color(0xFFA4C9FF) +val rv_theme_dark_outlineVariant = Color(0xFF43474E) +val rv_theme_dark_scrim = Color(0xFF000000) \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/ui/theme/Theme.kt b/app/src/main/java/app/revanced/manager/ui/theme/Theme.kt new file mode 100644 index 0000000000..17c89a5ca7 --- /dev/null +++ b/app/src/main/java/app/revanced/manager/ui/theme/Theme.kt @@ -0,0 +1,124 @@ +package app.revanced.manager.ui.theme + +import android.app.Activity +import android.os.Build +import androidx.compose.material3.* +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 +import androidx.compose.ui.platform.LocalView +import androidx.core.view.WindowCompat +import app.revanced.manager.R + +private val DarkColorScheme = darkColorScheme( + primary = rv_theme_dark_primary, + onPrimary = rv_theme_dark_onPrimary, + primaryContainer = rv_theme_dark_primaryContainer, + onPrimaryContainer = rv_theme_dark_onPrimaryContainer, + secondary = rv_theme_dark_secondary, + onSecondary = rv_theme_dark_onSecondary, + secondaryContainer = rv_theme_dark_secondaryContainer, + onSecondaryContainer = rv_theme_dark_onSecondaryContainer, + tertiary = rv_theme_dark_tertiary, + onTertiary = rv_theme_dark_onTertiary, + tertiaryContainer = rv_theme_dark_tertiaryContainer, + onTertiaryContainer = rv_theme_dark_onTertiaryContainer, + error = rv_theme_dark_error, + errorContainer = rv_theme_dark_errorContainer, + onError = rv_theme_dark_onError, + onErrorContainer = rv_theme_dark_onErrorContainer, + background = rv_theme_dark_background, + onBackground = rv_theme_dark_onBackground, + surface = rv_theme_dark_surface, + onSurface = rv_theme_dark_onSurface, + surfaceVariant = rv_theme_dark_surfaceVariant, + onSurfaceVariant = rv_theme_dark_onSurfaceVariant, + outline = rv_theme_dark_outline, + inverseOnSurface = rv_theme_dark_inverseOnSurface, + inverseSurface = rv_theme_dark_inverseSurface, + inversePrimary = rv_theme_dark_inversePrimary, + surfaceTint = rv_theme_dark_surfaceTint, + outlineVariant = rv_theme_dark_outlineVariant, + scrim = rv_theme_dark_scrim, +) + +private val LightColorScheme = lightColorScheme( + primary = rv_theme_light_primary, + onPrimary = rv_theme_light_onPrimary, + primaryContainer = rv_theme_light_primaryContainer, + onPrimaryContainer = rv_theme_light_onPrimaryContainer, + secondary = rv_theme_light_secondary, + onSecondary = rv_theme_light_onSecondary, + secondaryContainer = rv_theme_light_secondaryContainer, + onSecondaryContainer = rv_theme_light_onSecondaryContainer, + tertiary = rv_theme_light_tertiary, + onTertiary = rv_theme_light_onTertiary, + tertiaryContainer = rv_theme_light_tertiaryContainer, + onTertiaryContainer = rv_theme_light_onTertiaryContainer, + error = rv_theme_light_error, + errorContainer = rv_theme_light_errorContainer, + onError = rv_theme_light_onError, + onErrorContainer = rv_theme_light_onErrorContainer, + background = rv_theme_light_background, + onBackground = rv_theme_light_onBackground, + surface = rv_theme_light_surface, + onSurface = rv_theme_light_onSurface, + surfaceVariant = rv_theme_light_surfaceVariant, + onSurfaceVariant = rv_theme_light_onSurfaceVariant, + outline = rv_theme_light_outline, + inverseOnSurface = rv_theme_light_inverseOnSurface, + inverseSurface = rv_theme_light_inverseSurface, + inversePrimary = rv_theme_light_inversePrimary, + surfaceTint = rv_theme_light_surfaceTint, + outlineVariant = rv_theme_light_outlineVariant, + scrim = rv_theme_light_scrim, +) + +@Composable +fun ReVancedManagerTheme( + darkTheme: Boolean, + dynamicColor: Boolean, + content: @Composable () -> Unit +) { + val colorScheme = when { + dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> { + val context = LocalContext.current + if (darkTheme) + dynamicDarkColorScheme(context) + else + dynamicLightColorScheme(context) + } + + darkTheme -> DarkColorScheme + else -> LightColorScheme + } + + val view = LocalView.current + if (!view.isInEditMode) { + SideEffect { + val activity = view.context as Activity + + WindowCompat.setDecorFitsSystemWindows(activity.window, false) + + activity.window.statusBarColor = Color.Transparent.toArgb() + activity.window.navigationBarColor = Color.Transparent.toArgb() + + WindowCompat.getInsetsController(activity.window, view).isAppearanceLightStatusBars = !darkTheme + WindowCompat.getInsetsController(activity.window, view).isAppearanceLightNavigationBars = !darkTheme + } + } + + MaterialTheme( + colorScheme = colorScheme, + typography = Typography, + content = content + ) +} + +enum class Theme(val displayName: Int) { + SYSTEM(R.string.system), + LIGHT(R.string.light), + DARK(R.string.dark); +} \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/ui/theme/Type.kt b/app/src/main/java/app/revanced/manager/ui/theme/Type.kt new file mode 100644 index 0000000000..d999aae2c0 --- /dev/null +++ b/app/src/main/java/app/revanced/manager/ui/theme/Type.kt @@ -0,0 +1,17 @@ +package app.revanced.manager.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( + bodyLarge = TextStyle( + fontFamily = FontFamily.Default, + fontWeight = FontWeight.Normal, + fontSize = 16.sp, + lineHeight = 24.sp, + letterSpacing = 0.5.sp + ) +) \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/ui/viewmodel/AboutViewModel.kt b/app/src/main/java/app/revanced/manager/ui/viewmodel/AboutViewModel.kt new file mode 100644 index 0000000000..77b2b6b6b7 --- /dev/null +++ b/app/src/main/java/app/revanced/manager/ui/viewmodel/AboutViewModel.kt @@ -0,0 +1,59 @@ +package app.revanced.manager.ui.viewmodel + +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.Language +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import app.revanced.manager.network.api.ReVancedAPI +import app.revanced.manager.network.dto.ReVancedDonationLink +import app.revanced.manager.network.dto.ReVancedSocial +import app.revanced.manager.network.utils.getOrNull +import compose.icons.FontAwesomeIcons +import compose.icons.fontawesomeicons.Brands +import compose.icons.fontawesomeicons.brands.Discord +import compose.icons.fontawesomeicons.brands.Github +import compose.icons.fontawesomeicons.brands.Reddit +import compose.icons.fontawesomeicons.brands.Telegram +import compose.icons.fontawesomeicons.brands.XTwitter +import compose.icons.fontawesomeicons.brands.Youtube +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext + +class AboutViewModel(private val reVancedAPI: ReVancedAPI) : ViewModel() { + var socials by mutableStateOf(emptyList<ReVancedSocial>()) + private set + var contact by mutableStateOf<String?>(null) + private set + var donate by mutableStateOf<String?>(null) + private set + + init { + viewModelScope.launch { + withContext(Dispatchers.IO) { + reVancedAPI.getInfo("https://api.revanced.app").getOrNull() + }?.let { + socials = it.socials + contact = it.contact.email + donate = it.donations.links.find(ReVancedDonationLink::preferred)?.url + } + } + } + + companion object { + private val socialIcons = mapOf( + "Discord" to FontAwesomeIcons.Brands.Discord, + "GitHub" to FontAwesomeIcons.Brands.Github, + "Reddit" to FontAwesomeIcons.Brands.Reddit, + "Telegram" to FontAwesomeIcons.Brands.Telegram, + "Twitter" to FontAwesomeIcons.Brands.XTwitter, + "X" to FontAwesomeIcons.Brands.XTwitter, + "YouTube" to FontAwesomeIcons.Brands.Youtube, + ) + + fun getSocialIcon(name: String) = socialIcons[name] ?: Icons.Outlined.Language + } +} \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/ui/viewmodel/AdvancedSettingsViewModel.kt b/app/src/main/java/app/revanced/manager/ui/viewmodel/AdvancedSettingsViewModel.kt new file mode 100644 index 0000000000..cbad0045ff --- /dev/null +++ b/app/src/main/java/app/revanced/manager/ui/viewmodel/AdvancedSettingsViewModel.kt @@ -0,0 +1,70 @@ +package app.revanced.manager.ui.viewmodel + +import android.app.Application +import android.net.Uri +import android.util.Log +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import app.revanced.manager.R +import app.revanced.manager.domain.manager.PreferencesManager +import app.revanced.manager.domain.repository.PatchBundleRepository +import app.revanced.manager.util.tag +import app.revanced.manager.util.toast +import com.github.pgreze.process.Redirect +import com.github.pgreze.process.process +import kotlinx.coroutines.CancellationException +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import java.time.LocalDateTime +import java.time.format.DateTimeFormatter + +class AdvancedSettingsViewModel( + val prefs: PreferencesManager, + private val app: Application, + private val patchBundleRepository: PatchBundleRepository +) : ViewModel() { + val debugLogFileName: String + get() { + val time = DateTimeFormatter.ISO_LOCAL_DATE_TIME.format(LocalDateTime.now()) + + return "revanced-manager_logcat_$time" + } + + fun setApiUrl(value: String) = viewModelScope.launch(Dispatchers.Default) { + if (value == prefs.api.get()) return@launch + + prefs.api.update(value) + patchBundleRepository.reloadApiBundles() + } + + fun exportDebugLogs(target: Uri) = viewModelScope.launch { + val exitCode = try { + withContext(Dispatchers.IO) { + app.contentResolver.openOutputStream(target)!!.bufferedWriter().use { writer -> + val consumer = Redirect.Consume { flow -> + flow.onEach { + writer.write(it) + }.flowOn(Dispatchers.IO).collect() + } + + process("logcat", "-d", stdout = consumer).resultCode + } + } + } catch (e: CancellationException) { + throw e + } catch (e: Exception) { + Log.e(tag, "Got exception while exporting logs", e) + app.toast(app.getString(R.string.debug_logs_export_failed)) + return@launch + } + + if (exitCode == 0) + app.toast(app.getString(R.string.debug_logs_export_success)) + else + app.toast(app.getString(R.string.debug_logs_export_read_failed, exitCode)) + } +} \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/ui/viewmodel/AppSelectorViewModel.kt b/app/src/main/java/app/revanced/manager/ui/viewmodel/AppSelectorViewModel.kt new file mode 100644 index 0000000000..538b85e33a --- /dev/null +++ b/app/src/main/java/app/revanced/manager/ui/viewmodel/AppSelectorViewModel.kt @@ -0,0 +1,92 @@ +package app.revanced.manager.ui.viewmodel + +import android.app.Application +import android.content.pm.PackageInfo +import android.net.Uri +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue +import androidx.lifecycle.SavedStateHandle +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import androidx.lifecycle.viewmodel.compose.SavedStateHandleSaveableApi +import androidx.lifecycle.viewmodel.compose.saveable +import app.revanced.manager.R +import app.revanced.manager.data.platform.Filesystem +import app.revanced.manager.domain.repository.PatchBundleRepository +import app.revanced.manager.ui.model.SelectedApp +import app.revanced.manager.util.PM +import app.revanced.manager.util.toast +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.receiveAsFlow +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import java.io.File +import java.nio.file.Files + +@OptIn(SavedStateHandleSaveableApi::class) +class AppSelectorViewModel( + private val app: Application, + private val pm: PM, + fs: Filesystem, + private val patchBundleRepository: PatchBundleRepository, + savedStateHandle: SavedStateHandle, +) : ViewModel() { + private val inputFile = savedStateHandle.saveable(key = "inputFile") { + File( + fs.uiTempDir, + "input.apk" + ).also(File::delete) + } + val appList = pm.appList + + private val storageSelectionChannel = Channel<SelectedApp.Local>() + val storageSelectionFlow = storageSelectionChannel.receiveAsFlow() + + val suggestedAppVersions = patchBundleRepository.suggestedVersions.flowOn(Dispatchers.Default) + + var nonSuggestedVersionDialogSubject by mutableStateOf<SelectedApp.Local?>(null) + private set + + fun loadLabel(app: PackageInfo?) = with(pm) { app?.label() ?: "Not installed" } + + fun dismissNonSuggestedVersionDialog() { + nonSuggestedVersionDialogSubject = null + } + + fun handleStorageResult(uri: Uri) = viewModelScope.launch { + val selectedApp = withContext(Dispatchers.IO) { + loadSelectedFile(uri) + } + + if (selectedApp == null) { + app.toast(app.getString(R.string.failed_to_load_apk)) + return@launch + } + + if (patchBundleRepository.isVersionAllowed(selectedApp.packageName, selectedApp.version)) { + storageSelectionChannel.send(selectedApp) + } else { + nonSuggestedVersionDialogSubject = selectedApp + } + } + + private fun loadSelectedFile(uri: Uri) = + app.contentResolver.openInputStream(uri)?.use { stream -> + with(inputFile) { + delete() + Files.copy(stream, toPath()) + + pm.getPackageInfo(this)?.let { packageInfo -> + SelectedApp.Local( + packageName = packageInfo.packageName, + version = packageInfo.versionName!!, + file = this, + temporary = true + ) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/ui/viewmodel/ChangelogsViewModel.kt b/app/src/main/java/app/revanced/manager/ui/viewmodel/ChangelogsViewModel.kt new file mode 100644 index 0000000000..bfe1bbfa97 --- /dev/null +++ b/app/src/main/java/app/revanced/manager/ui/viewmodel/ChangelogsViewModel.kt @@ -0,0 +1,30 @@ +package app.revanced.manager.ui.viewmodel + +import android.app.Application +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import app.revanced.manager.R +import app.revanced.manager.network.api.ReVancedAPI +import app.revanced.manager.network.dto.ReVancedAsset +import app.revanced.manager.network.utils.getOrThrow +import app.revanced.manager.util.uiSafe +import kotlinx.coroutines.launch + +class ChangelogsViewModel( + private val api: ReVancedAPI, + private val app: Application, +) : ViewModel() { + var releaseInfo: ReVancedAsset? by mutableStateOf(null) + private set + + init { + viewModelScope.launch { + uiSafe(app, R.string.changelog_download_fail, "Failed to download changelog") { + releaseInfo = api.getLatestAppInfo().getOrThrow() + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/ui/viewmodel/ContributorViewModel.kt b/app/src/main/java/app/revanced/manager/ui/viewmodel/ContributorViewModel.kt new file mode 100644 index 0000000000..72fbfd7dd1 --- /dev/null +++ b/app/src/main/java/app/revanced/manager/ui/viewmodel/ContributorViewModel.kt @@ -0,0 +1,27 @@ +package app.revanced.manager.ui.viewmodel + +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateListOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import app.revanced.manager.network.api.ReVancedAPI +import app.revanced.manager.network.dto.ReVancedGitRepository +import app.revanced.manager.network.utils.getOrNull +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext + +class ContributorViewModel(private val reVancedAPI: ReVancedAPI) : ViewModel() { + var repositories: List<ReVancedGitRepository>? by mutableStateOf(null) + private set + + init { + viewModelScope.launch { + repositories = withContext(Dispatchers.IO) { + reVancedAPI.getContributors().getOrNull() + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/ui/viewmodel/DashboardViewModel.kt b/app/src/main/java/app/revanced/manager/ui/viewmodel/DashboardViewModel.kt new file mode 100644 index 0000000000..e524c83e2e --- /dev/null +++ b/app/src/main/java/app/revanced/manager/ui/viewmodel/DashboardViewModel.kt @@ -0,0 +1,151 @@ +package app.revanced.manager.ui.viewmodel + +import android.app.Application +import android.content.ContentResolver +import android.net.Uri +import android.os.Build +import android.os.PowerManager +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateListOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue +import androidx.core.content.getSystemService +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import app.revanced.manager.R +import app.revanced.manager.data.platform.NetworkInfo +import app.revanced.manager.domain.bundles.PatchBundleSource +import app.revanced.manager.domain.bundles.PatchBundleSource.Extensions.asRemoteOrNull +import app.revanced.manager.domain.bundles.RemotePatchBundle +import app.revanced.manager.domain.manager.PreferencesManager +import app.revanced.manager.domain.repository.DownloaderPluginRepository +import app.revanced.manager.domain.repository.PatchBundleRepository +import app.revanced.manager.network.api.ReVancedAPI +import app.revanced.manager.util.PM +import app.revanced.manager.util.toast +import app.revanced.manager.util.uiSafe +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.launch + +class DashboardViewModel( + private val app: Application, + private val patchBundleRepository: PatchBundleRepository, + private val downloaderPluginRepository: DownloaderPluginRepository, + private val reVancedAPI: ReVancedAPI, + private val networkInfo: NetworkInfo, + val prefs: PreferencesManager, + private val pm: PM, +) : ViewModel() { + val availablePatches = + patchBundleRepository.bundles.map { it.values.sumOf { bundle -> bundle.patches.size } } + private val contentResolver: ContentResolver = app.contentResolver + private val powerManager = app.getSystemService<PowerManager>()!! + val sources = patchBundleRepository.sources + val selectedSources = mutableStateListOf<PatchBundleSource>() + + val newDownloaderPluginsAvailable = downloaderPluginRepository.newPluginPackageNames.map { it.isNotEmpty() } + + /** + * Android 11 kills the app process after granting the "install apps" permission, which is a problem for the patcher screen. + * This value is true when the conditions that trigger the bug are met. + * + * See: https://github.com/ReVanced/revanced-manager/issues/2138 + */ + val android11BugActive get() = Build.VERSION.SDK_INT == Build.VERSION_CODES.R && !pm.canInstallPackages() + + var updatedManagerVersion: String? by mutableStateOf(null) + private set + val showBatteryOptimizationsWarningFlow = flow { + while (true) { + // There is no callback for this, so we have to poll it. + val result = !powerManager.isIgnoringBatteryOptimizations(app.packageName) + emit(result) + if (!result) return@flow + delay(500L) + } + } + + init { + viewModelScope.launch { + checkForManagerUpdates() + } + } + + fun ignoreNewDownloaderPlugins() = viewModelScope.launch { + downloaderPluginRepository.acknowledgeAllNewPlugins() + } + + fun dismissUpdateDialog() { + updatedManagerVersion = null + } + + private suspend fun checkForManagerUpdates() { + if (!prefs.managerAutoUpdates.get() || !networkInfo.isConnected()) return + + uiSafe(app, R.string.failed_to_check_updates, "Failed to check for updates") { + updatedManagerVersion = reVancedAPI.getAppUpdate()?.version + } + } + + fun setShowManagerUpdateDialogOnLaunch(value: Boolean) { + viewModelScope.launch { + prefs.showManagerUpdateDialogOnLaunch.update(value) + } + } + + fun applyAutoUpdatePrefs(manager: Boolean, patches: Boolean) = viewModelScope.launch { + prefs.firstLaunch.update(false) + + prefs.managerAutoUpdates.update(manager) + + if (manager) checkForManagerUpdates() + + if (patches) { + with(patchBundleRepository) { + sources + .first() + .find { it.uid == 0 } + ?.asRemoteOrNull + ?.setAutoUpdate(true) + + updateCheck() + } + } + } + + + fun cancelSourceSelection() { + selectedSources.clear() + } + + fun createLocalSource(patchBundle: Uri) = + viewModelScope.launch { + contentResolver.openInputStream(patchBundle)!!.use { patchesStream -> + patchBundleRepository.createLocal(patchesStream) + } + } + + fun createRemoteSource(apiUrl: String, autoUpdate: Boolean) = + viewModelScope.launch { patchBundleRepository.createRemote(apiUrl, autoUpdate) } + + fun delete(bundle: PatchBundleSource) = + viewModelScope.launch { patchBundleRepository.remove(bundle) } + + fun update(bundle: PatchBundleSource) = viewModelScope.launch { + if (bundle !is RemotePatchBundle) return@launch + + uiSafe( + app, + R.string.source_download_fail, + RemotePatchBundle.updateFailMsg + ) { + if (bundle.update()) + app.toast(app.getString(R.string.bundle_update_success, bundle.getName())) + else + app.toast(app.getString(R.string.bundle_update_unavailable, bundle.getName())) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/ui/viewmodel/DeveloperOptionsViewModel.kt b/app/src/main/java/app/revanced/manager/ui/viewmodel/DeveloperOptionsViewModel.kt new file mode 100644 index 0000000000..bc8d052761 --- /dev/null +++ b/app/src/main/java/app/revanced/manager/ui/viewmodel/DeveloperOptionsViewModel.kt @@ -0,0 +1,27 @@ +package app.revanced.manager.ui.viewmodel + +import android.app.Application +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import app.revanced.manager.R +import app.revanced.manager.domain.bundles.RemotePatchBundle +import app.revanced.manager.domain.manager.PreferencesManager +import app.revanced.manager.domain.repository.PatchBundleRepository +import app.revanced.manager.util.uiSafe +import kotlinx.coroutines.launch + +class DeveloperOptionsViewModel( + val prefs: PreferencesManager, + private val app: Application, + private val patchBundleRepository: PatchBundleRepository +) : ViewModel() { + fun redownloadBundles() = viewModelScope.launch { + uiSafe(app, R.string.source_download_fail, RemotePatchBundle.updateFailMsg) { + patchBundleRepository.redownloadRemoteBundles() + } + } + + fun resetBundles() = viewModelScope.launch { + patchBundleRepository.reset() + } +} \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/ui/viewmodel/DownloadsViewModel.kt b/app/src/main/java/app/revanced/manager/ui/viewmodel/DownloadsViewModel.kt new file mode 100644 index 0000000000..e2c750df8d --- /dev/null +++ b/app/src/main/java/app/revanced/manager/ui/viewmodel/DownloadsViewModel.kt @@ -0,0 +1,68 @@ +package app.revanced.manager.ui.viewmodel + +import android.content.pm.PackageInfo +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import app.revanced.manager.data.room.apps.downloaded.DownloadedApp +import app.revanced.manager.domain.repository.DownloadedAppRepository +import app.revanced.manager.domain.repository.DownloaderPluginRepository +import app.revanced.manager.util.PM +import app.revanced.manager.util.mutableStateSetOf +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.NonCancellable +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext + +class DownloadsViewModel( + private val downloadedAppRepository: DownloadedAppRepository, + private val downloaderPluginRepository: DownloaderPluginRepository, + val pm: PM +) : ViewModel() { + val downloaderPluginStates = downloaderPluginRepository.pluginStates + val downloadedApps = downloadedAppRepository.getAll().map { downloadedApps -> + downloadedApps.sortedWith( + compareBy<DownloadedApp> { + it.packageName + }.thenBy { it.version } + ) + } + val appSelection = mutableStateSetOf<DownloadedApp>() + + var isRefreshingPlugins by mutableStateOf(false) + private set + + fun toggleApp(downloadedApp: DownloadedApp) { + if (appSelection.contains(downloadedApp)) + appSelection.remove(downloadedApp) + else + appSelection.add(downloadedApp) + } + + fun deleteApps() { + viewModelScope.launch(NonCancellable) { + downloadedAppRepository.delete(appSelection) + + withContext(Dispatchers.Main) { + appSelection.clear() + } + } + } + + fun refreshPlugins() = viewModelScope.launch { + isRefreshingPlugins = true + downloaderPluginRepository.reload() + isRefreshingPlugins = false + } + + fun trustPlugin(packageName: String) = viewModelScope.launch { + downloaderPluginRepository.trustPackage(packageName) + } + + fun revokePluginTrust(packageName: String) = viewModelScope.launch { + downloaderPluginRepository.revokeTrustForPackage(packageName) + } +} \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/ui/viewmodel/GeneralSettingsViewModel.kt b/app/src/main/java/app/revanced/manager/ui/viewmodel/GeneralSettingsViewModel.kt new file mode 100644 index 0000000000..ea15c75745 --- /dev/null +++ b/app/src/main/java/app/revanced/manager/ui/viewmodel/GeneralSettingsViewModel.kt @@ -0,0 +1,15 @@ +package app.revanced.manager.ui.viewmodel + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import app.revanced.manager.domain.manager.PreferencesManager +import app.revanced.manager.ui.theme.Theme +import kotlinx.coroutines.launch + +class GeneralSettingsViewModel( + val prefs: PreferencesManager +) : ViewModel() { + fun setTheme(theme: Theme) = viewModelScope.launch { + prefs.theme.update(theme) + } +} \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/ui/viewmodel/ImportExportViewModel.kt b/app/src/main/java/app/revanced/manager/ui/viewmodel/ImportExportViewModel.kt new file mode 100644 index 0000000000..6fa042b760 --- /dev/null +++ b/app/src/main/java/app/revanced/manager/ui/viewmodel/ImportExportViewModel.kt @@ -0,0 +1,211 @@ +package app.revanced.manager.ui.viewmodel + +import android.app.Application +import android.net.Uri +import androidx.activity.result.contract.ActivityResultContract +import androidx.activity.result.contract.ActivityResultContracts +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import app.revanced.manager.R +import app.revanced.manager.domain.manager.KeystoreManager +import app.revanced.manager.domain.repository.PatchSelectionRepository +import app.revanced.manager.domain.repository.SerializedSelection +import app.revanced.manager.domain.repository.PatchBundleRepository +import app.revanced.manager.domain.bundles.PatchBundleSource +import app.revanced.manager.domain.repository.PatchOptionsRepository +import app.revanced.manager.util.JSON_MIMETYPE +import app.revanced.manager.util.toast +import app.revanced.manager.util.uiSafe +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import kotlinx.serialization.ExperimentalSerializationApi +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.decodeFromStream +import kotlinx.serialization.json.encodeToStream +import java.io.File +import java.nio.file.Files +import java.nio.file.Path +import java.nio.file.StandardCopyOption +import kotlin.io.path.deleteExisting +import kotlin.io.path.inputStream + +@OptIn(ExperimentalSerializationApi::class) +class ImportExportViewModel( + private val app: Application, + private val keystoreManager: KeystoreManager, + private val selectionRepository: PatchSelectionRepository, + private val optionsRepository: PatchOptionsRepository, + patchBundleRepository: PatchBundleRepository +) : ViewModel() { + private val contentResolver = app.contentResolver + val patchBundles = patchBundleRepository.sources + var selectedBundle by mutableStateOf<PatchBundleSource?>(null) + private set + var selectionAction by mutableStateOf<SelectionAction?>(null) + private set + private var keystoreImportPath by mutableStateOf<Path?>(null) + val showCredentialsDialog by derivedStateOf { keystoreImportPath != null } + + val packagesWithOptions = optionsRepository.getPackagesWithSavedOptions() + + fun resetOptionsForPackage(packageName: String) = viewModelScope.launch { + optionsRepository.clearOptionsForPackage(packageName) + app.toast(app.getString(R.string.patch_options_reset_toast)) + } + + fun clearOptionsForBundle(patchBundle: PatchBundleSource) = viewModelScope.launch { + optionsRepository.clearOptionsForPatchBundle(patchBundle.uid) + app.toast(app.getString(R.string.patch_options_reset_toast)) + } + + fun resetOptions() = viewModelScope.launch { + optionsRepository.reset() + app.toast(app.getString(R.string.patch_options_reset_toast)) + } + + fun startKeystoreImport(content: Uri) = viewModelScope.launch { + uiSafe(app, R.string.failed_to_import_keystore, "Failed to import keystore") { + val path = withContext(Dispatchers.IO) { + File.createTempFile("signing", "ks", app.cacheDir).toPath().also { + Files.copy( + contentResolver.openInputStream(content)!!, + it, + StandardCopyOption.REPLACE_EXISTING + ) + } + } + + aliases.forEach { alias -> + knownPasswords.forEach { pass -> + if (tryKeystoreImport(alias, pass, path)) { + return@launch + } + } + } + + keystoreImportPath = path + } + } + + fun cancelKeystoreImport() { + keystoreImportPath?.deleteExisting() + keystoreImportPath = null + } + + suspend fun tryKeystoreImport(cn: String, pass: String) = + tryKeystoreImport(cn, pass, keystoreImportPath!!) + + private suspend fun tryKeystoreImport(cn: String, pass: String, path: Path): Boolean { + path.inputStream().use { stream -> + if (keystoreManager.import(cn, pass, stream)) { + app.toast(app.getString(R.string.import_keystore_success)) + cancelKeystoreImport() + return true + } + } + + return false + } + + override fun onCleared() { + super.onCleared() + + cancelKeystoreImport() + } + + fun canExport() = keystoreManager.hasKeystore() + + fun exportKeystore(target: Uri) = viewModelScope.launch { + keystoreManager.export(contentResolver.openOutputStream(target)!!) + app.toast(app.getString(R.string.export_keystore_success)) + } + + fun regenerateKeystore() = viewModelScope.launch { + keystoreManager.regenerate() + app.toast(app.getString(R.string.regenerate_keystore_success)) + } + + fun resetSelection() = viewModelScope.launch { + withContext(Dispatchers.Default) { selectionRepository.reset() } + app.toast(app.getString(R.string.reset_patch_selection_success)) + } + + fun executeSelectionAction(target: Uri) = viewModelScope.launch { + val source = selectedBundle!! + val action = selectionAction!! + clearSelectionAction() + + action.execute(source.uid, target) + } + + fun selectBundle(bundle: PatchBundleSource) { + selectedBundle = bundle + } + + fun clearSelectionAction() { + selectionAction = null + selectedBundle = null + } + + fun importSelection() = clearSelectionAction().also { + selectionAction = Import() + } + + fun exportSelection() = clearSelectionAction().also { + selectionAction = Export() + } + + sealed interface SelectionAction { + suspend fun execute(bundleUid: Int, location: Uri) + val activityContract: ActivityResultContract<String, Uri?> + val activityArg: String + } + + private inner class Import : SelectionAction { + override val activityContract = ActivityResultContracts.GetContent() + override val activityArg = JSON_MIMETYPE + override suspend fun execute(bundleUid: Int, location: Uri) = uiSafe( + app, + R.string.import_patch_selection_fail, + "Failed to restore patch selection" + ) { + val selection = withContext(Dispatchers.IO) { + contentResolver.openInputStream(location)!!.use { + Json.decodeFromStream<SerializedSelection>(it) + } + } + + selectionRepository.import(bundleUid, selection) + app.toast(app.getString(R.string.import_patch_selection_success)) + } + } + + private inner class Export : SelectionAction { + override val activityContract = ActivityResultContracts.CreateDocument(JSON_MIMETYPE) + override val activityArg = "selection.json" + override suspend fun execute(bundleUid: Int, location: Uri) = uiSafe( + app, + R.string.export_patch_selection_fail, + "Failed to backup patch selection" + ) { + val selection = selectionRepository.export(bundleUid) + + withContext(Dispatchers.IO) { + contentResolver.openOutputStream(location, "wt")!!.use { + Json.Default.encodeToStream(selection, it) + } + } + app.toast(app.getString(R.string.export_patch_selection_success)) + } + } + + private companion object { + val knownPasswords = arrayOf("ReVanced", "s3cur3p@ssw0rd") + val aliases = arrayOf(KeystoreManager.DEFAULT, "alias", "ReVanced Key") + } +} \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/ui/viewmodel/InstalledAppInfoViewModel.kt b/app/src/main/java/app/revanced/manager/ui/viewmodel/InstalledAppInfoViewModel.kt new file mode 100644 index 0000000000..27b86263e3 --- /dev/null +++ b/app/src/main/java/app/revanced/manager/ui/viewmodel/InstalledAppInfoViewModel.kt @@ -0,0 +1,137 @@ +package app.revanced.manager.ui.viewmodel + +import android.app.Application +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.content.IntentFilter +import android.content.pm.PackageInfo +import android.content.pm.PackageInstaller +import android.util.Log +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue +import androidx.core.content.ContextCompat +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import app.revanced.manager.R +import app.revanced.manager.data.room.apps.installed.InstallType +import app.revanced.manager.data.room.apps.installed.InstalledApp +import app.revanced.manager.domain.installer.RootInstaller +import app.revanced.manager.domain.repository.InstalledAppRepository +import app.revanced.manager.service.UninstallService +import app.revanced.manager.util.PM +import app.revanced.manager.util.PatchSelection +import app.revanced.manager.util.simpleMessage +import app.revanced.manager.util.tag +import app.revanced.manager.util.toast +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import org.koin.core.component.KoinComponent +import org.koin.core.component.inject + +class InstalledAppInfoViewModel( + packageName: String +) : ViewModel(), KoinComponent { + private val context: Application by inject() + private val pm: PM by inject() + private val installedAppRepository: InstalledAppRepository by inject() + val rootInstaller: RootInstaller by inject() + + lateinit var onBackClick: () -> Unit + + var installedApp: InstalledApp? by mutableStateOf(null) + private set + var appInfo: PackageInfo? by mutableStateOf(null) + private set + var appliedPatches: PatchSelection? by mutableStateOf(null) + var isMounted by mutableStateOf(false) + private set + + init { + viewModelScope.launch { + installedApp = installedAppRepository.get(packageName)?.also { + isMounted = rootInstaller.isAppMounted(it.currentPackageName) + appInfo = withContext(Dispatchers.IO) { + pm.getPackageInfo(it.currentPackageName) + } + appliedPatches = withContext(Dispatchers.IO) { + installedAppRepository.getAppliedPatches(it.currentPackageName) + } + } + } + } + + fun launch() = installedApp?.currentPackageName?.let(pm::launch) + + fun mountOrUnmount() = viewModelScope.launch { + val pkgName = installedApp?.currentPackageName ?: return@launch + try { + if (isMounted) + rootInstaller.unmount(pkgName) + else + rootInstaller.mount(pkgName) + } catch (e: Exception) { + if (isMounted) { + context.toast(context.getString(R.string.failed_to_unmount, e.simpleMessage())) + Log.e(tag, "Failed to unmount", e) + } else { + context.toast(context.getString(R.string.failed_to_mount, e.simpleMessage())) + Log.e(tag, "Failed to mount", e) + } + } finally { + isMounted = rootInstaller.isAppMounted(pkgName) + } + } + + fun uninstall() { + val app = installedApp ?: return + when (app.installType) { + InstallType.DEFAULT -> pm.uninstallPackage(app.currentPackageName) + + InstallType.MOUNT -> viewModelScope.launch { + rootInstaller.uninstall(app.currentPackageName) + installedAppRepository.delete(app) + onBackClick() + } + } + } + + private val uninstallBroadcastReceiver = object : BroadcastReceiver() { + override fun onReceive(context: Context?, intent: Intent?) { + when (intent?.action) { + UninstallService.APP_UNINSTALL_ACTION -> { + val extraStatus = + intent.getIntExtra(UninstallService.EXTRA_UNINSTALL_STATUS, -999) + val extraStatusMessage = + intent.getStringExtra(UninstallService.EXTRA_UNINSTALL_STATUS_MESSAGE) + + if (extraStatus == PackageInstaller.STATUS_SUCCESS) { + viewModelScope.launch { + installedApp?.let { + installedAppRepository.delete(it) + } + onBackClick() + } + } else if (extraStatus != PackageInstaller.STATUS_FAILURE_ABORTED) { + this@InstalledAppInfoViewModel.context.toast(this@InstalledAppInfoViewModel.context.getString(R.string.uninstall_app_fail, extraStatusMessage)) + } + + } + } + } + }.also { + ContextCompat.registerReceiver( + context, + it, + IntentFilter(UninstallService.APP_UNINSTALL_ACTION), + ContextCompat.RECEIVER_NOT_EXPORTED + ) + } + + override fun onCleared() { + super.onCleared() + context.unregisterReceiver(uninstallBroadcastReceiver) + } +} \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/ui/viewmodel/InstalledAppsViewModel.kt b/app/src/main/java/app/revanced/manager/ui/viewmodel/InstalledAppsViewModel.kt new file mode 100644 index 0000000000..42ad08c7fa --- /dev/null +++ b/app/src/main/java/app/revanced/manager/ui/viewmodel/InstalledAppsViewModel.kt @@ -0,0 +1,52 @@ +package app.revanced.manager.ui.viewmodel + +import android.content.pm.PackageInfo +import androidx.compose.runtime.mutableStateMapOf +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import app.revanced.manager.data.room.apps.installed.InstallType +import app.revanced.manager.domain.installer.RootServiceException +import app.revanced.manager.domain.installer.RootInstaller +import app.revanced.manager.domain.repository.InstalledAppRepository +import app.revanced.manager.util.PM +import app.revanced.manager.util.collectEach +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext + +class InstalledAppsViewModel( + private val installedAppsRepository: InstalledAppRepository, + private val pm: PM, + private val rootInstaller: RootInstaller +) : ViewModel() { + val apps = installedAppsRepository.getAll().flowOn(Dispatchers.IO) + + val packageInfoMap = mutableStateMapOf<String, PackageInfo?>() + + init { + viewModelScope.launch { + apps.collectEach { installedApp -> + packageInfoMap[installedApp.currentPackageName] = withContext(Dispatchers.IO) { + try { + if ( + installedApp.installType == InstallType.MOUNT && !rootInstaller.isAppInstalled(installedApp.currentPackageName) + ) { + installedAppsRepository.delete(installedApp) + return@withContext null + } + } catch (_: RootServiceException) { } + + val packageInfo = pm.getPackageInfo(installedApp.currentPackageName) + + if (packageInfo == null && installedApp.installType != InstallType.MOUNT) { + installedAppsRepository.delete(installedApp) + return@withContext null + } + + packageInfo + } + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/ui/viewmodel/MainViewModel.kt b/app/src/main/java/app/revanced/manager/ui/viewmodel/MainViewModel.kt new file mode 100644 index 0000000000..dd87c0844c --- /dev/null +++ b/app/src/main/java/app/revanced/manager/ui/viewmodel/MainViewModel.kt @@ -0,0 +1,169 @@ +package app.revanced.manager.ui.viewmodel + +import android.app.Activity +import android.app.Application +import android.content.Intent +import android.util.Base64 +import android.util.Log +import androidx.activity.result.ActivityResult +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import app.revanced.manager.R +import app.revanced.manager.domain.bundles.PatchBundleSource.Extensions.asRemoteOrNull +import app.revanced.manager.domain.manager.KeystoreManager +import app.revanced.manager.domain.manager.PreferencesManager +import app.revanced.manager.domain.repository.DownloadedAppRepository +import app.revanced.manager.domain.repository.PatchBundleRepository +import app.revanced.manager.domain.repository.PatchSelectionRepository +import app.revanced.manager.domain.repository.SerializedSelection +import app.revanced.manager.ui.model.SelectedApp +import app.revanced.manager.ui.theme.Theme +import app.revanced.manager.util.tag +import app.revanced.manager.util.toast +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.receiveAsFlow +import kotlinx.coroutines.launch +import kotlinx.serialization.Serializable +import kotlinx.serialization.SerializationException +import kotlinx.serialization.json.Json + +class MainViewModel( + private val patchBundleRepository: PatchBundleRepository, + private val patchSelectionRepository: PatchSelectionRepository, + private val downloadedAppRepository: DownloadedAppRepository, + private val keystoreManager: KeystoreManager, + private val app: Application, + val prefs: PreferencesManager, + private val json: Json +) : ViewModel() { + private val appSelectChannel = Channel<SelectedApp>() + val appSelectFlow = appSelectChannel.receiveAsFlow() + private val legacyImportActivityChannel = Channel<Intent>() + val legacyImportActivityFlow = legacyImportActivityChannel.receiveAsFlow() + + private suspend fun suggestedVersion(packageName: String) = + patchBundleRepository.suggestedVersions.first()[packageName] + + private suspend fun findDownloadedApp(app: SelectedApp): SelectedApp.Local? { + if (app !is SelectedApp.Search) return null + + val suggestedVersion = suggestedVersion(app.packageName) ?: return null + + val downloadedApp = + downloadedAppRepository.get(app.packageName, suggestedVersion, markUsed = true) + ?: return null + return SelectedApp.Local( + downloadedApp.packageName, + downloadedApp.version, + downloadedAppRepository.getApkFileForApp(downloadedApp), + false + ) + } + + fun selectApp(app: SelectedApp) = viewModelScope.launch { + appSelectChannel.send(findDownloadedApp(app) ?: app) + } + + fun selectApp(packageName: String) = viewModelScope.launch { + selectApp(SelectedApp.Search(packageName, suggestedVersion(packageName))) + } + + init { + viewModelScope.launch { + if (!prefs.firstLaunch.get()) return@launch + legacyImportActivityChannel.send(Intent().apply { + setClassName( + "app.revanced.manager.flutter", + "app.revanced.manager.flutter.ExportSettingsActivity" + ) + }) + } + } + + fun applyLegacySettings(result: ActivityResult) { + if (result.resultCode != Activity.RESULT_OK) { + app.toast(app.getString(R.string.legacy_import_failed)) + Log.e( + tag, + "Got unknown result code while importing legacy settings: ${result.resultCode}" + ) + return + } + + val jsonStr = result.data?.getStringExtra("data") + if (jsonStr == null) { + app.toast(app.getString(R.string.legacy_import_failed)) + Log.e(tag, "Legacy settings data is null") + return + } + val settings = try { + json.decodeFromString<LegacySettings>(jsonStr) + } catch (e: SerializationException) { + app.toast(app.getString(R.string.legacy_import_failed)) + Log.e(tag, "Legacy settings data could not be deserialized", e) + return + } + + applyLegacySettings(settings) + } + + private fun applyLegacySettings(settings: LegacySettings) = viewModelScope.launch { + settings.themeMode?.let { theme -> + val themeMap = mapOf( + 0 to Theme.SYSTEM, + 1 to Theme.LIGHT, + 2 to Theme.DARK + ) + prefs.theme.update(themeMap[theme] ?: Theme.SYSTEM) + } + settings.useDynamicTheme?.let { dynamicColor -> + prefs.dynamicColor.update(dynamicColor) + } + settings.apiUrl?.let { api -> + prefs.api.update(api.removeSuffix("/")) + } + settings.experimentalPatchesEnabled?.let { allowExperimental -> + prefs.disablePatchVersionCompatCheck.update(allowExperimental) + } + settings.patchesAutoUpdate?.let { autoUpdate -> + with(patchBundleRepository) { + sources + .first() + .find { it.uid == 0 } + ?.asRemoteOrNull + ?.setAutoUpdate(autoUpdate) + + updateCheck() + } + } + settings.patchesChangeEnabled?.let { disableSelectionWarning -> + prefs.disableSelectionWarning.update(disableSelectionWarning) + } + settings.keystore?.let { keystore -> + val keystoreBytes = Base64.decode(keystore, Base64.DEFAULT) + keystoreManager.import( + "ReVanced", + settings.keystorePassword, + keystoreBytes.inputStream() + ) + } + settings.patches?.let { selection -> + patchSelectionRepository.import(0, selection) + } + Log.d(tag, "Imported legacy settings") + } + + @Serializable + private data class LegacySettings( + val keystorePassword: String, + val themeMode: Int? = null, + val useDynamicTheme: Boolean? = null, + val apiUrl: String? = null, + val experimentalPatchesEnabled: Boolean? = null, + val patchesAutoUpdate: Boolean? = null, + val patchesChangeEnabled: Boolean? = null, + val keystore: String? = null, + val patches: SerializedSelection? = null, + ) +} diff --git a/app/src/main/java/app/revanced/manager/ui/viewmodel/PatcherViewModel.kt b/app/src/main/java/app/revanced/manager/ui/viewmodel/PatcherViewModel.kt new file mode 100644 index 0000000000..4ca53667ca --- /dev/null +++ b/app/src/main/java/app/revanced/manager/ui/viewmodel/PatcherViewModel.kt @@ -0,0 +1,529 @@ +package app.revanced.manager.ui.viewmodel + +import android.app.Application +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.content.IntentFilter +import android.content.pm.PackageInstaller +import android.net.Uri +import android.os.ParcelUuid +import android.util.Log +import androidx.activity.result.ActivityResult +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.saveable.autoSaver +import androidx.compose.runtime.setValue +import androidx.compose.runtime.toMutableStateList +import androidx.core.content.ContextCompat +import androidx.lifecycle.SavedStateHandle +import androidx.lifecycle.ViewModel +import androidx.lifecycle.map +import androidx.lifecycle.viewModelScope +import androidx.lifecycle.viewmodel.compose.SavedStateHandleSaveableApi +import androidx.lifecycle.viewmodel.compose.saveable +import androidx.work.WorkInfo +import androidx.work.WorkManager +import app.revanced.manager.R +import app.revanced.manager.data.platform.Filesystem +import app.revanced.manager.data.room.apps.installed.InstallType +import app.revanced.manager.data.room.apps.installed.InstalledApp +import app.revanced.manager.domain.installer.RootInstaller +import app.revanced.manager.domain.repository.InstalledAppRepository +import app.revanced.manager.domain.worker.WorkerRepository +import app.revanced.manager.patcher.logger.LogLevel +import app.revanced.manager.patcher.logger.Logger +import app.revanced.manager.patcher.worker.PatcherWorker +import app.revanced.manager.plugin.downloader.PluginHostApi +import app.revanced.manager.plugin.downloader.UserInteractionException +import app.revanced.manager.service.InstallService +import app.revanced.manager.service.UninstallService +import app.revanced.manager.ui.model.InstallerModel +import app.revanced.manager.ui.model.ProgressKey +import app.revanced.manager.ui.model.SelectedApp +import app.revanced.manager.ui.model.State +import app.revanced.manager.ui.model.Step +import app.revanced.manager.ui.model.StepCategory +import app.revanced.manager.ui.model.StepProgressProvider +import app.revanced.manager.ui.model.navigation.Patcher +import app.revanced.manager.util.PM +import app.revanced.manager.util.saveableVar +import app.revanced.manager.util.saver.snapshotStateListSaver +import app.revanced.manager.util.simpleMessage +import app.revanced.manager.util.tag +import app.revanced.manager.util.toast +import app.revanced.manager.util.uiSafe +import kotlinx.coroutines.CompletableDeferred +import kotlinx.coroutines.DelicateCoroutinesApi +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.flow.receiveAsFlow +import kotlinx.coroutines.launch +import kotlinx.coroutines.time.withTimeout +import kotlinx.coroutines.withContext +import org.koin.core.component.KoinComponent +import org.koin.core.component.get +import org.koin.core.component.inject +import java.io.File +import java.nio.file.Files +import java.time.Duration + +@OptIn(SavedStateHandleSaveableApi::class, PluginHostApi::class) +class PatcherViewModel( + private val input: Patcher.ViewModelParams +) : ViewModel(), KoinComponent, StepProgressProvider, InstallerModel { + private val app: Application by inject() + private val fs: Filesystem by inject() + private val pm: PM by inject() + private val workerRepository: WorkerRepository by inject() + private val installedAppRepository: InstalledAppRepository by inject() + private val rootInstaller: RootInstaller by inject() + private val savedStateHandle: SavedStateHandle = get() + + private var installedApp: InstalledApp? = null + val packageName = input.selectedApp.packageName + + var installedPackageName by savedStateHandle.saveable( + key = "installedPackageName", + // Force Kotlin to select the correct overload. + stateSaver = autoSaver() + ) { + mutableStateOf<String?>(null) + } + private set + private var ongoingPmSession: Boolean by savedStateHandle.saveableVar { false } + var packageInstallerStatus: Int? by savedStateHandle.saveable( + key = "packageInstallerStatus", + stateSaver = autoSaver() + ) { + mutableStateOf(null) + } + private set + + var isInstalling by mutableStateOf(ongoingPmSession) + private set + + private var currentActivityRequest: Pair<CompletableDeferred<Boolean>, String>? by mutableStateOf( + null + ) + val activityPromptDialog by derivedStateOf { currentActivityRequest?.second } + + private var launchedActivity: CompletableDeferred<ActivityResult>? = null + private val launchActivityChannel = Channel<Intent>() + val launchActivityFlow = launchActivityChannel.receiveAsFlow() + + private val tempDir = savedStateHandle.saveable(key = "tempDir") { + fs.uiTempDir.resolve("installer").also { + it.deleteRecursively() + it.mkdirs() + } + } + + private var inputFile: File? by savedStateHandle.saveableVar() + private val outputFile = tempDir.resolve("output.apk") + + private val logs by savedStateHandle.saveable<MutableList<Pair<LogLevel, String>>> { mutableListOf() } + private val logger = object : Logger() { + override fun log(level: LogLevel, message: String) { + level.androidLog(message) + if (level == LogLevel.TRACE) return + + viewModelScope.launch { + logs.add(level to message) + } + } + } + + private val patchCount = input.selectedPatches.values.sumOf { it.size } + private var completedPatchCount by savedStateHandle.saveable { + // SavedStateHandle.saveable only supports the boxed version. + @Suppress("AutoboxingStateCreation") mutableStateOf( + 0 + ) + } + val patchesProgress get() = completedPatchCount to patchCount + override var downloadProgress by savedStateHandle.saveable( + key = "downloadProgress", + stateSaver = autoSaver() + ) { + mutableStateOf<Pair<Long, Long?>?>(null) + } + private set + val steps by savedStateHandle.saveable(saver = snapshotStateListSaver()) { + generateSteps( + app, + input.selectedApp + ).toMutableStateList() + } + private var currentStepIndex = 0 + + val progress by derivedStateOf { + val current = steps.count { + it.state == State.COMPLETED && it.category != StepCategory.PATCHING + } + completedPatchCount + + val total = steps.size - 1 + patchCount + + current.toFloat() / total.toFloat() + } + + private val workManager = WorkManager.getInstance(app) + + private val patcherWorkerId by savedStateHandle.saveable<ParcelUuid> { + ParcelUuid(workerRepository.launchExpedited<PatcherWorker, PatcherWorker.Args>( + "patching", PatcherWorker.Args( + input.selectedApp, + outputFile.path, + input.selectedPatches, + input.options, + logger, + onDownloadProgress = { + withContext(Dispatchers.Main) { + downloadProgress = it + } + }, + onPatchCompleted = { withContext(Dispatchers.Main) { completedPatchCount += 1 } }, + setInputFile = { withContext(Dispatchers.Main) { inputFile = it } }, + handleStartActivityRequest = { plugin, intent -> + withContext(Dispatchers.Main) { + if (currentActivityRequest != null) throw Exception("Another request is already pending.") + try { + // Wait for the dialog interaction. + val accepted = with(CompletableDeferred<Boolean>()) { + currentActivityRequest = this to plugin.name + + await() + } + if (!accepted) throw UserInteractionException.RequestDenied() + + // Launch the activity and wait for the result. + try { + with(CompletableDeferred<ActivityResult>()) { + launchedActivity = this + launchActivityChannel.send(intent) + await() + } + } finally { + launchedActivity = null + } + } finally { + currentActivityRequest = null + } + } + }, + onProgress = { name, state, message -> + viewModelScope.launch { + steps[currentStepIndex] = steps[currentStepIndex].run { + copy( + name = name ?: this.name, + state = state ?: this.state, + message = message ?: this.message + ) + } + + if (state == State.COMPLETED && currentStepIndex != steps.lastIndex) { + currentStepIndex++ + + steps[currentStepIndex] = + steps[currentStepIndex].copy(state = State.RUNNING) + } + } + } + ) + )) + } + + val patcherSucceeded = + workManager.getWorkInfoByIdLiveData(patcherWorkerId.uuid).map { workInfo: WorkInfo? -> + when (workInfo?.state) { + WorkInfo.State.SUCCEEDED -> true + WorkInfo.State.FAILED -> false + else -> null + } + } + + private val installerBroadcastReceiver = object : BroadcastReceiver() { + override fun onReceive(context: Context?, intent: Intent?) { + when (intent?.action) { + InstallService.APP_INSTALL_ACTION -> { + val pmStatus = intent.getIntExtra( + InstallService.EXTRA_INSTALL_STATUS, + PackageInstaller.STATUS_FAILURE + ) + + intent.getStringExtra(UninstallService.EXTRA_UNINSTALL_STATUS_MESSAGE) + ?.let(logger::trace) + + if (pmStatus == PackageInstaller.STATUS_SUCCESS) { + app.toast(app.getString(R.string.install_app_success)) + installedPackageName = + intent.getStringExtra(InstallService.EXTRA_PACKAGE_NAME) + viewModelScope.launch { + installedAppRepository.addOrUpdate( + installedPackageName!!, + packageName, + input.selectedApp.version + ?: pm.getPackageInfo(outputFile)?.versionName!!, + InstallType.DEFAULT, + input.selectedPatches + ) + } + } else packageInstallerStatus = pmStatus + + isInstalling = false + } + + UninstallService.APP_UNINSTALL_ACTION -> { + val pmStatus = intent.getIntExtra( + UninstallService.EXTRA_UNINSTALL_STATUS, + PackageInstaller.STATUS_FAILURE + ) + + intent.getStringExtra(UninstallService.EXTRA_UNINSTALL_STATUS_MESSAGE) + ?.let(logger::trace) + + if (pmStatus != PackageInstaller.STATUS_SUCCESS) + packageInstallerStatus = pmStatus + } + } + } + } + + init { + // TODO: detect system-initiated process death during the patching process. + ContextCompat.registerReceiver( + app, + installerBroadcastReceiver, + IntentFilter().apply { + addAction(InstallService.APP_INSTALL_ACTION) + addAction(UninstallService.APP_UNINSTALL_ACTION) + }, + ContextCompat.RECEIVER_NOT_EXPORTED + ) + + viewModelScope.launch { + installedApp = installedAppRepository.get(packageName) + } + } + + @OptIn(DelicateCoroutinesApi::class) + override fun onCleared() { + super.onCleared() + app.unregisterReceiver(installerBroadcastReceiver) + workManager.cancelWorkById(patcherWorkerId.uuid) + + if (input.selectedApp is SelectedApp.Installed && installedApp?.installType == InstallType.MOUNT) { + GlobalScope.launch(Dispatchers.Main) { + uiSafe(app, R.string.failed_to_mount, "Failed to mount") { + withTimeout(Duration.ofMinutes(1L)) { + rootInstaller.mount(packageName) + } + } + } + } + } + + fun onBack() { + // tempDir cannot be deleted inside onCleared because it gets called on system-initiated process death. + tempDir.deleteRecursively() + } + + fun isDeviceRooted() = rootInstaller.isDeviceRooted() + + fun rejectInteraction() { + currentActivityRequest?.first?.complete(false) + } + + fun allowInteraction() { + currentActivityRequest?.first?.complete(true) + } + + fun handleActivityResult(result: ActivityResult) { + launchedActivity?.complete(result) + } + + fun export(uri: Uri?) = viewModelScope.launch { + uri?.let { + withContext(Dispatchers.IO) { + app.contentResolver.openOutputStream(it) + .use { stream -> Files.copy(outputFile.toPath(), stream) } + } + app.toast(app.getString(R.string.save_apk_success)) + } + } + + fun exportLogs(context: Context) { + val sendIntent: Intent = Intent().apply { + action = Intent.ACTION_SEND + putExtra( + Intent.EXTRA_TEXT, + logs.asSequence().map { (level, msg) -> "[${level.name}]: $msg" }.joinToString("\n") + ) + type = "text/plain" + } + + val shareIntent = Intent.createChooser(sendIntent, null) + context.startActivity(shareIntent) + } + + fun open() = installedPackageName?.let(pm::launch) + + fun install(installType: InstallType) = viewModelScope.launch { + var pmInstallStarted = false + try { + isInstalling = true + + val currentPackageInfo = pm.getPackageInfo(outputFile) + ?: throw Exception("Failed to load application info") + + // If the app is currently installed + val existingPackageInfo = pm.getPackageInfo(currentPackageInfo.packageName) + if (existingPackageInfo != null) { + // Check if the app version is less than the installed version + if (pm.getVersionCode(currentPackageInfo) < pm.getVersionCode(existingPackageInfo)) { + // Exit if the selected app version is less than the installed version + packageInstallerStatus = PackageInstaller.STATUS_FAILURE_CONFLICT + return@launch + } + } + + when (installType) { + InstallType.DEFAULT -> { + // Check if the app is mounted as root + // If it is, unmount it first, silently + if (rootInstaller.hasRootAccess() && rootInstaller.isAppMounted(packageName)) { + rootInstaller.unmount(packageName) + } + + // Install regularly + pm.installApp(listOf(outputFile)) + pmInstallStarted = true + } + + InstallType.MOUNT -> { + try { + val packageInfo = pm.getPackageInfo(outputFile) + ?: throw Exception("Failed to load application info") + val label = with(pm) { + packageInfo.label() + } + + // Check for base APK, first check if the app is already installed + if (existingPackageInfo == null) { + // If the app is not installed, check if the output file is a base apk + if (currentPackageInfo.splitNames.isNotEmpty()) { + // Exit if there is no base APK package + packageInstallerStatus = PackageInstaller.STATUS_FAILURE_INVALID + return@launch + } + } + + val inputVersion = input.selectedApp.version + ?: inputFile?.let(pm::getPackageInfo)?.versionName + ?: throw Exception("Failed to determine input APK version") + + // Install as root + rootInstaller.install( + outputFile, + inputFile, + packageName, + inputVersion, + label + ) + + installedAppRepository.addOrUpdate( + packageInfo.packageName, + packageName, + inputVersion, + InstallType.MOUNT, + input.selectedPatches + ) + + rootInstaller.mount(packageName) + + installedPackageName = packageName + + app.toast(app.getString(R.string.install_app_success)) + } catch (e: Exception) { + Log.e(tag, "Failed to install as root", e) + app.toast(app.getString(R.string.install_app_fail, e.simpleMessage())) + try { + rootInstaller.uninstall(packageName) + } catch (_: Exception) { + } + } + } + } + } catch (e: Exception) { + Log.e(tag, "Failed to install", e) + app.toast(app.getString(R.string.install_app_fail, e.simpleMessage())) + } finally { + if (!pmInstallStarted) isInstalling = false + } + } + + override fun install() { + // InstallType.MOUNT is never used here since this overload is for the package installer status dialog. + install(InstallType.DEFAULT) + } + + override fun reinstall() { + viewModelScope.launch { + uiSafe(app, R.string.reinstall_app_fail, "Failed to reinstall") { + pm.getPackageInfo(outputFile)?.packageName?.let { pm.uninstallPackage(it) } + ?: throw Exception("Failed to load application info") + + pm.installApp(listOf(outputFile)) + isInstalling = true + } + } + } + + fun dismissPackageInstallerDialog() { + packageInstallerStatus = null + } + + private companion object { + const val TAG = "ReVanced Patcher" + + fun LogLevel.androidLog(msg: String) = when (this) { + LogLevel.TRACE -> Log.v(TAG, msg) + LogLevel.INFO -> Log.i(TAG, msg) + LogLevel.WARN -> Log.w(TAG, msg) + LogLevel.ERROR -> Log.e(TAG, msg) + } + + fun generateSteps(context: Context, selectedApp: SelectedApp): List<Step> { + val needsDownload = + selectedApp is SelectedApp.Download || selectedApp is SelectedApp.Search + + return listOfNotNull( + Step( + context.getString(R.string.download_apk), + StepCategory.PREPARING, + state = State.RUNNING, + progressKey = ProgressKey.DOWNLOAD, + ).takeIf { needsDownload }, + Step( + context.getString(R.string.patcher_step_load_patches), + StepCategory.PREPARING, + state = if (needsDownload) State.WAITING else State.RUNNING, + ), + Step( + context.getString(R.string.patcher_step_unpack), + StepCategory.PREPARING + ), + + Step( + context.getString(R.string.execute_patches), + StepCategory.PATCHING + ), + + Step(context.getString(R.string.patcher_step_write_patched), StepCategory.SAVING), + Step(context.getString(R.string.patcher_step_sign_apk), StepCategory.SAVING) + ) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/ui/viewmodel/PatchesSelectorViewModel.kt b/app/src/main/java/app/revanced/manager/ui/viewmodel/PatchesSelectorViewModel.kt new file mode 100644 index 0000000000..54f2b7b896 --- /dev/null +++ b/app/src/main/java/app/revanced/manager/ui/viewmodel/PatchesSelectorViewModel.kt @@ -0,0 +1,241 @@ +package app.revanced.manager.ui.viewmodel + +import android.app.Application +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.setValue +import androidx.compose.runtime.mutableStateListOf +import androidx.compose.runtime.mutableStateMapOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.saveable.Saver +import androidx.compose.runtime.snapshots.SnapshotStateMap +import androidx.lifecycle.SavedStateHandle +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import androidx.lifecycle.viewmodel.compose.SavedStateHandleSaveableApi +import androidx.lifecycle.viewmodel.compose.saveable +import app.revanced.manager.R +import app.revanced.manager.domain.manager.PreferencesManager +import app.revanced.manager.domain.repository.PatchBundleRepository +import app.revanced.manager.patcher.patch.PatchInfo +import app.revanced.manager.ui.model.BundleInfo +import app.revanced.manager.ui.model.BundleInfo.Extensions.bundleInfoFlow +import app.revanced.manager.ui.model.BundleInfo.Extensions.toPatchSelection +import app.revanced.manager.ui.model.navigation.SelectedApplicationInfo +import app.revanced.manager.util.Options +import app.revanced.manager.util.PatchSelection +import app.revanced.manager.util.saver.Nullable +import app.revanced.manager.util.saver.nullableSaver +import app.revanced.manager.util.saver.persistentMapSaver +import app.revanced.manager.util.saver.persistentSetSaver +import app.revanced.manager.util.saver.snapshotStateMapSaver +import app.revanced.manager.util.toast +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.launch +import org.koin.core.component.KoinComponent +import org.koin.core.component.get +import kotlinx.collections.immutable.* +import kotlinx.coroutines.CoroutineStart +import kotlinx.coroutines.async +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.flow.map + +@OptIn(SavedStateHandleSaveableApi::class) +class PatchesSelectorViewModel(input: SelectedApplicationInfo.PatchesSelector.ViewModelParams) : + ViewModel(), KoinComponent { + private val app: Application = get() + private val savedStateHandle: SavedStateHandle = get() + private val prefs: PreferencesManager = get() + + private val packageName = input.app.packageName + val appVersion = input.app.version + + var pendingUniversalPatchAction by mutableStateOf<(() -> Unit)?>(null) + + var selectionWarningEnabled by mutableStateOf(true) + private set + var universalPatchWarningEnabled by mutableStateOf(true) + private set + + val allowIncompatiblePatches = + get<PreferencesManager>().disablePatchVersionCompatCheck.getBlocking() + val bundlesFlow = + get<PatchBundleRepository>().bundleInfoFlow(packageName, input.app.version) + + init { + viewModelScope.launch { + universalPatchWarningEnabled = !prefs.disableUniversalPatchWarning.get() + + if (prefs.disableSelectionWarning.get()) { + selectionWarningEnabled = false + return@launch + } + + fun BundleInfo.hasDefaultPatches() = + patchSequence(allowIncompatiblePatches).any { it.include } + + // Don't show the warning if there are no default patches. + selectionWarningEnabled = bundlesFlow.first().any(BundleInfo::hasDefaultPatches) + } + } + + private var hasModifiedSelection = false + var customPatchSelection: PersistentPatchSelection? by savedStateHandle.saveable( + key = "selection", + stateSaver = selectionSaver, + ) { + mutableStateOf(input.currentSelection?.toPersistentPatchSelection()) + } + + private val patchOptions: PersistentOptions by savedStateHandle.saveable( + saver = optionsSaver, + ) { + // Convert Options to PersistentOptions + input.options.mapValuesTo(mutableStateMapOf()) { (_, allPatches) -> + allPatches.mapValues { (_, options) -> options.toPersistentMap() }.toPersistentMap() + } + } + + /** + * Show the patch options dialog for this patch. + */ + var optionsDialog by mutableStateOf<Pair<Int, PatchInfo>?>(null) + + val compatibleVersions = mutableStateListOf<String>() + + var filter by mutableIntStateOf(SHOW_UNIVERSAL) + private set + + private val defaultPatchSelection = bundlesFlow.map { bundles -> + bundles.toPatchSelection(allowIncompatiblePatches) { _, patch -> patch.include } + .toPersistentPatchSelection() + } + + val defaultSelectionCount = defaultPatchSelection.map { selection -> + selection.values.sumOf { it.size } + } + + // This is for the required options screen. + private val requiredOptsPatchesDeferred = viewModelScope.async(start = CoroutineStart.LAZY) { + bundlesFlow.first().map { bundle -> + bundle to bundle.all.filter { patch -> + val opts by lazy { + getOptions(bundle.uid, patch).orEmpty() + } + isSelected( + bundle.uid, + patch + ) && patch.options?.any { it.required && it.default == null && it.key !in opts } ?: false + }.toList() + }.filter { (_, patches) -> patches.isNotEmpty() } + } + val requiredOptsPatches = flow { emit(requiredOptsPatchesDeferred.await()) } + + fun selectionIsValid(bundles: List<BundleInfo>) = bundles.any { bundle -> + bundle.patchSequence(allowIncompatiblePatches).any { patch -> + isSelected(bundle.uid, patch) + } + } + + fun isSelected(bundle: Int, patch: PatchInfo) = customPatchSelection?.let { selection -> + selection[bundle]?.contains(patch.name) ?: false + } ?: patch.include + + fun togglePatch(bundle: Int, patch: PatchInfo) = viewModelScope.launch { + hasModifiedSelection = true + + val selection = customPatchSelection ?: defaultPatchSelection.first() + val newPatches = selection[bundle]?.let { patches -> + if (patch.name in patches) + patches.remove(patch.name) + else + patches.add(patch.name) + } ?: persistentSetOf(patch.name) + + customPatchSelection = selection.put(bundle, newPatches) + } + + fun confirmUniversalPatchWarning() { + universalPatchWarningEnabled = false + + pendingUniversalPatchAction?.invoke() + pendingUniversalPatchAction = null + } + + fun dismissUniversalPatchWarning() { + pendingUniversalPatchAction = null + } + + fun reset() { + patchOptions.clear() + customPatchSelection = null + hasModifiedSelection = false + app.toast(app.getString(R.string.patch_selection_reset_toast)) + } + + fun getCustomSelection(): PatchSelection? { + // Convert persistent collections to standard hash collections because persistent collections are not parcelable. + + return customPatchSelection?.mapValues { (_, v) -> v.toSet() } + } + + fun getOptions(): Options { + // Convert the collection for the same reasons as in getCustomSelection() + + return patchOptions.mapValues { (_, allPatches) -> allPatches.mapValues { (_, options) -> options.toMap() } } + } + + fun getOptions(bundle: Int, patch: PatchInfo) = patchOptions[bundle]?.get(patch.name) + + fun setOption(bundle: Int, patch: PatchInfo, key: String, value: Any?) { + // All patches + val patchesToOpts = patchOptions.getOrElse(bundle, ::persistentMapOf) + // The key-value options of an individual patch + val patchToOpts = patchesToOpts + .getOrElse(patch.name, ::persistentMapOf) + .put(key, value) + + patchOptions[bundle] = patchesToOpts.put(patch.name, patchToOpts) + } + + fun resetOptions(bundle: Int, patch: PatchInfo) { + app.toast(app.getString(R.string.patch_options_reset_toast)) + patchOptions[bundle] = patchOptions[bundle]?.remove(patch.name) ?: return + } + + fun dismissDialogs() { + optionsDialog = null + compatibleVersions.clear() + } + + fun openUnsupportedDialog(unsupportedPatch: PatchInfo) { + compatibleVersions.addAll(unsupportedPatch.compatiblePackages?.find { it.packageName == packageName }?.versions.orEmpty()) + } + + fun toggleFlag(flag: Int) { + filter = filter xor flag + } + + companion object { + const val SHOW_UNSUPPORTED = 1 // 2^0 + const val SHOW_UNIVERSAL = 2 // 2^1 + + private val optionsSaver: Saver<PersistentOptions, Options> = snapshotStateMapSaver( + // Patch name -> Options + valueSaver = persistentMapSaver( + // Option key -> Option value + valueSaver = persistentMapSaver() + ) + ) + + private val selectionSaver: Saver<PersistentPatchSelection?, Nullable<PatchSelection>> = + nullableSaver(persistentMapSaver(valueSaver = persistentSetSaver())) + } +} + +// Versions of other types, but utilizing persistent/observable collection types. +private typealias PersistentOptions = SnapshotStateMap<Int, PersistentMap<String, PersistentMap<String, Any?>>> +private typealias PersistentPatchSelection = PersistentMap<Int, PersistentSet<String>> + +private fun PatchSelection.toPersistentPatchSelection(): PersistentPatchSelection = + mapValues { (_, v) -> v.toPersistentSet() }.toPersistentMap() \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/ui/viewmodel/SelectedAppInfoViewModel.kt b/app/src/main/java/app/revanced/manager/ui/viewmodel/SelectedAppInfoViewModel.kt new file mode 100644 index 0000000000..302b5e89c9 --- /dev/null +++ b/app/src/main/java/app/revanced/manager/ui/viewmodel/SelectedAppInfoViewModel.kt @@ -0,0 +1,364 @@ +package app.revanced.manager.ui.viewmodel + +import android.app.Activity +import android.app.Application +import android.content.Intent +import android.content.pm.PackageInfo +import android.os.Parcelable +import android.util.Log +import androidx.activity.result.ActivityResult +import androidx.annotation.StringRes +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue +import androidx.compose.runtime.snapshotFlow +import androidx.lifecycle.SavedStateHandle +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import androidx.lifecycle.viewmodel.compose.SavedStateHandleSaveableApi +import androidx.lifecycle.viewmodel.compose.saveable +import app.revanced.manager.R +import app.revanced.manager.data.room.apps.installed.InstalledApp +import app.revanced.manager.domain.installer.RootInstaller +import app.revanced.manager.domain.manager.PreferencesManager +import app.revanced.manager.domain.repository.DownloaderPluginRepository +import app.revanced.manager.domain.repository.InstalledAppRepository +import app.revanced.manager.domain.repository.PatchBundleRepository +import app.revanced.manager.domain.repository.PatchOptionsRepository +import app.revanced.manager.domain.repository.PatchSelectionRepository +import app.revanced.manager.network.downloader.LoadedDownloaderPlugin +import app.revanced.manager.network.downloader.ParceledDownloaderData +import app.revanced.manager.patcher.patch.PatchInfo +import app.revanced.manager.plugin.downloader.GetScope +import app.revanced.manager.plugin.downloader.PluginHostApi +import app.revanced.manager.plugin.downloader.UserInteractionException +import app.revanced.manager.ui.model.BundleInfo +import app.revanced.manager.ui.model.BundleInfo.Extensions.bundleInfoFlow +import app.revanced.manager.ui.model.BundleInfo.Extensions.toPatchSelection +import app.revanced.manager.ui.model.BundleInfo.Extensions.requiredOptionsSet +import app.revanced.manager.ui.model.SelectedApp +import app.revanced.manager.ui.model.navigation.Patcher +import app.revanced.manager.ui.model.navigation.SelectedApplicationInfo +import app.revanced.manager.util.Options +import app.revanced.manager.util.PM +import app.revanced.manager.util.PatchSelection +import app.revanced.manager.util.simpleMessage +import app.revanced.manager.util.tag +import app.revanced.manager.util.toast +import kotlinx.coroutines.CancellationException +import kotlinx.coroutines.CompletableDeferred +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.async +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.receiveAsFlow +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import kotlinx.parcelize.Parcelize +import org.koin.core.component.KoinComponent +import org.koin.core.component.get + +@OptIn(SavedStateHandleSaveableApi::class, PluginHostApi::class) +class SelectedAppInfoViewModel( + input: SelectedApplicationInfo.ViewModelParams +) : ViewModel(), KoinComponent { + private val app: Application = get() + private val bundleRepository: PatchBundleRepository = get() + private val selectionRepository: PatchSelectionRepository = get() + private val optionsRepository: PatchOptionsRepository = get() + private val pluginsRepository: DownloaderPluginRepository = get() + private val installedAppRepository: InstalledAppRepository = get() + private val rootInstaller: RootInstaller = get() + private val pm: PM = get() + private val savedStateHandle: SavedStateHandle = get() + val prefs: PreferencesManager = get() + val plugins = pluginsRepository.loadedPluginsFlow + val desiredVersion = input.app.version + val packageName = input.app.packageName + + private val persistConfiguration = input.patches == null + + val hasRoot = rootInstaller.hasRootAccess() + var installedAppData: Pair<SelectedApp.Installed, InstalledApp?>? by mutableStateOf(null) + private set + + private var _selectedApp by savedStateHandle.saveable { + mutableStateOf(input.app) + } + + var selectedAppInfo: PackageInfo? by mutableStateOf(null) + private set + + var selectedApp + get() = _selectedApp + set(value) { + _selectedApp = value + invalidateSelectedAppInfo() + } + + init { + invalidateSelectedAppInfo() + viewModelScope.launch(Dispatchers.Main) { + val packageInfo = async(Dispatchers.IO) { pm.getPackageInfo(packageName) } + val installedAppDeferred = + async(Dispatchers.IO) { installedAppRepository.get(packageName) } + + installedAppData = + packageInfo.await()?.let { + SelectedApp.Installed( + packageName, + it.versionName!! + ) to installedAppDeferred.await() + } + } + } + + val requiredVersion = combine( + prefs.suggestedVersionSafeguard.flow, + bundleRepository.suggestedVersions + ) { suggestedVersionSafeguard, suggestedVersions -> + if (!suggestedVersionSafeguard) return@combine null + + suggestedVersions[input.app.packageName] + } + + var options: Options by savedStateHandle.saveable { + val state = mutableStateOf<Options>(emptyMap()) + + viewModelScope.launch { + if (!persistConfiguration) return@launch // TODO: save options for patched apps. + + state.value = withContext(Dispatchers.Default) { + val bundlePatches = bundleRepository.bundles.first() + .mapValues { (_, bundle) -> bundle.patches.associateBy { it.name } } + + optionsRepository.getOptions(packageName, bundlePatches) + } + } + + state + } + private set + + private var selectionState by savedStateHandle.saveable { + if (input.patches != null) + return@saveable mutableStateOf(SelectionState.Customized(input.patches)) + + val selection: MutableState<SelectionState> = mutableStateOf(SelectionState.Default) + + // Try to get the previous selection if customization is enabled. + viewModelScope.launch { + if (!prefs.disableSelectionWarning.get()) return@launch + + val previous = selectionRepository.getSelection(packageName) + if (previous.values.sumOf { it.size } == 0) return@launch + selection.value = SelectionState.Customized(previous) + } + + selection + } + + var showSourceSelector by mutableStateOf(false) + private set + private var pluginAction: Pair<LoadedDownloaderPlugin, Job>? by mutableStateOf(null) + val activePluginAction get() = pluginAction?.first?.packageName + private var launchedActivity by mutableStateOf<CompletableDeferred<ActivityResult>?>(null) + private val launchActivityChannel = Channel<Intent>() + val launchActivityFlow = launchActivityChannel.receiveAsFlow() + + val errorFlow = combine(plugins, snapshotFlow { selectedApp }) { pluginsList, app -> + when { + app is SelectedApp.Search && pluginsList.isEmpty() -> Error.NoPlugins + else -> null + } + } + + val bundleInfoFlow by derivedStateOf { + bundleRepository.bundleInfoFlow(packageName, selectedApp.version) + } + + fun showSourceSelector() { + dismissSourceSelector() + showSourceSelector = true + } + + private fun cancelPluginAction() { + pluginAction?.second?.cancel() + pluginAction = null + } + + fun dismissSourceSelector() { + cancelPluginAction() + showSourceSelector = false + } + + fun searchUsingPlugin(plugin: LoadedDownloaderPlugin) { + cancelPluginAction() + pluginAction = plugin to viewModelScope.launch { + try { + val scope = object : GetScope { + override val hostPackageName = app.packageName + override val pluginPackageName = plugin.packageName + override suspend fun requestStartActivity(intent: Intent) = + withContext(Dispatchers.Main) { + if (launchedActivity != null) error("Previous activity has not finished") + try { + val result = with(CompletableDeferred<ActivityResult>()) { + launchedActivity = this + launchActivityChannel.send(intent) + await() + } + when (result.resultCode) { + Activity.RESULT_OK -> result.data + Activity.RESULT_CANCELED -> throw UserInteractionException.Activity.Cancelled() + else -> throw UserInteractionException.Activity.NotCompleted( + result.resultCode, + result.data + ) + } + } finally { + launchedActivity = null + } + } + } + + withContext(Dispatchers.IO) { + plugin.get(scope, packageName, desiredVersion) + }?.let { (data, version) -> + if (desiredVersion != null && version != desiredVersion) { + app.toast(app.getString(R.string.downloader_invalid_version)) + return@launch + } + selectedApp = SelectedApp.Download( + packageName, + version, + ParceledDownloaderData(plugin, data) + ) + } ?: app.toast(app.getString(R.string.downloader_app_not_found)) + } catch (e: UserInteractionException.Activity) { + app.toast(e.message!!) + } catch (e: CancellationException) { + throw e + } catch (e: Exception) { + app.toast(app.getString(R.string.downloader_error, e.simpleMessage())) + Log.e(tag, "Downloader.get threw an exception", e) + } finally { + pluginAction = null + dismissSourceSelector() + } + } + } + + fun handlePluginActivityResult(result: ActivityResult) { + launchedActivity?.complete(result) + } + + private fun invalidateSelectedAppInfo() = viewModelScope.launch { + val info = when (val app = selectedApp) { + is SelectedApp.Local -> withContext(Dispatchers.IO) { pm.getPackageInfo(app.file) } + is SelectedApp.Installed -> withContext(Dispatchers.IO) { pm.getPackageInfo(app.packageName) } + else -> null + } + + selectedAppInfo = info + } + + suspend fun hasSetRequiredOptions(patchSelection: PatchSelection) = bundleInfoFlow + .first() + .requiredOptionsSet( + isSelected = { bundle, patch -> patch.name in patchSelection[bundle.uid]!! }, + optionsForPatch = { bundle, patch -> options[bundle.uid]?.get(patch.name) }, + ) + + suspend fun getPatcherParams(): Patcher.ViewModelParams { + val allowUnsupported = prefs.disablePatchVersionCompatCheck.get() + val bundles = bundleInfoFlow.first() + return Patcher.ViewModelParams( + selectedApp, + getPatches(bundles, allowUnsupported), + getOptionsFiltered(bundles) + ) + } + + fun getOptionsFiltered(bundles: List<BundleInfo>) = options.filtered(bundles) + + fun getPatches(bundles: List<BundleInfo>, allowUnsupported: Boolean) = + selectionState.patches(bundles, allowUnsupported) + + fun getCustomPatches( + bundles: List<BundleInfo>, + allowUnsupported: Boolean + ): PatchSelection? = + (selectionState as? SelectionState.Customized)?.patches(bundles, allowUnsupported) + + fun updateConfiguration(selection: PatchSelection?, options: Options) = viewModelScope.launch { + val bundles = bundleInfoFlow.first() + + selectionState = selection?.let(SelectionState::Customized) ?: SelectionState.Default + + val filteredOptions = options.filtered(bundles) + this@SelectedAppInfoViewModel.options = filteredOptions + + if (!persistConfiguration) return@launch + viewModelScope.launch(Dispatchers.Default) { + selection?.let { selectionRepository.updateSelection(packageName, it) } + ?: selectionRepository.clearSelection(packageName) + + optionsRepository.saveOptions(packageName, filteredOptions) + } + } + + enum class Error(@StringRes val resourceId: Int) { + NoPlugins(R.string.downloader_no_plugins_available) + } + + private companion object { + /** + * Returns a copy with all nonexistent options removed. + */ + private fun Options.filtered(bundles: List<BundleInfo>): Options = buildMap options@{ + bundles.forEach bundles@{ bundle -> + val bundleOptions = this@filtered[bundle.uid] ?: return@bundles + + val patches = bundle.all.associateBy { it.name } + + this@options[bundle.uid] = buildMap bundleOptions@{ + bundleOptions.forEach patch@{ (patchName, values) -> + // Get all valid option keys for the patch. + val validOptionKeys = + patches[patchName]?.options?.map { it.key }?.toSet() ?: return@patch + + this@bundleOptions[patchName] = values.filterKeys { key -> + key in validOptionKeys + } + } + } + } + } + } +} + +private sealed interface SelectionState : Parcelable { + fun patches(bundles: List<BundleInfo>, allowUnsupported: Boolean): PatchSelection + + @Parcelize + data class Customized(val patchSelection: PatchSelection) : SelectionState { + override fun patches(bundles: List<BundleInfo>, allowUnsupported: Boolean) = + bundles.toPatchSelection( + allowUnsupported + ) { uid, patch -> + patchSelection[uid]?.contains(patch.name) ?: false + } + } + + @Parcelize + data object Default : SelectionState { + override fun patches(bundles: List<BundleInfo>, allowUnsupported: Boolean) = + bundles.toPatchSelection(allowUnsupported) { _, patch -> patch.include } + } +} + diff --git a/app/src/main/java/app/revanced/manager/ui/viewmodel/UpdateViewModel.kt b/app/src/main/java/app/revanced/manager/ui/viewmodel/UpdateViewModel.kt new file mode 100644 index 0000000000..644563de81 --- /dev/null +++ b/app/src/main/java/app/revanced/manager/ui/viewmodel/UpdateViewModel.kt @@ -0,0 +1,151 @@ +package app.revanced.manager.ui.viewmodel + +import android.app.Application +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.content.IntentFilter +import android.content.pm.PackageInstaller +import androidx.annotation.StringRes +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue +import androidx.core.content.ContextCompat +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import app.revanced.manager.R +import app.revanced.manager.data.platform.Filesystem +import app.revanced.manager.data.platform.NetworkInfo +import app.revanced.manager.network.api.ReVancedAPI +import app.revanced.manager.network.dto.ReVancedAsset +import app.revanced.manager.network.service.HttpService +import app.revanced.manager.service.InstallService +import app.revanced.manager.util.PM +import app.revanced.manager.util.toast +import app.revanced.manager.util.uiSafe +import io.ktor.client.plugins.onDownload +import io.ktor.client.request.url +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import org.koin.core.component.KoinComponent +import org.koin.core.component.inject + +class UpdateViewModel( + private val downloadOnScreenEntry: Boolean +) : ViewModel(), KoinComponent { + private val app: Application by inject() + private val reVancedAPI: ReVancedAPI by inject() + private val http: HttpService by inject() + private val pm: PM by inject() + private val networkInfo: NetworkInfo by inject() + private val fs: Filesystem by inject() + + var downloadedSize by mutableStateOf(0L) + private set + var totalSize by mutableStateOf(0L) + private set + val downloadProgress by derivedStateOf { + if (downloadedSize == 0L || totalSize == 0L) return@derivedStateOf 0f + + downloadedSize.toFloat() / totalSize.toFloat() + } + var showInternetCheckDialog by mutableStateOf(false) + var state by mutableStateOf(State.CAN_DOWNLOAD) + private set + + var installError by mutableStateOf("") + + var releaseInfo: ReVancedAsset? by mutableStateOf(null) + private set + + private val location = fs.tempDir.resolve("updater.apk") + private val job = viewModelScope.launch { + uiSafe(app, R.string.download_manager_failed, "Failed to download ReVanced Manager") { + releaseInfo = reVancedAPI.getAppUpdate() ?: throw Exception("No update available") + + if (downloadOnScreenEntry) { + downloadUpdate() + } else { + state = State.CAN_DOWNLOAD + } + } + } + + fun downloadUpdate(ignoreInternetCheck: Boolean = false) = viewModelScope.launch { + uiSafe(app, R.string.failed_to_download_update, "Failed to download update") { + val release = releaseInfo!! + withContext(Dispatchers.IO) { + if (!networkInfo.isSafe() && !ignoreInternetCheck) { + showInternetCheckDialog = true + } else { + state = State.DOWNLOADING + + http.download(location) { + url(release.downloadUrl) + onDownload { bytesSentTotal, contentLength -> + downloadedSize = bytesSentTotal + totalSize = contentLength + } + } + state = State.CAN_INSTALL + } + } + } + } + + fun installUpdate() = viewModelScope.launch { + state = State.INSTALLING + + pm.installApp(listOf(location)) + } + + private val installBroadcastReceiver = object : BroadcastReceiver() { + override fun onReceive(context: Context?, intent: Intent?) { + intent?.let { + val pmStatus = intent.getIntExtra(InstallService.EXTRA_INSTALL_STATUS, -999) + val extra = + intent.getStringExtra(InstallService.EXTRA_INSTALL_STATUS_MESSAGE)!! + + when(pmStatus) { + PackageInstaller.STATUS_SUCCESS -> { + app.toast(app.getString(R.string.install_app_success)) + state = State.SUCCESS + } + PackageInstaller.STATUS_FAILURE_ABORTED -> { + state = State.CAN_INSTALL + } + else -> { + app.toast(app.getString(R.string.install_app_fail, extra)) + installError = extra + state = State.FAILED + } + } + } + } + } + + init { + ContextCompat.registerReceiver(app, installBroadcastReceiver, IntentFilter().apply { + addAction(InstallService.APP_INSTALL_ACTION) + }, ContextCompat.RECEIVER_NOT_EXPORTED) + } + + override fun onCleared() { + super.onCleared() + app.unregisterReceiver(installBroadcastReceiver) + + job.cancel() + location.delete() + } + + enum class State(@StringRes val title: Int, val showCancel: Boolean = false) { + CAN_DOWNLOAD(R.string.update_available), + DOWNLOADING(R.string.downloading_manager_update, true), + CAN_INSTALL(R.string.ready_to_install_update, true), + INSTALLING(R.string.installing_manager_update), + FAILED(R.string.install_update_manager_failed), + SUCCESS(R.string.update_completed) + } +} diff --git a/app/src/main/java/app/revanced/manager/ui/viewmodel/UpdatesSettingsViewModel.kt b/app/src/main/java/app/revanced/manager/ui/viewmodel/UpdatesSettingsViewModel.kt new file mode 100644 index 0000000000..385aeddf52 --- /dev/null +++ b/app/src/main/java/app/revanced/manager/ui/viewmodel/UpdatesSettingsViewModel.kt @@ -0,0 +1,31 @@ +package app.revanced.manager.ui.viewmodel + +import android.app.Application +import androidx.lifecycle.ViewModel +import app.revanced.manager.R +import app.revanced.manager.domain.manager.PreferencesManager +import app.revanced.manager.network.api.ReVancedAPI +import app.revanced.manager.util.toast +import app.revanced.manager.util.uiSafe + +class UpdatesSettingsViewModel( + prefs: PreferencesManager, + private val app: Application, + private val reVancedAPI: ReVancedAPI, +) : ViewModel() { + val managerAutoUpdates = prefs.managerAutoUpdates + val showManagerUpdateDialogOnLaunch = prefs.showManagerUpdateDialogOnLaunch + + suspend fun checkForUpdates(): Boolean { + uiSafe(app, R.string.failed_to_check_updates, "Failed to check for updates") { + app.toast(app.getString(R.string.update_check)) + + if (reVancedAPI.getAppUpdate() == null) + app.toast(app.getString(R.string.no_update_available)) + else + return true + } + + return false + } +} \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/util/Constants.kt b/app/src/main/java/app/revanced/manager/util/Constants.kt new file mode 100644 index 0000000000..000da463f6 --- /dev/null +++ b/app/src/main/java/app/revanced/manager/util/Constants.kt @@ -0,0 +1,8 @@ +package app.revanced.manager.util + +const val tag = "ReVanced Manager" + +const val JAR_MIMETYPE = "application/java-archive" +const val APK_MIMETYPE = "application/vnd.android.package-archive" +const val JSON_MIMETYPE = "application/json" +const val BIN_MIMETYPE = "application/octet-stream" \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/util/PM.kt b/app/src/main/java/app/revanced/manager/util/PM.kt new file mode 100644 index 0000000000..b484fc50f4 --- /dev/null +++ b/app/src/main/java/app/revanced/manager/util/PM.kt @@ -0,0 +1,205 @@ +package app.revanced.manager.util + +import android.annotation.SuppressLint +import android.app.Application +import android.app.PendingIntent +import android.content.Context +import android.content.Intent +import android.content.pm.PackageInfo +import android.content.pm.PackageInstaller +import android.content.pm.PackageManager +import android.content.pm.PackageManager.PackageInfoFlags +import android.content.pm.PackageManager.NameNotFoundException +import androidx.core.content.pm.PackageInfoCompat +import android.content.pm.Signature +import android.os.Build +import android.os.Parcelable +import androidx.compose.runtime.Immutable +import app.revanced.manager.domain.repository.PatchBundleRepository +import app.revanced.manager.service.InstallService +import app.revanced.manager.service.UninstallService +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.async +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.withContext +import kotlinx.parcelize.Parcelize +import java.io.File + +private const val byteArraySize = 1024 * 1024 // Because 1,048,576 is not readable + +@Immutable +@Parcelize +data class AppInfo( + val packageName: String, + val patches: Int?, + val packageInfo: PackageInfo? +) : Parcelable + +@SuppressLint("QueryPermissionsNeeded") +class PM( + private val app: Application, + patchBundleRepository: PatchBundleRepository +) { + private val scope = CoroutineScope(Dispatchers.IO) + + val appList = patchBundleRepository.bundles.map { bundles -> + val compatibleApps = scope.async { + val compatiblePackages = bundles.values + .flatMap { it.patches } + .flatMap { it.compatiblePackages.orEmpty() } + .groupingBy { it.packageName } + .eachCount() + + compatiblePackages.keys.map { pkg -> + getPackageInfo(pkg)?.let { packageInfo -> + AppInfo( + pkg, + compatiblePackages[pkg], + packageInfo + ) + } ?: AppInfo( + pkg, + compatiblePackages[pkg], + null + ) + } + } + + val installedApps = scope.async { + getInstalledPackages().map { packageInfo -> + AppInfo( + packageInfo.packageName, + 0, + packageInfo + ) + } + } + + if (compatibleApps.await().isNotEmpty()) { + (compatibleApps.await() + installedApps.await()) + .distinctBy { it.packageName } + .sortedWith( + compareByDescending<AppInfo> { + it.packageInfo != null && (it.patches ?: 0) > 0 + }.thenByDescending { + it.patches + }.thenBy { + it.packageInfo?.label() + }.thenBy { it.packageName } + ) + } else { + emptyList() + } + }.flowOn(Dispatchers.IO) + + private fun getInstalledPackages(flags: Int = 0): List<PackageInfo> = + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) + app.packageManager.getInstalledPackages(PackageInfoFlags.of(flags.toLong())) + else + app.packageManager.getInstalledPackages(flags) + + fun getPackagesWithFeature(feature: String) = + getInstalledPackages(PackageManager.GET_CONFIGURATIONS) + .filter { pkg -> + pkg.reqFeatures?.any { it.name == feature } ?: false + } + + fun getPackageInfo(packageName: String, flags: Int = 0): PackageInfo? = + try { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) + app.packageManager.getPackageInfo(packageName, PackageInfoFlags.of(flags.toLong())) + else + app.packageManager.getPackageInfo(packageName, flags) + } catch (e: NameNotFoundException) { + null + } + + fun getPackageInfo(file: File): PackageInfo? { + val path = file.absolutePath + val pkgInfo = app.packageManager.getPackageArchiveInfo(path, 0) ?: return null + + // This is needed in order to load label and icon. + pkgInfo.applicationInfo!!.apply { + sourceDir = path + publicSourceDir = path + } + + return pkgInfo + } + + fun PackageInfo.label() = this.applicationInfo!!.loadLabel(app.packageManager).toString() + + fun getVersionCode(packageInfo: PackageInfo) = PackageInfoCompat.getLongVersionCode(packageInfo) + + fun getSignature(packageName: String): Signature = + // Get the last signature from the list because we want the newest one if SigningInfo.getSigningCertificateHistory() was used. + PackageInfoCompat.getSignatures(app.packageManager, packageName).last() + + @SuppressLint("InlinedApi") + fun hasSignature(packageName: String, signature: ByteArray) = PackageInfoCompat.hasSignatures( + app.packageManager, + packageName, + mapOf(signature to PackageManager.CERT_INPUT_RAW_X509), + false + ) + + suspend fun installApp(apks: List<File>) = withContext(Dispatchers.IO) { + val packageInstaller = app.packageManager.packageInstaller + packageInstaller.openSession(packageInstaller.createSession(sessionParams)).use { session -> + apks.forEach { apk -> session.writeApk(apk) } + session.commit(app.installIntentSender) + } + } + + fun uninstallPackage(pkg: String) { + val packageInstaller = app.packageManager.packageInstaller + packageInstaller.uninstall(pkg, app.uninstallIntentSender) + } + + fun launch(pkg: String) = app.packageManager.getLaunchIntentForPackage(pkg)?.let { + it.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + app.startActivity(it) + } + + fun canInstallPackages() = app.packageManager.canRequestPackageInstalls() + + private fun PackageInstaller.Session.writeApk(apk: File) { + apk.inputStream().use { inputStream -> + openWrite(apk.name, 0, apk.length()).use { outputStream -> + inputStream.copyTo(outputStream, byteArraySize) + fsync(outputStream) + } + } + } + + private val intentFlags + get() = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) + PendingIntent.FLAG_MUTABLE + else + 0 + + private val sessionParams + get() = PackageInstaller.SessionParams( + PackageInstaller.SessionParams.MODE_FULL_INSTALL + ).apply { + setInstallReason(PackageManager.INSTALL_REASON_USER) + } + + private val Context.installIntentSender + get() = PendingIntent.getService( + this, + 0, + Intent(this, InstallService::class.java), + intentFlags + ).intentSender + + private val Context.uninstallIntentSender + get() = PendingIntent.getService( + this, + 0, + Intent(this, UninstallService::class.java), + intentFlags + ).intentSender +} diff --git a/app/src/main/java/app/revanced/manager/util/RequestInstallAppsContract.kt b/app/src/main/java/app/revanced/manager/util/RequestInstallAppsContract.kt new file mode 100644 index 0000000000..b5060999a1 --- /dev/null +++ b/app/src/main/java/app/revanced/manager/util/RequestInstallAppsContract.kt @@ -0,0 +1,18 @@ +package app.revanced.manager.util + +import android.content.Context +import android.content.Intent +import android.net.Uri +import android.provider.Settings +import androidx.activity.result.contract.ActivityResultContract +import org.koin.core.component.KoinComponent +import org.koin.core.component.inject + +object RequestInstallAppsContract : ActivityResultContract<String, Boolean>(), KoinComponent { + private val pm: PM by inject() + override fun createIntent(context: Context, input: String) = Intent(Settings.ACTION_MANAGE_UNKNOWN_APP_SOURCES, Uri.fromParts("package", input, null)) + + override fun parseResult(resultCode: Int, intent: Intent?): Boolean { + return pm.canInstallPackages() + } +} \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/util/RequestManageStorageContract.kt b/app/src/main/java/app/revanced/manager/util/RequestManageStorageContract.kt new file mode 100644 index 0000000000..67dce3d463 --- /dev/null +++ b/app/src/main/java/app/revanced/manager/util/RequestManageStorageContract.kt @@ -0,0 +1,19 @@ +package app.revanced.manager.util + +import android.content.Context +import android.content.Intent +import android.net.Uri +import android.os.Build +import android.os.Environment +import android.provider.Settings +import androidx.activity.result.contract.ActivityResultContract +import androidx.annotation.RequiresApi + +@RequiresApi(Build.VERSION_CODES.R) +class RequestManageStorageContract(private val forceLaunch: Boolean = false) : ActivityResultContract<String, Boolean>() { + override fun createIntent(context: Context, input: String) = Intent(Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION, Uri.fromParts("package", context.packageName, null)) + + override fun getSynchronousResult(context: Context, input: String): SynchronousResult<Boolean>? = if (!forceLaunch && Environment.isExternalStorageManager()) SynchronousResult(true) else null + + override fun parseResult(resultCode: Int, intent: Intent?) = Environment.isExternalStorageManager() +} \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/util/SnapshotStateSet.kt b/app/src/main/java/app/revanced/manager/util/SnapshotStateSet.kt new file mode 100644 index 0000000000..bada2676b1 --- /dev/null +++ b/app/src/main/java/app/revanced/manager/util/SnapshotStateSet.kt @@ -0,0 +1,60 @@ +package app.revanced.manager.util + +/* + * Copyright 2022 The Android Open Source Project + * + * 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. + */ + +// Source: https://gist.github.com/alexvanyo/a31826820ded6f654fb96291aff6b425 + +import androidx.compose.runtime.Stable +import androidx.compose.runtime.mutableStateMapOf +import androidx.compose.runtime.snapshots.SnapshotStateMap +import androidx.compose.runtime.snapshots.StateObject + +/** + * An implementation of [MutableSet] that can be observed and snapshot. This is the result type + * created by [mutableStateSetOf]. + * + * This class closely implements the same semantics as [HashSet]. + * + * This class is backed by a [SnapshotStateMap]. + * + * @see mutableStateSetOf + */ +@Stable +class SnapshotStateSet<T> private constructor( + private val delegateSnapshotStateMap: SnapshotStateMap<T, Unit>, +) : MutableSet<T> by delegateSnapshotStateMap.keys, StateObject by delegateSnapshotStateMap { + constructor() : this(delegateSnapshotStateMap = mutableStateMapOf()) + + override fun add(element: T): Boolean = + delegateSnapshotStateMap.put(element, Unit) == null + + override fun addAll(elements: Collection<T>): Boolean = + elements.map(::add).any() + + override fun remove(element: T) = delegateSnapshotStateMap.remove(element) != null +} + +/** + * Create a instance of [MutableSet]<T> that is observable and can be snapshot. + */ +fun <T> mutableStateSetOf() = SnapshotStateSet<T>() + +/** + * Create an instance of [MutableSet]<T> from a collection that is observable and can be + * snapshot. + */ +fun <T> Collection<T>.toMutableStateSet() = SnapshotStateSet<T>().also { it.addAll(this) } \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/util/Util.kt b/app/src/main/java/app/revanced/manager/util/Util.kt new file mode 100644 index 0000000000..09c220228a --- /dev/null +++ b/app/src/main/java/app/revanced/manager/util/Util.kt @@ -0,0 +1,287 @@ +package app.revanced.manager.util + +import android.content.Context +import android.content.Intent +import android.content.pm.ApplicationInfo +import android.util.Log +import android.widget.Toast +import androidx.annotation.MainThread +import androidx.annotation.StringRes +import androidx.compose.foundation.ScrollState +import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.material3.ListItemColors +import androidx.compose.material3.ListItemDefaults +import androidx.compose.runtime.Composable +import androidx.compose.runtime.ReadOnlyComposable +import androidx.compose.runtime.LaunchedEffect +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.rememberUpdatedState +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.alpha +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalView +import androidx.core.net.toUri +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.LifecycleOwner +import androidx.lifecycle.compose.LocalLifecycleOwner +import androidx.lifecycle.SavedStateHandle +import androidx.lifecycle.lifecycleScope +import androidx.lifecycle.repeatOnLifecycle +import app.revanced.manager.R +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.DelicateCoroutinesApi +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.launch +import kotlinx.datetime.Clock +import kotlinx.datetime.LocalDateTime +import kotlinx.datetime.TimeZone +import kotlinx.datetime.format.MonthNames +import kotlinx.datetime.format.char +import kotlinx.datetime.toInstant +import kotlinx.datetime.toLocalDateTime +import java.util.Locale +import kotlin.properties.PropertyDelegateProvider +import kotlin.properties.ReadWriteProperty +import kotlin.reflect.KProperty + +typealias PatchSelection = Map<Int, Set<String>> +typealias Options = Map<Int, Map<String, Map<String, Any?>>> + +val Context.isDebuggable get() = 0 != applicationInfo.flags and ApplicationInfo.FLAG_DEBUGGABLE + +fun Context.openUrl(url: String) { + startActivity(Intent(Intent.ACTION_VIEW, url.toUri()).apply { + flags = Intent.FLAG_ACTIVITY_NEW_TASK + }) +} + +fun Context.toast(string: String, duration: Int = Toast.LENGTH_SHORT) { + Toast.makeText(this, string, duration).show() +} + +/** + * Safely perform an operation that may fail to avoid crashing the app. + * If [block] fails, the error will be logged and a toast will be shown to the user to inform them that the action failed. + * + * @param context The android [Context]. + * @param toastMsg The toast message to show if [block] throws. + * @param logMsg The log message. + * @param block The code to execute. + */ +@OptIn(DelicateCoroutinesApi::class) +inline fun uiSafe(context: Context, @StringRes toastMsg: Int, logMsg: String, block: () -> Unit) { + try { + block() + } catch (error: Exception) { + // You can only toast on the main thread. + GlobalScope.launch(Dispatchers.Main) { + context.toast( + context.getString( + toastMsg, + error.simpleMessage() + ) + ) + } + + Log.e(tag, logMsg, error) + } +} + +fun Throwable.simpleMessage() = this.message ?: this.cause?.message ?: this::class.simpleName + +inline fun LifecycleOwner.launchAndRepeatWithViewLifecycle( + minActiveState: Lifecycle.State = Lifecycle.State.STARTED, + crossinline block: suspend CoroutineScope.() -> Unit +) { + lifecycleScope.launch { + lifecycle.repeatOnLifecycle(minActiveState) { + block() + } + } +} + +/** + * Run [transformer] on the [Iterable] and then [combine] the result using [combiner]. + * This is used to transform collections that contain [Flow]s into something that is easier to work with. + */ +@OptIn(ExperimentalCoroutinesApi::class) +inline fun <T, reified R, C> Flow<Iterable<T>>.flatMapLatestAndCombine( + crossinline combiner: (Array<R>) -> C, + crossinline transformer: (T) -> Flow<R>, +): Flow<C> = flatMapLatest { iterable -> + combine(iterable.map(transformer)) { + combiner(it) + } +} + +val Color.hexCode: String + inline get() { + val a: Int = (alpha * 255).toInt() + val r: Int = (red * 255).toInt() + val g: Int = (green * 255).toInt() + val b: Int = (blue * 255).toInt() + return java.lang.String.format(Locale.getDefault(), "%02X%02X%02X%02X", r, g, b, a) + } + +suspend fun <T> Flow<Iterable<T>>.collectEach(block: suspend (T) -> Unit) { + this.collect { iterable -> + iterable.forEach { + block(it) + } + } +} + +fun LocalDateTime.relativeTime(context: Context): String { + try { + val now = Clock.System.now() + val duration = now - this.toInstant(TimeZone.UTC) + + return when { + duration.inWholeMinutes < 1 -> context.getString(R.string.just_now) + duration.inWholeMinutes < 60 -> context.getString( + R.string.minutes_ago, + duration.inWholeMinutes.toString() + ) + + duration.inWholeHours < 24 -> context.getString( + R.string.hours_ago, + duration.inWholeHours.toString() + ) + + duration.inWholeHours < 30 -> context.getString( + R.string.days_ago, + duration.inWholeDays.toString() + ) + + else -> LocalDateTime.Format { + monthName(MonthNames.ENGLISH_ABBREVIATED) + char(' ') + dayOfMonth() + if (now.toLocalDateTime(TimeZone.UTC).year != this@relativeTime.year) { + chars(", ") + year() + } + }.format(this) + } + } catch (e: IllegalArgumentException) { + return context.getString(R.string.invalid_date) + } +} + +private var transparentListItemColorsCached: ListItemColors? = null + +/** + * The default ListItem colors, but with [ListItemColors.containerColor] set to [Color.Transparent]. + */ +val transparentListItemColors + @Composable get() = transparentListItemColorsCached + ?: ListItemDefaults.colors(containerColor = Color.Transparent) + .also { transparentListItemColorsCached = it } + +@Composable +fun <T> EventEffect(flow: Flow<T>, vararg keys: Any?, state: Lifecycle.State = Lifecycle.State.STARTED, block: suspend (T) -> Unit) { + val lifecycleOwner = LocalLifecycleOwner.current + val currentBlock by rememberUpdatedState(block) + + LaunchedEffect(flow, state, *keys) { + lifecycleOwner.repeatOnLifecycle(state) { + flow.collect { + currentBlock(it) + } + } + } +} + +const val isScrollingUpSensitivity = 10 + +@Composable +fun LazyListState.isScrollingUp(): State<Boolean> { + return remember(this) { + var previousIndex by mutableIntStateOf(firstVisibleItemIndex) + var previousScrollOffset by mutableIntStateOf(firstVisibleItemScrollOffset) + + derivedStateOf { + val indexChanged = previousIndex != firstVisibleItemIndex + val offsetChanged = + kotlin.math.abs(previousScrollOffset - firstVisibleItemScrollOffset) > isScrollingUpSensitivity + + if (indexChanged) { + previousIndex > firstVisibleItemIndex + } else if (offsetChanged) { + previousScrollOffset > firstVisibleItemScrollOffset + } else { + true + }.also { + previousIndex = firstVisibleItemIndex + previousScrollOffset = firstVisibleItemScrollOffset + } + } + } +} + +// TODO: support sensitivity +@Composable +fun ScrollState.isScrollingUp(): State<Boolean> { + return remember(this) { + var previousScrollOffset by mutableIntStateOf(value) + derivedStateOf { + (previousScrollOffset >= value).also { + previousScrollOffset = value + } + } + } +} + +val LazyListState.isScrollingUp: Boolean @Composable get() = this.isScrollingUp().value +val ScrollState.isScrollingUp: Boolean @Composable get() = this.isScrollingUp().value + +@Composable +@ReadOnlyComposable +fun <R> (() -> R).withHapticFeedback(constant: Int): () -> R { + val view = LocalView.current + return { + view.performHapticFeedback(constant) + this() + } +} + +@Composable +@ReadOnlyComposable +fun <T, R> ((T) -> R).withHapticFeedback(constant: Int): (T) -> R { + val view = LocalView.current + return { + view.performHapticFeedback(constant) + this(it) + } +} + +fun Modifier.enabled(condition: Boolean) = if (condition) this else alpha(0.5f) + +@MainThread +fun <T : Any> SavedStateHandle.saveableVar(init: () -> T): PropertyDelegateProvider<Any?, ReadWriteProperty<Any?, T>> = + PropertyDelegateProvider { _: Any?, property -> + val name = property.name + if (name !in this) this[name] = init() + object : ReadWriteProperty<Any?, T> { + override fun getValue(thisRef: Any?, property: KProperty<*>): T = get(name)!! + override fun setValue(thisRef: Any?, property: KProperty<*>, value: T) = + set(name, value) + } + } + +fun <T : Any> SavedStateHandle.saveableVar(): ReadWriteProperty<Any?, T?> = + object : ReadWriteProperty<Any?, T?> { + override fun getValue(thisRef: Any?, property: KProperty<*>): T? = get(property.name) + override fun setValue(thisRef: Any?, property: KProperty<*>, value: T?) = + set(property.name, value) + } \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/util/saver/NullableSaver.kt b/app/src/main/java/app/revanced/manager/util/saver/NullableSaver.kt new file mode 100644 index 0000000000..a94a53886c --- /dev/null +++ b/app/src/main/java/app/revanced/manager/util/saver/NullableSaver.kt @@ -0,0 +1,24 @@ +package app.revanced.manager.util.saver + +import android.os.Parcelable +import androidx.compose.runtime.saveable.Saver +import kotlinx.parcelize.Parcelize +import kotlinx.parcelize.RawValue + +@Parcelize +class Nullable<T>(val inner: @RawValue T?) : Parcelable + +/** + * Creates a saver that can save nullable versions of types that have custom savers. + */ +fun <Original : Any, Saveable : Any> nullableSaver(baseSaver: Saver<Original, Saveable>): Saver<Original?, Nullable<Saveable>> = + Saver( + save = { value -> + with(baseSaver) { + save(value ?: return@Saver Nullable(null)) + }?.let(::Nullable) + }, + restore = { + it.inner?.let(baseSaver::restore) + } + ) \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/util/saver/PathSaver.kt b/app/src/main/java/app/revanced/manager/util/saver/PathSaver.kt new file mode 100644 index 0000000000..d16eeb9234 --- /dev/null +++ b/app/src/main/java/app/revanced/manager/util/saver/PathSaver.kt @@ -0,0 +1,13 @@ +package app.revanced.manager.util.saver + +import androidx.compose.runtime.saveable.Saver +import java.nio.file.Path +import kotlin.io.path.Path + +/** + * A [Saver] that can save [Path]s. Only works with the default filesystem. + */ +val PathSaver = Saver<Path, String>( + save = { it.toString() }, + restore = { Path(it) } +) \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/util/saver/PersistentCollectionSavers.kt b/app/src/main/java/app/revanced/manager/util/saver/PersistentCollectionSavers.kt new file mode 100644 index 0000000000..2a1418cdf6 --- /dev/null +++ b/app/src/main/java/app/revanced/manager/util/saver/PersistentCollectionSavers.kt @@ -0,0 +1,69 @@ +package app.revanced.manager.util.saver + +import androidx.compose.runtime.saveable.Saver +import kotlinx.collections.immutable.* + +/** + * Create a [Saver] for [PersistentList]s. + */ +fun <T> persistentListSaver() = Saver<PersistentList<T>, List<T>>( + save = { + it.toList() + }, + restore = { + it.toPersistentList() + } +) + +/** + * Create a [Saver] for [PersistentSet]s. + */ +fun <T> persistentSetSaver() = Saver<PersistentSet<T>, Set<T>>( + save = { + it.toSet() + }, + restore = { + it.toPersistentSet() + } +) + +/** + * Create a [Saver] for [PersistentMap]s. + */ +fun <K, V> persistentMapSaver() = Saver<PersistentMap<K, V>, Map<K, V>>( + save = { + it.toMap() + }, + restore = { + it.toPersistentMap() + } +) + +/** + * Create a saver for [PersistentMap]s with a custom [Saver] used for the values. + * Null values will not be saved by this [Saver]. + * + * @param valueSaver The [Saver] used for the values of the [Map]. + */ +fun <K, Original, Saveable : Any> persistentMapSaver( + valueSaver: Saver<Original, Saveable> +) = Saver<PersistentMap<K, Original>, Map<K, Saveable>>( + save = { + buildMap { + it.forEach { (key, value) -> + with(valueSaver) { + save(value)?.let { + this@buildMap[key] = it + } + } + } + } + }, + restore = { + buildMap { + it.forEach { (key, value) -> + this[key] = valueSaver.restore(value) ?: return@forEach + } + }.toPersistentMap() + } +) \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/util/saver/SnapshotStateCollectionSavers.kt b/app/src/main/java/app/revanced/manager/util/saver/SnapshotStateCollectionSavers.kt new file mode 100644 index 0000000000..985cbeb6ab --- /dev/null +++ b/app/src/main/java/app/revanced/manager/util/saver/SnapshotStateCollectionSavers.kt @@ -0,0 +1,75 @@ +package app.revanced.manager.util.saver + +import androidx.compose.runtime.mutableStateMapOf +import androidx.compose.runtime.saveable.Saver +import androidx.compose.runtime.snapshots.SnapshotStateList +import androidx.compose.runtime.snapshots.SnapshotStateMap +import androidx.compose.runtime.toMutableStateList +import androidx.compose.runtime.toMutableStateMap +import app.revanced.manager.util.SnapshotStateSet +import app.revanced.manager.util.toMutableStateSet + +/** + * Create a [Saver] for [SnapshotStateList]s. + */ +fun <T> snapshotStateListSaver() = Saver<SnapshotStateList<T>, List<T>>( + save = { + it.toMutableList() + }, + restore = { + it.toMutableStateList() + } +) + +/** + * Create a [Saver] for [SnapshotStateSet]s. + */ +fun <T> snapshotStateSetSaver() = Saver<SnapshotStateSet<T>, Set<T>>( + save = { + it.toMutableSet() + }, + restore = { + it.toMutableStateSet() + } +) + +/** + * Create a [Saver] for [SnapshotStateMap]s. + */ +fun <K, V> snapshotStateMapSaver() = Saver<SnapshotStateMap<K, V>, Map<K, V>>( + save = { + it.toMutableMap() + }, + restore = { + mutableStateMapOf<K, V>().apply { + this.putAll(it) + } + } +) + +/** + * Create a saver for [SnapshotStateMap]s with a custom [Saver] used for the values. + * Null values will not be saved by this [Saver]. + * + * @param valueSaver The [Saver] used for the values of the [Map]. + */ +fun <K, Original, Saveable : Any> snapshotStateMapSaver( + valueSaver: Saver<Original, Saveable> +) = Saver<SnapshotStateMap<K, Original>, Map<K, Saveable>>( + save = { + buildMap { + it.forEach { (key, value) -> + with(valueSaver) { + save(value)?.let { + this@buildMap[key] = it + } + } + } + } + }, + restore = { + it.mapNotNull { (key, value) -> + valueSaver.restore(value)?.let { restored -> key to restored } + }.toMutableStateMap() + } +) \ No newline at end of file diff --git a/android/app/src/main/jniLibs/arm64-v8a/libaapt2.so b/app/src/main/jniLibs/arm64-v8a/libaapt2.so similarity index 100% rename from android/app/src/main/jniLibs/arm64-v8a/libaapt2.so rename to app/src/main/jniLibs/arm64-v8a/libaapt2.so diff --git a/android/app/src/main/jniLibs/armeabi-v7a/libaapt2.so b/app/src/main/jniLibs/armeabi-v7a/libaapt2.so similarity index 98% rename from android/app/src/main/jniLibs/armeabi-v7a/libaapt2.so rename to app/src/main/jniLibs/armeabi-v7a/libaapt2.so index 8506316d8b..46bdce5ce7 100644 Binary files a/android/app/src/main/jniLibs/armeabi-v7a/libaapt2.so and b/app/src/main/jniLibs/armeabi-v7a/libaapt2.so differ diff --git a/android/app/src/main/jniLibs/x86/libaapt2.so b/app/src/main/jniLibs/x86/libaapt2.so similarity index 100% rename from android/app/src/main/jniLibs/x86/libaapt2.so rename to app/src/main/jniLibs/x86/libaapt2.so diff --git a/android/app/src/main/jniLibs/x86_64/libaapt2.so b/app/src/main/jniLibs/x86_64/libaapt2.so similarity index 100% rename from android/app/src/main/jniLibs/x86_64/libaapt2.so rename to app/src/main/jniLibs/x86_64/libaapt2.so diff --git a/android/app/src/main/res/drawable/ic_launcher_foreground.xml b/app/src/main/res/drawable/ic_launcher_foreground.xml similarity index 100% rename from android/app/src/main/res/drawable/ic_launcher_foreground.xml rename to app/src/main/res/drawable/ic_launcher_foreground.xml diff --git a/android/app/src/main/res/drawable/ic_launcher_round.png b/app/src/main/res/drawable/ic_launcher_round.png similarity index 100% rename from android/app/src/main/res/drawable/ic_launcher_round.png rename to app/src/main/res/drawable/ic_launcher_round.png diff --git a/app/src/main/res/drawable/ic_logo_ring.xml b/app/src/main/res/drawable/ic_logo_ring.xml new file mode 100644 index 0000000000..1640a24c9f --- /dev/null +++ b/app/src/main/res/drawable/ic_logo_ring.xml @@ -0,0 +1,30 @@ +<vector android:height="72dp" android:viewportHeight="800" + android:viewportWidth="800" android:width="72dp" + xmlns:aapt="http://schemas.android.com/aapt" xmlns:android="http://schemas.android.com/apk/res/android"> + <path android:fillColor="#1b1b1b" android:fillType="evenOdd" + android:pathData="M400,400m-400,0a400,400 0,1 1,800 0a400,400 0,1 1,-800 0" android:strokeLineJoin="round"/> + <path android:fillType="evenOdd" + android:pathData="m400,0c220.77,0 400,179.23 400,400s-179.23,400 -400,400 -400,-179.23 -400,-400 179.23,-400 400,-400zM400,36c200.9,-0 364,163.1 364,364s-163.1,364 -364,364 -364,-163.1 -364,-364 163.1,-364 364,-364z" android:strokeLineJoin="round"> + <aapt:attr name="android:fillColor"> + <gradient android:endX="400" android:endY="800" + android:startX="400" android:startY="0" android:type="linear"> + <item android:color="#FFF04E98" android:offset="0"/> + <item android:color="#FF5F65D4" android:offset="0.5"/> + <item android:color="#FF4E98F0" android:offset="1"/> + </gradient> + </aapt:attr> + </path> + <path android:fillColor="#fff" android:fillType="evenOdd" + android:pathData="m538.74,269.87c1.48,-3.38 1.16,-7.28 -0.86,-10.37 -2.02,-3.09 -5.46,-4.95 -9.16,-4.95h-14.16c-3.1,0 -5.91,1.83 -7.15,4.67 -12.47,28.4 -78.27,178.27 -100.25,228.33 -1.25,2.84 -4.05,4.67 -7.15,4.67 -3.1,0 -5.91,-1.83 -7.15,-4.67 -21.98,-50.06 -87.78,-199.93 -100.25,-228.33 -1.25,-2.84 -4.05,-4.67 -7.15,-4.67h-14.16c-3.69,0 -7.14,1.86 -9.16,4.95 -2.02,3.09 -2.34,6.99 -0.86,10.37 23.56,53.77 101.87,232.52 117.87,269.03 1.74,3.98 5.67,6.55 10.02,6.55h21.7c4.34,-0 8.27,-2.57 10.02,-6.55 16,-36.51 94.32,-215.27 117.87,-269.03z" android:strokeLineJoin="round"/> + <path android:fillType="evenOdd" + android:pathData="m408.12,395.31c-1.67,2.9 -4.77,4.69 -8.12,4.69s-6.44,-1.79 -8.12,-4.69c-17,-29.44 -56.16,-97.26 -73.15,-126.7 -1.67,-2.9 -1.67,-6.47 0,-9.38s4.77,-4.69 8.12,-4.69h146.31c3.35,0 6.44,1.79 8.12,4.69s1.67,6.47 -0,9.38c-17,29.44 -56.16,97.26 -73.15,126.7z" android:strokeLineJoin="round"> + <aapt:attr name="android:fillColor"> + <gradient android:endX="400" android:endY="543.86" + android:startX="400" android:startY="254.54" android:type="linear"> + <item android:color="#FFF04E98" android:offset="0"/> + <item android:color="#FF5F65D4" android:offset="0.5"/> + <item android:color="#FF4E98F0" android:offset="1"/> + </gradient> + </aapt:attr> + </path> +</vector> diff --git a/android/app/src/main/res/drawable/ic_notification.png b/app/src/main/res/drawable/ic_notification.png similarity index 100% rename from android/app/src/main/res/drawable/ic_notification.png rename to app/src/main/res/drawable/ic_notification.png diff --git a/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/app/src/main/res/mipmap-anydpi/ic_launcher.xml similarity index 100% rename from android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml rename to app/src/main/res/mipmap-anydpi/ic_launcher.xml diff --git a/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/app/src/main/res/mipmap-hdpi/ic_launcher.png similarity index 100% rename from android/app/src/main/res/mipmap-hdpi/ic_launcher.png rename to app/src/main/res/mipmap-hdpi/ic_launcher.png diff --git a/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/app/src/main/res/mipmap-mdpi/ic_launcher.png similarity index 100% rename from android/app/src/main/res/mipmap-mdpi/ic_launcher.png rename to app/src/main/res/mipmap-mdpi/ic_launcher.png diff --git a/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/app/src/main/res/mipmap-xhdpi/ic_launcher.png similarity index 100% rename from android/app/src/main/res/mipmap-xhdpi/ic_launcher.png rename to app/src/main/res/mipmap-xhdpi/ic_launcher.png diff --git a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/app/src/main/res/mipmap-xxhdpi/ic_launcher.png similarity index 100% rename from android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png rename to app/src/main/res/mipmap-xxhdpi/ic_launcher.png diff --git a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png similarity index 100% rename from android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png rename to app/src/main/res/mipmap-xxxhdpi/ic_launcher.png diff --git a/app/src/main/res/resources.properties b/app/src/main/res/resources.properties new file mode 100644 index 0000000000..467b3efec9 --- /dev/null +++ b/app/src/main/res/resources.properties @@ -0,0 +1 @@ +unqualifiedResLocale=en-US diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml new file mode 100644 index 0000000000..6be58a6139 --- /dev/null +++ b/app/src/main/res/values/colors.xml @@ -0,0 +1,12 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <color name="purple_200">#FFBB86FC</color> + <color name="purple_500">#FF6200EE</color> + <color name="purple_700">#FF3700B3</color> + <color name="teal_200">#FF03DAC5</color> + <color name="teal_700">#FF018786</color> + <color name="black">#FF000000</color> + <color name="white">#FFFFFFFF</color> + + <color name="ic_launcher_background">#1B1B1B</color> +</resources> \ No newline at end of file diff --git a/app/src/main/res/values/plurals.xml b/app/src/main/res/values/plurals.xml new file mode 100644 index 0000000000..d017807383 --- /dev/null +++ b/app/src/main/res/values/plurals.xml @@ -0,0 +1,14 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <plurals name="patch_count"> + <item quantity="one">%d patch</item> + <item quantity="other">%d patches</item> + </plurals> + <plurals name="patches_executed"> + <item quantity="one">Executed %d patch</item> + <item quantity="other">Executed %d patches</item> + </plurals> + <plurals name="selected_count"> + <item quantity="other">%d selected</item> + </plurals> +</resources> \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml new file mode 100644 index 0000000000..fab90c15c5 --- /dev/null +++ b/app/src/main/res/values/strings.xml @@ -0,0 +1,424 @@ +<resources> + <string name="app_name">ReVanced Manager</string> + <string name="patcher">Patcher</string> + <string name="patches">Patches</string> + <string name="cli">CLI</string> + <string name="manager">Manager</string> + + <string name="plugin_host_permission_label">ReVanced Manager plugin host</string> + <string name="plugin_host_permission_description">Used to control access to ReVanced Manager plugins. Only ReVanced Manager has this.</string> + + <string name="toast_copied_to_clipboard">Copied!</string> + <string name="copy_to_clipboard">Copy to clipboard</string> + + <string name="dashboard">Dashboard</string> + <string name="settings">Settings</string> + <string name="select_app">Select an app</string> + <string name="patches_selected">%1$d/%2$d selected</string> + + <string name="new_downloader_plugins_notification">New downloader plugins available. Click here to configure them.</string> + <string name="unsupported_architecture_warning">Patching on this device architecture is unsupported and will most likely fail.</string> + + <string name="import_">Import</string> + <string name="import_bundle">Import patch bundle</string> + <string name="bundle_patches">Bundle patches</string> + <string name="patch_bundle_field">Patch bundle</string> + <string name="file_field_set">Selected</string> + <string name="file_field_not_set">Not selected</string> + + <string name="field_not_set">Not set</string> + + <string name="bundle_missing">Missing</string> + <string name="bundle_error">Error</string> + <string name="bundle_error_description">Bundle could not be loaded. Click to view the error</string> + <string name="bundle_not_downloaded">Bundle has not been downloaded. Click here to download it</string> + <string name="bundle_name_default">Default</string> + <string name="bundle_name_fallback">Unnamed</string> + + <string name="android_11_bug_dialog_title">Android 11 bug</string> + <string name="android_11_bug_dialog_description">The app installation permission must be granted ahead of time to avoid a bug in the Android 11 system that will negatively affect the user experience.</string> + + <string name="selected_app_meta_any_version">Any available version</string> + <string name="app_source_dialog_title">Select source</string> + <string name="app_source_dialog_option_auto">Auto</string> + <string name="app_source_dialog_option_auto_description">Use all installed downloaders to find a suitable APK file</string> + <string name="app_source_dialog_option_auto_unavailable">No plugins available</string> + <string name="app_source_dialog_option_installed_no_root">Mounted apps cannot be patched again without root access</string> + <string name="app_source_dialog_option_installed_version_not_suggested">Version %s does not match the suggested version</string> + + <string name="patch_item_description">Start patching the application</string> + <string name="patch_selector_item">Patch selection and options</string> + <string name="patch_selector_item_description">%d patches selected</string> + <string name="no_patches_selected">No patches selected</string> + + <string name="apk_source_selector_item">Change source</string> + <string name="apk_source_auto">Current: All downloaders</string> + <string name="apk_source_downloader">Current: %s</string> + <string name="apk_source_installed">Current: Installed</string> + <string name="apk_source_local">Current: File</string> + + <string name="legacy_import_failed">Could not import legacy settings</string> + + <string name="auto_updates_dialog_title">Configure updates</string> + <string name="auto_updates_dialog_description">Do you want ReVanced Manager to periodically check for updates for the following components?</string> + <string name="auto_updates_dialog_manager">ReVanced Manager</string> + <string name="auto_updates_dialog_patches">ReVanced Patches</string> + <string name="auto_updates_dialog_note">These settings can be changed later.</string> + + <string name="general">General</string> + <string name="general_description">Theme, dynamic color</string> + <string name="updates">Updates</string> + <string name="updates_description">Check for updates and view changelogs</string> + <string name="downloads">Downloads</string> + <string name="downloads_description">Downloader plugins and downloaded apps</string> + <string name="import_export">Import & export</string> + <string name="import_export_description">Keystore, patch options and selection</string> + <string name="advanced">Advanced</string> + <string name="advanced_description">API URL, memory limit, debugging</string> + <string name="about">About</string> + <string name="opensource_licenses">Open source licenses</string> + <string name="opensource_licenses_description">View all the libraries used to make this application</string> + + <string name="contributors">Contributors</string> + <string name="contributors_description">View the contributors of ReVanced</string> + <string name="dynamic_color">Dynamic color</string> + <string name="dynamic_color_description">Adapt colors to the wallpaper</string> + <string name="theme">Theme</string> + <string name="theme_description">Choose between light or dark theme</string> + <string name="safeguards">Safeguards</string> + <string name="patch_compat_check">Disable version compatibility check</string> + <string name="patch_compat_check_description">The check restricts patches to supported app versions</string> + <string name="patch_compat_check_confirmation">Selecting incompatible patches can result in a broken app.\n\nDo you want to proceed anyways?</string> + <string name="suggested_version_safeguard">Require suggested app version</string> + <string name="suggested_version_safeguard_description">Enforce selection of the suggested app version</string> + <string name="suggested_version_safeguard_confirmation">Selecting an app that is not the suggested version may cause unexpected issues.\n\nDo you want to proceed anyways?</string> + <string name="patch_selection_safeguard">Allow changing patch selection</string> + <string name="patch_selection_safeguard_description">Do not prevent selecting or deselecting patches</string> + <string name="patch_selection_safeguard_confirmation">Changing the selection of patches may cause unexpected issues.\n\nEnable anyways?</string> + <string name="universal_patches_safeguard">Disable universal patch warning</string> + <string name="universal_patches_safeguard_description">Disables the warning that appears when you try to select universal patches</string> + <string name="universal_patches_safeguard_confirmation">Universal patches are not as well tested as those that target specific apps.\n\nEnable anyways?</string> + <string name="import_keystore">Import keystore</string> + <string name="import_keystore_description">Import a custom keystore</string> + <string name="import_keystore_dialog_title">Enter keystore credentials</string> + <string name="import_keystore_dialog_description">You\'ll need enter the keystore’s credentials to import it.</string> + <string name="import_keystore_dialog_alias_field">Username (Alias)</string> + <string name="import_keystore_dialog_password_field">Password</string> + <string name="import_keystore_dialog_button">Import</string> + <string name="import_keystore_wrong_credentials">Wrong keystore credentials</string> + <string name="import_keystore_success">Imported keystore</string> + <string name="export_keystore">Export keystore</string> + <string name="export_keystore_description">Export the current keystore</string> + <string name="export_keystore_unavailable">No keystore to export</string> + <string name="export_keystore_success">Exported keystore</string> + <string name="regenerate_keystore">Regenerate keystore</string> + <string name="regenerate_keystore_description">Generate a new keystore</string> + <string name="regenerate_keystore_success">The keystore has been successfully replaced</string> + <string name="import_patch_selection">Import patch selection</string> + <string name="import_patch_selection_description">Import patch selection from a JSON file</string> + <string name="import_patch_selection_fail">Could not import patch selection: %s</string> + <string name="import_patch_selection_success">Imported patch selection</string> + <string name="export_patch_selection">Export patch selection</string> + <string name="export_patch_selection_description">Export patch selection to a JSON file</string> + <string name="export_patch_selection_fail">Could not export patch selection: %s</string> + <string name="export_patch_selection_success">Exported patch selection</string> + <string name="reset_patch_selection">Reset patch selection</string> + <string name="reset_patch_selection_description">Reset the stored patch selection</string> + <string name="reset_patch_selection_success">Patch selection has been reset</string> + <string name="patch_options_reset_package">Reset patch options for app</string> + <string name="patch_options_reset_package_description">Resets patch options for a single app</string> + <string name="patch_options_reset_bundle">Resets patch options for bundle</string> + <string name="patch_options_reset_bundle_description">Resets patch options for all patches in a bundle</string> + <string name="patch_options_reset_all">Reset patch options</string> + <string name="patch_options_reset_all_description">Resets all patch options</string> + <string name="downloader_plugins">Plugins</string> + <string name="downloader_plugin_state_trusted">Trusted</string> + <string name="downloader_plugin_state_failed">Failed to load. Click for more details</string> + <string name="downloader_plugin_state_untrusted">Untrusted</string> + <string name="downloader_plugin_trust_dialog_title">Trust plugin?</string> + <string name="downloader_plugin_revoke_trust_dialog_title">Revoke trust?</string> + <string name="downloader_plugin_trust_dialog_body">Package name: %1$s\nSignature (SHA-256): %2$s</string> + <string name="downloader_settings_no_apps">No downloaded apps found</string> + + <string name="search_apps">Search apps…</string> + <string name="loading_body">Loading…</string> + <string name="downloading_patches">Downloading patch bundle…</string> + + <string name="options">Options</string> + <string name="ok">OK</string> + <string name="yes">Yes</string> + <string name="no">No</string> + <string name="edit">Edit</string> + <string name="dialog_input_placeholder">Value</string> + <string name="reset">Reset</string> + <string name="share">Share</string> + <string name="patch">Patch</string> + <string name="select_from_storage">Select from storage</string> + <string name="select_from_storage_description">Select an APK file from storage using file picker</string> + <string name="suggested_version_info">Suggested version: %s</string> + <string name="type_anything">Type anything to continue</string> + <string name="search">Search patches…</string> + <string name="apply">Apply</string> + <string name="help">Help</string> + <string name="back">Back</string> + <string name="warning">Warning</string> + <string name="add">Add</string> + <string name="close">Close</string> + <string name="clear">Clear</string> + <string name="system">System</string> + <string name="light">Light</string> + <string name="dark">Dark</string> + <string name="appearance">Appearance</string> + <string name="downloaded_apps">Downloaded apps</string> + <string name="process_runtime">Run Patcher in another process (experimental)</string> + <string name="process_runtime_description">This is faster and allows Patcher to use more memory.</string> + <string name="process_runtime_memory_limit">Patcher process memory limit</string> + <string name="process_runtime_memory_limit_description">The max amount of memory that the Patcher process can use (in megabytes)</string> + <string name="debug_logs_export">Export debug logs</string> + <string name="debug_logs_export_read_failed">Failed to read logs (exit code %d)</string> + <string name="debug_logs_export_failed">Failed to export logs</string> + <string name="debug_logs_export_success">Exported logs</string> + <string name="api_url">API URL</string> + <string name="api_url_description">The API used to download necessary files.</string> + <string name="api_url_dialog_title">Set custom API URL</string> + <string name="api_url_dialog_description">Set the API URL of ReVanced Manager. ReVanced Manager uses the API to download patches and updates.</string> + <string name="api_url_dialog_warning">ReVanced Manager connects to the API to download patches and updates. Make sure that you trust it.</string> + <string name="api_url_dialog_save">Set</string> + <string name="api_url_dialog_reset">Reset API URL</string> + <string name="device">Device</string> + <string name="device_android_version">Android version</string> + <string name="device_model">Model</string> + <string name="device_architectures">CPU Architectures</string> + <string name="device_memory_limit">Memory limits</string> + <string name="device_memory_limit_format">%1$dMB (Normal) - %2$dMB (Large)</string> + <string name="patch_bundles_section">Patch bundles</string> + <string name="patch_bundles_force_download">Force download all patch bundles</string> + <string name="patch_bundles_reset">Reset patch bundles</string> + <string name="patching">Patching</string> + <string name="signing">Signing</string> + <string name="storage">Storage</string> + <string name="patches_unavailable">No patches are available. Check your bundles</string> + <string name="tab_apps">Apps</string> + <string name="tab_bundles">Patch bundles</string> + <string name="delete">Delete</string> + <string name="refresh">Refresh</string> + <string name="continue_anyways">Continue anyways</string> + <string name="download_another_version">Download another version</string> + <string name="download_app">Download app</string> + <string name="download_apk">Download APK file</string> + <string name="source_download_fail">Failed to download patch bundle: %s</string> + <string name="source_replace_fail">Failed to load updated patch bundle: %s</string> + <string name="no_patched_apps_found">No patched apps found</string> + <string name="tap_on_patches">Tap on the patches to get more information about them</string> + <string name="bundles_selected">%s selected</string> + <string name="unsupported_patches">Incompatible patches</string> + <string name="universal_patches">Universal patches</string> + <string name="patch_selection_reset_toast">Patch selection and options has been reset to recommended defaults</string> + <string name="patch_options_reset_toast">Patch options have been reset</string> + <string name="non_suggested_version_warning_title">Non suggested version</string> + <string name="non_suggested_version_warning_description">The version of the app you have selected does not match the suggested version.\nPlease use the suggested version: %s\n\nTo continue anyway, disable \"Require suggested app version\" in the advanced settings.</string> + <string name="selection_warning_title">Stop using defaults?</string> + <string name="selection_warning_description">It is recommended to use the default patch selection and options. Changing them may result in unexpected issues.\n\nYou need to turn on \"Allow changing patch selection\" in the advanced settings before toggling patches.</string> + <string name="universal_patch_warning_description">Universal patches have a more generalized use and do not work as reliably as patches that target specific apps. You may encounter issues while using them.\n\nThis warning can be disabled in the advanced settings.</string> + <string name="supported">This version</string> + <string name="universal">Any app</string> + <string name="unsupported">Unsupported</string> + <string name="search_patches">Search patches</string> + <string name="app_not_supported">This patch is not compatible with the selected app version (%1$s).\n\nIt only supports the following version(s): %2$s.</string> + <string name="continue_with_version">Continue with this version?</string> + <string name="version_not_supported">Not all patches support this version (%s). Do you want to continue anyway?</string> + <string name="download_application">Download application?</string> + <string name="app_not_installed">The app you selected isn\'t installed. Do you want to download it?</string> + <string name="failed_to_load_apk">Failed to load APK</string> + <string name="loading">Loading…</string> + <string name="not_installed">Not installed</string> + <string name="installed">Installed</string> + + <string name="app_info">App info</string> + <string name="uninstall">Uninstall</string> + <string name="unpatch">Unpatch</string> + <string name="repatch">Repatch</string> + <string name="install_type">Installation type</string> + <string name="package_name">Package name</string> + <string name="original_package_name">Original package name</string> + <string name="applied_patches">Applied patches</string> + <string name="view_applied_patches">View applied patches</string> + <string name="default_install">Default</string> + <string name="mount_install">Mount</string> + <string name="mounted">Mounted</string> + <string name="not_mounted">Not mounted</string> + <string name="mount">Mount</string> + <string name="unmount">Unmount</string> + <string name="failed_to_mount">Failed to mount: %s</string> + <string name="failed_to_unmount">Failed to unmount: %s</string> + <string name="unpatch_app">Unpatch app?</string> + <string name="unpatch_description">Are you sure you want to unpatch this app?</string> + + <string name="downloader_invalid_version">Downloader did not fetch the correct version</string> + <string name="downloader_app_not_found">Downloader did not find the app</string> + <string name="downloader_error">Downloader error: %s</string> + <string name="downloader_no_plugins_installed">No plugins installed.</string> + <string name="downloader_no_plugins_available">No trusted plugins available for use. Check your settings.</string> + <string name="already_patched">Already patched</string> + + <string name="patch_selector_sheet_filter_title">Filter</string> + <string name="patch_selector_sheet_filter_compat_title">Compatibility</string> + + <string name="string_option_menu_description">More options</string> + <string name="option_preset_custom_value">Custom value</string> + + <string name="path_selector">Select from storage</string> + <string name="path_selector_parent_dir">Previous directory</string> + <string name="path_selector_dirs">Directories</string> + <string name="path_selector_files">Files</string> + + <string name="show_password_field">Show password</string> + <string name="hide_password_field">Hide password</string> + + <string name="installer">Installer</string> + <string name="install_app">Install</string> + <string name="install_app_success">App installed</string> + <string name="install_app_fail">Failed to install app: %s</string> + <string name="reinstall_app_fail">Failed to reinstall app: %s</string> + <string name="uninstall_app_fail">Failed to uninstall app: %s</string> + <string name="open_app">Open</string> + <string name="save_apk">Save APK</string> + <string name="save_apk_success">APK Saved</string> + <string name="sign_fail">Failed to sign APK: %s</string> + <string name="save_logs">Save logs</string> + <string name="plugin_activity_dialog_body">User interaction is required in order to proceed with this plugin.</string> + <string name="select_install_type">Select installation type</string> + + <string name="patcher_step_group_preparing">Preparing</string> + <string name="patcher_step_load_patches">Load patches</string> + <string name="patcher_step_unpack">Read APK file</string> + <string name="patcher_step_group_patching">Patching</string> + <string name="patcher_step_group_saving">Saving</string> + <string name="patcher_step_write_patched">Write patched APK file</string> + <string name="patcher_step_sign_apk">Sign patched APK file</string> + <string name="patcher_notification_message">Patching in progress…</string> + <string name="execute_patches">Execute patches</string> + <string name="executing_patch">Execute %s</string> + <string name="failed_to_execute_patch">Failed to execute %s</string> + + <string name="step_completed">completed</string> + <string name="step_failed">failed</string> + <string name="step_running">running</string> + <string name="step_waiting">waiting</string> + + <string name="expand_content">expand</string> + <string name="collapse_content">collapse</string> + <string name="drag_handle">reorder</string> + + <string name="more">More</string> + <string name="less">Less</string> + <string name="continue_">Continue</string> + <string name="dismiss">Dismiss</string> + <string name="permanent_dismiss">Do not show this again</string> + <string name="donate">Donate</string> + <string name="website">Website</string> + <string name="github">GitHub</string> + <string name="contact">Contact</string> + <string name="version">Version</string> + <string name="submit_feedback">Submit issue or feedback</string> + <string name="submit_feedback_description">Help us improve this application</string> + <string name="developer_options">Developer options</string> + <string name="developer_options_description">Options for debugging issues</string> + <string name="bundle_input_source_url">Source URL</string> + <string name="bundle_update_success">Successfully updated %s</string> + <string name="bundle_update_unavailable">No update available for %s</string> + <string name="bundle_auto_update">Auto update</string> + <string name="bundle_auto_update_description">Automatically update this bundle when ReVanced starts</string> + <string name="bundle_view_patches">View patches</string> + <string name="bundle_view_patches_any_version">Any version</string> + <string name="bundle_view_patches_any_package">Any package</string> + + <string name="about_revanced_manager">About ReVanced Manager</string> + <string name="revanced_manager_description">ReVanced Manager is an application designed to work with ReVanced Patcher, which allows for long-lasting patches to be created for Android apps. The patching system is designed to automatically work with new versions of apps with minimal maintenance.</string> + <string name="update_available">An update is available</string> + <string name="current_version">Current version: %s</string> + <string name="new_version">New version: %s</string> + <string name="ready_to_install_update">Ready to install update</string> + <string name="update_completed">Update installed</string> + <string name="install_update_manager_failed">Failed to install update</string> + <string name="manual_update_check">Check for updates</string> + <string name="manual_update_check_description">Manually check for updates</string> + <string name="update_checking_manager">Auto check for updates</string> + <string name="update_checking_manager_description">Check for new versions of ReVanced Manager when the application starts</string> + <string name="changelog">View changelogs</string> + <string name="changelog_loading">Loading changelog</string> + <string name="changelog_download_fail">Failed to download changelog: %s</string> + <string name="changelog_description">Check out the latest changes in this update</string> + <string name="battery_optimization_notification">Battery optimizations must be turned off in order for ReVanced Manager to work correctly in the background. Click here to turn off optimizations.</string> + <string name="installing_manager_update">Installing update…</string> + <string name="downloading_manager_update">Downloading update…</string> + <string name="download_manager_failed">Failed to download update: %s</string> + <string name="cancel">Cancel</string> + <string name="save">Save</string> + <string name="save_with_count">Save (%1$s)</string> + <string name="update">Update</string> + <string name="empty">Empty</string> + <string name="installing_message">Tap on <b>Update</b> when prompted. \n ReVanced Manager will close when updating.</string> + <string name="no_changelogs_found">No changelogs found</string> + <string name="just_now">Just now</string> + <string name="minutes_ago">%sm ago</string> + <string name="hours_ago">%sh ago</string> + <string name="days_ago">%sd ago</string> + <string name="invalid_date">Invalid date</string> + <string name="disable_battery_optimization">Disable battery optimization</string> + <string name="input_dialog_value_invalid">Invalid value</string> + <string name="option_required">This option is required</string> + <string name="required_options_screen">Required options</string> + + <string name="failed_to_check_updates">Failed to check for updates: %s</string> + <string name="no_update_available">No update available</string> + <string name="update_check">Checking for updates…</string> + <string name="dismiss_temporary">Not now</string> + <string name="update_available_dialog_description">A new version of ReVanced Manager (%s) is available.</string> + <string name="failed_to_download_update">Failed to download update: %s</string> + <string name="download">Download</string> + <string name="download_confirmation_metered">You are currently on a metered connection, and data charges from your service provider may apply.\n\nDo you still want to continue?</string> + <string name="download_update_confirmation">Download update?</string> + <string name="no_contributors_found">No contributors found</string> + <string name="select">Select</string> + <string name="select_deselect_all">Select or deselect all</string> + <string name="select_bundle_type_dialog_title">Add new bundle</string> + <string name="select_bundle_type_dialog_description">Add a new bundle from a URL or storage</string> + <string name="local_bundle_description">Import local files from your storage, does not automatically update</string> + <string name="remote_bundle_description">Import remote files from a URL, can automatically update</string> + <string name="recommended">Recommended</string> + + <string name="installation_failed_dialog_title">Installation failed</string> + <string name="installation_cancelled_dialog_title">Installation cancelled</string> + <string name="installation_blocked_dialog_title">Installation blocked</string> + <string name="installation_conflict_dialog_title">Installation conflict</string> + <string name="installation_incompatible_dialog_title">Installation incompatible</string> + <string name="installation_invalid_dialog_title">Installation invalid</string> + <string name="installation_storage_issue_dialog_title">Not enough storage</string> + <string name="installation_timeout_dialog_title">Installation timed out</string> + <string name="installation_failed_description">The installation failed due to an unknown reason. Try again?</string> + <string name="installation_aborted_description">The installation was cancelled manually. Try again?</string> + <string name="installation_blocked_description">The installation was blocked. Review your device security settings and try again.</string> + <string name="installation_conflict_description">The installation was prevented by an existing installation of the app. Uninstall the installed app and try again?</string> + <string name="installation_incompatible_description">The app is incompatible with this device. Use an APK that is supported by this device and try again.</string> + <string name="installation_invalid_description">The app is invalid. Uninstall the app and try again?</string> + <string name="installation_storage_issue_description">The app could not be installed due to insufficient storage. Free up some space and try again.</string> + <string name="installation_timeout_description">The installation took too long. Try again?</string> + <string name="reinstall">Reinstall</string> + <string name="show">Show</string> + <string name="debugging">Debugging</string> + <string name="about_device">About device</string> + <string name="enter_url">Enter URL</string> + <string name="next">Next</string> + <string name="add_patch_bundle">Add patch bundle</string> + <string name="bundle_url">Bundle URL</string> + <string name="auto_update">Auto update</string> + <string name="unsupported_patches_dialog">These patches are not compatible with the selected app version (%1$s).\n\nClick on the patches to see more details.</string> + <string name="unsupported_patch">Unsupported patch</string> + <string name="any_version">Any</string> + <string name="never_show_again">Never show again</string> + <string name="show_manager_update_dialog_on_launch">Show update message on launch</string> + <string name="show_manager_update_dialog_on_launch_description">Shows a popup notification whenever there is a new update available on launch.</string> + <string name="failed_to_import_keystore">Failed to import keystore</string> + <string name="export">Export</string> +</resources> diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml new file mode 100644 index 0000000000..59eb8c3478 --- /dev/null +++ b/app/src/main/res/values/themes.xml @@ -0,0 +1,10 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + + <style name="Theme.ReVancedManager" parent="Theme.SplashScreen"> + <item name="windowSplashScreenAnimatedIcon">@drawable/ic_launcher_foreground</item> + <item name="postSplashScreenTheme">@style/Theme.AppCompat.NoActionBar</item> + <item name="android:windowActionBar">false</item> + <item name="android:windowNoTitle">true</item> + </style> +</resources> \ No newline at end of file diff --git a/app/src/main/res/xml/backup_rules.xml b/app/src/main/res/xml/backup_rules.xml new file mode 100644 index 0000000000..fa0f996d2c --- /dev/null +++ b/app/src/main/res/xml/backup_rules.xml @@ -0,0 +1,13 @@ +<?xml version="1.0" encoding="utf-8"?><!-- + Sample backup rules file; uncomment and customize as necessary. + See https://developer.android.com/guide/topics/data/autobackup + for details. + Note: This file is ignored for devices older that API 31 + See https://developer.android.com/about/versions/12/backup-restore +--> +<full-backup-content> + <!-- + <include domain="sharedpref" path="."/> + <exclude domain="sharedpref" path="device.xml"/> +--> +</full-backup-content> \ 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 new file mode 100644 index 0000000000..9ee9997b0b --- /dev/null +++ b/app/src/main/res/xml/data_extraction_rules.xml @@ -0,0 +1,19 @@ +<?xml version="1.0" encoding="utf-8"?><!-- + Sample data extraction rules file; uncomment and customize as necessary. + See https://developer.android.com/about/versions/12/backup-restore#xml-changes + for details. +--> +<data-extraction-rules> + <cloud-backup> + <!-- TODO: Use <include> and <exclude> to control what is backed up. + <include .../> + <exclude .../> + --> + </cloud-backup> + <!-- + <device-transfer> + <include .../> + <exclude .../> + </device-transfer> + --> +</data-extraction-rules> \ No newline at end of file diff --git a/assets/i18n/en_US.json b/assets/i18n/en_US.json deleted file mode 100644 index 63e8267798..0000000000 --- a/assets/i18n/en_US.json +++ /dev/null @@ -1,306 +0,0 @@ -{ - "okButton": "OK", - "cancelButton": "Cancel", - "quitButton": "Quit", - "updateButton": "Update", - "enabledLabel": "Enabled", - "disabledLabel": "Disabled", - "installed":"Installed: {version}", - "suggested":"Suggested: {version}", - "yesButton": "Yes", - "noButton": "No", - "warning": "Warning", - "notice": "Notice", - "noShowAgain": "Don't show this again", - "new": "New", - "navigationView": { - "dashboardTab": "Dashboard", - "patcherTab": "Patcher", - "settingsTab": "Settings" - }, - "homeView": { - "refreshSuccess": "Refreshed successfully", - "widgetTitle": "Dashboard", - - "updatesSubtitle": "Updates", - "patchedSubtitle": "Patched applications", - - "noUpdates": "No updates available", - - "WIP": "Work in progress...", - - "noInstallations": "No patched applications installed", - "installUpdate": "Continue to install the update?", - - "updateDialogTitle": "Update Manager", - "updatePatchesDialogTitle": "Update ReVanced Patches", - "updateChangelogTitle": "Changelog", - - "patchesConsentDialogText": "ReVanced Patches needs to be downloaded.", - "patchesConsentDialogText2": "This will connect you to {url}.", - "patchesConsentDialogText3": "Auto update?", - "patchesConsentDialogText3Sub": "You can change this in settings at a later time.", - - "notificationTitle": "Update downloaded", - "notificationText": "Tap to install the update", - - "downloadingMessage": "Downloading update...", - "downloadedMessage": "Update downloaded!", - - "installingMessage": "Installing update...", - - "errorDownloadMessage": "Unable to download update", - "errorInstallMessage": "Unable to install update", - - "noConnection": "No internet connection", - "updatesDisabled": "Updating a patched app is currently disabled. Repatch the app again." - }, - "applicationItem": { - "patchButton": "Patch", - "infoButton": "Info", - "changelogLabel": "Changelog" - }, - "latestCommitCard": { - "loadingLabel": "Loading...", - "timeagoLabel": "{time} ago", - "patcherLabel": "Patcher: ", - "managerLabel": "Manager: ", - "updateButton": "Update Manager" - }, - "patcherView": { - "widgetTitle": "Patcher", - "patchButton": "Patch", - - "patchDialogText": "You have selected a resource patch and a split APK installation has been detected, so patching errors may occur.\nAre you sure you want to proceed?", - "armv7WarningDialogText": "Patching on ARMv7 devices is not yet supported and might fail. Proceed anyways?", - "splitApkWarningDialogText": "Patching a split APK is not yet supported and might fail. Proceed anyways?", - "removedPatchesWarningDialogText": "The following patches have been removed since the last time you used them.\n\n{patches}\n\nProceed anyways?" - }, - "appSelectorCard": { - "widgetTitle": "Select an application", - "widgetTitleSelected": "Selected application", - "widgetSubtitle": "No application selected", - - "noAppsLabel": "No applications found", - "notInstalled":"App not installed", - - "currentVersion": "Current", - "suggestedVersion": "Suggested", - "allVersions": "All versions" - }, - "patchSelectorCard": { - "widgetTitle": "Select patches", - "widgetTitleSelected": "Selected patches", - - "widgetSubtitle": "Select an application first", - "widgetEmptySubtitle": "No patches selected" - }, - "socialMediaCard": { - "widgetTitle": "Socials", - "widgetSubtitle": "We are online!" - }, - "appSelectorView": { - "viewTitle": "Select an application", - "searchBarHint": "Search applications", - - "storageButton": "Storage", - "selectFromStorageButton": "Select from storage", - - "errorMessage": "Unable to use selected application", - - "downloadToast": "Download function is not available yet", - - "featureNotAvailable": "Feature not implemented", - "featureNotAvailableText": "This application is a split APK and cannot be selected. Unfortunately, this feature is only available for rooted users at the moment. However, you can still install the application by selecting its APK files from your device's storage instead" - }, - "patchesSelectorView": { - "viewTitle": "Select patches", - "searchBarHint": "Search patches", - "universalPatches": "Universal patches", - - "doneButton": "Done", - - "default": "Default", - "defaultTooltip": "Select all default patches", - - "none": "None", - "noneTooltip": "Deselect all patches", - - "loadPatchesSelection": "Load patches selection", - "noSavedPatches": "No saved patches for the selected app.\nPress Done to save current selection.", - "noPatchesFound": "No patches found for the selected app", - - "selectAllPatchesWarningContent": "You are about to select all patches, that includes non-suggested patches and can cause unwanted behavior." - }, - "patchItem": { - "unsupportedDialogText": "Selecting this patch may result in patching errors.\n\nApp version: {packageVersion}\nSupported versions:\n{supportedVersions}", - "unsupportedPatchVersion": "Patch is not supported for this app version. Enable the experimental toggle in settings to proceed.", - - "newPatchDialogText": "This is a new patch that has been added since the last time you have patched this app.", - "newPatch": "New patch", - - "patchesChangeWarningDialogText": "It is recommended to use the default selection of patches because changing it may cause unexpected issues.\n\nIf you know what you are doing, you can enable \"Enable changing selection\" in the settings.", - "patchesChangeWarningDialogButton": "Use default selection" - }, - "installerView": { - "widgetTitle": "Installer", - "installType": "Select install type", - "installTypeDescription": "Select the installation type to proceed with.", - - "installButton": "Install", - "installRootType": "Root", - "installNonRootType": "Non-root", - "installRecommendedType": "Recommended", - - "pressBackAgain": "Press back again to cancel", - "openButton": "Open", - "shareButton": "Share file", - - "notificationTitle": "ReVanced Manager is patching", - "notificationText": "Tap to return to the installer", - - "exportApkButtonTooltip": "Export patched APK", - "exportLogButtonTooltip": "Export log", - - "installErrorDialogTitle": "Error", - "installErrorDialogText1": "Root install is not possible with the current patches selection.\nRepatch your app or choose non-root install.", - "installErrorDialogText2": "Non-root install is not possible with the current patches selection.\nRepatch your app or choose root install if you have your device rooted.", - "installErrorDialogText3": "Root install is not possible as the original APK was selected from storage.\nSelect an installed app or choose non-root install.", - "noExit": "Installer is still running, cannot exit..." - }, - "settingsView": { - "widgetTitle": "Settings", - - "appearanceSectionTitle": "Appearance", - "teamSectionTitle": "Team", - "infoSectionTitle": "Info", - "advancedSectionTitle": "Advanced", - "exportSectionTitle": "Import & export", - "logsSectionTitle": "Logs", - - "darkThemeLabel": "Dark mode", - "darkThemeHint": "Welcome to the dark side", - - "dynamicThemeLabel": "Material You", - "dynamicThemeHint": "Enjoy an experience closer to your device", - - "languageLabel": "Language", - "englishOption": "English", - - "sourcesLabel": "Sources", - "sourcesLabelHint": "Configure your custom sources", - "sourcesIntegrationsLabel": "Integrations source", - "sourcesResetDialogTitle": "Reset", - "sourcesResetDialogText": "Are you sure you want to reset custom sources to their default values?", - "apiURLResetDialogText": "Are you sure you want to reset API URL to its default value?", - "sourcesUpdateNote": "Note: ReVanced Patches will be updated to the latest version automatically.\n\nThis will reveal your IP address to the server.", - - "apiURLLabel": "API URL", - "apiURLHint": "Configure your custom API URL", - "selectApiURL": "API URL", - "hostRepositoryLabel": "Repository API", - "orgPatchesLabel": "Patches organization", - "sourcesPatchesLabel": "Patches source", - "orgIntegrationsLabel": "Integrations organization", - - "contributorsLabel": "Contributors", - "contributorsHint": "A list of contributors of ReVanced", - - "logsLabel": "Logs", - "logsHint": "Share Manager's logs", - - "enablePatchesSelectionLabel": "Enable changing selection", - "enablePatchesSelectionHint": "Enable changing the selection of patches.", - "enablePatchesSelectionWarningText": "Changing the default selection of patches may cause unexpected issues.\n\nEnable anyways?", - "disablePatchesSelectionWarningText": "You are about to disable changing the selection of patches.\nThe default selection of patches will be restored.\n\nDisable anyways?", - - "autoUpdatePatchesLabel": "Auto update patches", - "autoUpdatePatchesHint": "Automatically update ReVanced Patches to the latest version", - "experimentalUniversalPatchesLabel": "Experimental universal patches support", - "experimentalUniversalPatchesHint": "Display all applications to use with universal patches, loading list of apps may be slower", - "experimentalPatchesLabel": "Experimental patches support", - "experimentalPatchesHint": "Enable usage of unsupported patches in any app version", - "enabledExperimentalPatches": "Experimental patches support enabled", - - "aboutLabel": "About", - "snackbarMessage": "Copied to clipboard", - "restartAppForChanges": "Restart the app to apply changes", - - "deleteTempDirLabel": "Delete temporary files", - "deleteTempDirHint": "Delete unused temporary files", - "deletedTempDir": "Temporary files deleted", - - "exportPatchesLabel": "Export patches selection", - "exportPatchesHint": "Export patches selection to a JSON file", - "exportedPatches": "Patches selection exported", - "noExportFileFound": "No patches selection to export", - - "importPatchesLabel": "Import patches selection", - "importPatchesHint": "Import patches selection from a JSON file", - "importedPatches": "Patches selection imported", - - "resetStoredPatchesLabel": "Reset patches", - "resetStoredPatchesHint": "Reset the stored patches selection", - - "resetStoredPatchesDialogTitle": "Reset patches selection?", - "resetStoredPatchesDialogText": "Resetting patches selection will remove all selected patches.", - "resetStoredPatches": "Patches selection has been reset", - - "deleteLogsLabel": "Delete logs", - "deleteLogsHint": "Delete collected manager logs", - "deletedLogs": "Logs deleted", - - "regenerateKeystoreLabel": "Regenerate keystore", - "regenerateKeystoreHint": "Regenerate the keystore used to sign the app", - - "regenerateKeystoreDialogTitle": "Regenerate keystore?", - "regenerateKeystoreDialogText": "Patched apps signed with the old keystore will no longer be able to update.", - "regeneratedKeystore": "Keystore regenerated", - - "exportKeystoreLabel": "Export keystore", - "exportKeystoreHint": "Export keystore used to sign apps", - "exportedKeystore": "Keystore exported", - "noKeystoreExportFileFound": "No keystore to export", - - "importKeystoreLabel": "Import keystore", - "importKeystoreHint": "Import keystore used to sign apps", - "importedKeystore": "Keystore imported", - - "selectKeystorePassword": "Keystore Password", - "selectKeystorePasswordHint": "Select keystore password used to sign the apk", - - "jsonSelectorErrorMessage": "Unable to use selected JSON file", - "keystoreSelectorErrorMessage": "Unable to use selected KEYSTORE file" - }, - "appInfoView": { - "widgetTitle": "App info", - "openButton": "Open", - "uninstallButton": "Uninstall", - "unpatchButton": "Unpatch", - "rootDialogTitle": "Error", - - "unpatchDialogText": "Are you sure you want to unpatch this app?", - "rootDialogText": "App was installed with superuser permissions, but currently ReVanced Manager has no permissions.\nPlease grant superuser permissions first.", - - "packageNameLabel": "Package name", - "originalPackageNameLabel": "Original package name", - "installTypeLabel": "Installation type", - "rootTypeLabel": "Root", - "nonRootTypeLabel": "Non-root", - "patchedDateLabel": "Patched date", - "appliedPatchesLabel": "Applied patches", - - "patchedDateHint": "{date} at {time}", - "appliedPatchesHint": "{quantity} applied patches", - - "updateNotImplemented": "This feature has not been implemented yet" - }, - "contributorsView": { - "widgetTitle": "Contributors", - "patcherContributors": "Patcher contributors", - "patchesContributors": "Patches contributors", - "integrationsContributors": "Integrations contributors", - "cliContributors": "CLI contributors", - "managerContributors": "Manager contributors" - } -} diff --git a/assets/revanced-headline/revanced-headline-vertical-dark.svg b/assets/revanced-headline/revanced-headline-vertical-dark.svg new file mode 100644 index 0000000000..a59bfb50bf --- /dev/null +++ b/assets/revanced-headline/revanced-headline-vertical-dark.svg @@ -0,0 +1 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg width="100%" height="100%" viewBox="0 0 9600 7249" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;"><clipPath id="_clip1"><rect id="Wordmark" x="614.902" y="5329.15" width="8370.2" height="1304.34"/></clipPath><g clip-path="url(#_clip1)"><path id="Wordmark1" serif:id="Wordmark" d="M614.902,6604.05l-0,-1274.9l538.293,0c12.394,0 29.069,0.443 50.022,1.328c20.953,0.885 39.693,2.804 56.22,5.755c76.14,11.805 138.557,36.889 187.251,75.255c48.694,38.365 84.551,86.616 107.57,144.754c23.019,58.138 34.529,122.916 34.529,194.335c-0,106.832 -26.561,198.171 -79.682,274.016c-53.121,75.845 -136.344,122.621 -249.668,140.328l-106.242,7.082l-297.478,0l-0,432.051l-240.815,0Zm718.904,0l-251.439,-518.815l247.898,-47.809l276.229,566.624l-272.688,0Zm-478.089,-656.93l286.853,0c12.395,0 25.971,-0.59 40.726,-1.77c14.756,-1.181 28.332,-3.542 40.727,-7.083c32.462,-8.854 57.547,-23.757 75.254,-44.71c17.707,-20.954 29.955,-44.268 36.742,-69.943c6.788,-25.675 10.182,-50.022 10.182,-73.041c-0,-23.019 -3.394,-47.367 -10.182,-73.042c-6.787,-25.675 -19.035,-48.989 -36.742,-69.942c-17.707,-20.954 -42.792,-35.857 -75.254,-44.711c-12.395,-3.541 -25.971,-5.902 -40.727,-7.082c-14.755,-1.181 -28.331,-1.771 -40.726,-1.771l-286.853,-0l-0,393.095Z" style="fill:#fff;fill-rule:nonzero;"/><path d="M2245.72,6630.61c-97.979,-0 -184.301,-21.101 -258.965,-63.303c-74.665,-42.202 -133.098,-100.34 -175.3,-174.414c-42.201,-74.074 -63.302,-158.92 -63.302,-254.538c-0,-104.471 20.658,-195.367 61.974,-272.688c41.317,-77.32 98.274,-137.377 170.873,-180.169c72.599,-42.791 156.117,-64.187 250.554,-64.187c100.34,-0 185.628,23.609 255.866,70.828c70.238,47.218 122.178,113.62 155.822,199.203c33.643,85.584 45.448,186.219 35.414,301.905l-238.159,-0l-0,-88.535c-0,-97.389 -15.494,-167.479 -46.481,-210.271c-30.988,-42.792 -81.6,-64.188 -151.838,-64.188c-82.042,0 -142.394,24.938 -181.054,74.812c-38.66,49.875 -57.99,123.507 -57.99,220.895c-0,89.125 19.33,158.035 57.99,206.729c38.66,48.695 95.47,73.042 170.43,73.042c47.219,-0 87.65,-10.329 121.293,-30.988c33.643,-20.658 59.318,-50.464 77.025,-89.42l240.816,69.057c-36.005,87.355 -92.815,155.232 -170.43,203.631c-77.616,48.399 -162.462,72.599 -254.538,72.599Zm-316.956,-437.363l0,-178.841l633.911,0l-0,178.841l-633.911,-0Z" style="fill:#fff;fill-rule:nonzero;"/><path d="M3099.19,6604.05l-389.554,-1274.9l247.898,0l318.726,1048.26l324.038,-1048.26l247.898,0l-389.554,1274.9l-359.452,0Z" style="fill:#fff;fill-rule:nonzero;"/><path d="M4166.93,6630.61c-68.467,-0 -126.457,-13.133 -173.971,-39.398c-47.514,-26.266 -83.518,-61.385 -108.013,-105.357c-24.495,-43.972 -36.742,-92.519 -36.742,-145.64c0,-44.268 6.788,-84.699 20.363,-121.293c13.576,-36.595 35.562,-68.91 65.959,-96.946c30.397,-28.036 71.27,-51.498 122.621,-70.385c35.414,-12.985 77.615,-24.495 126.605,-34.529c48.989,-10.034 104.471,-19.625 166.446,-28.774c61.974,-9.148 130.146,-19.33 204.515,-30.544l-86.764,47.809c0,-56.663 -13.575,-98.274 -40.726,-124.835c-27.151,-26.56 -72.599,-39.841 -136.344,-39.841c-35.414,0 -72.303,8.559 -110.669,25.676c-38.365,17.116 -65.22,47.513 -80.566,91.191l-217.797,-69.058c24.2,-79.091 69.648,-143.426 136.344,-193.006c66.697,-49.58 157.593,-74.369 272.688,-74.369c84.404,-0 159.363,12.985 224.879,38.955c65.516,25.97 115.096,70.828 148.739,134.573c18.887,35.414 30.102,70.828 33.643,106.242c3.542,35.414 5.312,74.96 5.312,118.637l0,584.331l-210.713,0l-0,-196.548l30.102,40.727c-46.629,64.335 -96.946,110.816 -150.952,139.442c-54.007,28.626 -122.326,42.94 -204.959,42.94Zm51.35,-189.465c44.268,-0 81.6,-7.821 111.997,-23.462c30.397,-15.641 54.597,-33.496 72.599,-53.564c18.002,-20.068 30.249,-36.889 36.742,-50.465c12.395,-25.97 19.625,-56.219 21.691,-90.748c2.066,-34.529 3.099,-63.303 3.099,-86.322l70.828,17.707c-71.418,11.805 -129.261,21.691 -173.529,29.66c-44.267,7.968 -79.976,15.198 -107.127,21.691c-27.151,6.492 -51.055,13.575 -71.714,21.248c-23.609,9.444 -42.644,19.625 -57.105,30.545c-14.46,10.919 -25.085,22.871 -31.872,35.856c-6.788,12.985 -10.182,27.446 -10.182,43.382c0,21.839 5.46,40.579 16.379,56.22c10.92,15.641 26.413,27.594 46.481,35.857c20.068,8.263 43.972,12.395 71.713,12.395Z" style="fill:#fff;fill-rule:nonzero;"/><path d="M5585.26,6604.05l-0,-451.528c-0,-21.839 -1.181,-49.728 -3.541,-83.666c-2.361,-33.938 -9.739,-68.024 -22.134,-102.258c-12.395,-34.233 -32.611,-62.86 -60.647,-85.879c-28.036,-23.019 -67.729,-34.529 -119.079,-34.529c-20.658,0 -42.792,3.247 -66.402,9.739c-23.609,6.493 -45.743,19.035 -66.401,37.628c-20.658,18.592 -37.627,45.89 -50.907,81.895c-13.281,36.004 -19.921,83.813 -19.921,143.426l-138.114,-65.516c-0,-75.549 15.346,-146.377 46.038,-212.484c30.692,-66.106 76.878,-119.522 138.557,-160.248c61.679,-40.726 139.443,-61.089 233.29,-61.089c74.959,-0 136.049,12.69 183.267,38.07c47.219,25.38 83.961,57.548 110.226,96.503c26.266,38.955 45.006,79.534 56.22,121.736c11.215,42.201 18.002,80.714 20.363,115.538c2.361,34.824 3.542,60.204 3.542,76.14l-0,536.522l-244.357,0Zm-653.388,0l-0,-956.178l214.254,0l0,316.955l30.102,0l0,639.223l-244.356,0Z" style="fill:#fff;fill-rule:nonzero;"/><path d="M6463.52,6630.61c-99.159,-0 -184.153,-22.134 -254.981,-66.402c-70.828,-44.267 -125.129,-104.471 -162.904,-180.611c-37.775,-76.14 -56.662,-162.019 -56.662,-257.637c-0,-96.798 19.625,-183.267 58.875,-259.407c39.251,-76.141 94.585,-136.049 166.003,-179.727c71.419,-43.677 155.822,-65.515 253.211,-65.515c112.734,-0 207.319,28.478 283.754,85.436c76.436,56.957 125.277,134.721 146.526,233.29l-240.815,63.745c-14.166,-49.58 -38.808,-88.24 -73.927,-115.981c-35.119,-27.741 -74.812,-41.612 -119.08,-41.612c-50.76,0 -92.371,12.248 -124.834,36.743c-32.463,24.494 -56.367,57.842 -71.714,100.044c-15.346,42.202 -23.019,89.863 -23.019,142.984c0,83.223 18.445,150.657 55.335,202.303c36.889,51.645 91.634,77.468 164.232,77.468c54.302,-0 95.618,-12.395 123.949,-37.185c28.331,-24.79 49.58,-60.204 63.745,-106.242l246.128,51.35c-27.151,101.52 -78.501,179.726 -154.051,234.618c-75.55,54.892 -168.807,82.338 -279.771,82.338Z" style="fill:#fff;fill-rule:nonzero;"/><path d="M7492.3,6630.61c-97.979,-0 -184.3,-21.101 -258.965,-63.303c-74.664,-42.202 -133.098,-100.34 -175.299,-174.414c-42.202,-74.074 -63.303,-158.92 -63.303,-254.538c0,-104.471 20.658,-195.367 61.975,-272.688c41.316,-77.32 98.274,-137.377 170.872,-180.169c72.599,-42.791 156.117,-64.187 250.554,-64.187c100.34,-0 185.629,23.609 255.867,70.828c70.237,47.218 122.178,113.62 155.821,199.203c33.643,85.584 45.448,186.219 35.414,301.905l-238.159,-0l-0,-88.535c-0,-97.389 -15.494,-167.479 -46.481,-210.271c-30.987,-42.792 -81.6,-64.188 -151.837,-64.188c-82.043,0 -142.394,24.938 -181.055,74.812c-38.66,49.875 -57.99,123.507 -57.99,220.895c0,89.125 19.33,158.035 57.99,206.729c38.661,48.695 95.471,73.042 170.43,73.042c47.219,-0 87.65,-10.329 121.293,-30.988c33.644,-20.658 59.319,-50.464 77.026,-89.42l240.815,69.057c-36.004,87.355 -92.814,155.232 -170.43,203.631c-77.616,48.399 -162.462,72.599 -254.538,72.599Zm-316.955,-437.363l-0,-178.841l633.91,0l0,178.841l-633.91,-0Z" style="fill:#fff;fill-rule:nonzero;"/><path d="M8512.22,6630.61c-87.945,-0 -164.97,-22.134 -231.076,-66.402c-66.106,-44.267 -117.604,-104.471 -154.494,-180.611c-36.889,-76.14 -55.334,-162.019 -55.334,-257.637c-0,-97.388 18.74,-184.005 56.22,-259.85c37.479,-75.845 90.158,-135.606 158.035,-179.284c67.876,-43.677 147.558,-65.515 239.044,-65.515c90.896,-0 167.331,22.133 229.306,66.401c61.974,44.267 108.898,104.471 140.77,180.611c31.873,76.14 47.809,162.019 47.809,257.637c0,95.618 -16.083,181.497 -48.251,257.637c-32.168,76.14 -80.124,136.344 -143.87,180.611c-63.745,44.268 -143.131,66.402 -238.159,66.402Zm38.956,-214.255c53.711,-0 96.65,-12.1 128.818,-36.299c32.168,-24.2 55.334,-58.138 69.5,-101.816c14.166,-43.677 21.248,-94.437 21.248,-152.28c0,-57.843 -7.082,-108.603 -21.248,-152.28c-14.166,-43.677 -36.742,-77.616 -67.729,-101.815c-30.987,-24.2 -71.566,-36.3 -121.736,-36.3c-53.711,0 -97.831,13.133 -132.36,39.398c-34.528,26.266 -60.056,61.385 -76.582,105.357c-16.527,43.972 -24.79,92.519 -24.79,145.64c-0,53.711 7.968,102.553 23.904,146.526c15.936,43.972 40.431,78.943 73.484,104.914c33.053,25.97 75.55,38.955 127.491,38.955Zm219.566,187.694l0,-655.159l-30.101,0l-0,-619.745l242.585,0l0,1274.9l-212.484,0Z" style="fill:#fff;fill-rule:nonzero;"/></g><g id="Logo"><g id="Ring"><circle id="Ring-Background" serif:id="Ring Background" cx="4800" cy="2664.57" r="2049.67" style="fill:#1b1b1b;"/><path id="Ring1" serif:id="Ring" d="M4800,614.902c1131.25,-0 2049.67,918.427 2049.67,2049.67c0,1131.25 -918.427,2049.67 -2049.67,2049.67c-1131.25,0 -2049.67,-918.427 -2049.67,-2049.67c-0,-1131.25 918.427,-2049.67 2049.67,-2049.67Zm-0,184.47c1029.43,0 1865.2,835.769 1865.2,1865.2c-0,1029.43 -835.769,1865.2 -1865.2,1865.2c-1029.43,-0 -1865.2,-835.769 -1865.2,-1865.2c0,-1029.43 835.769,-1865.2 1865.2,-1865.2Z" style="fill:url(#_Linear2);"/></g><g id="Shape"><path id="V-Shape" serif:id="V Shape" d="M5510.93,1997.78c7.593,-17.329 5.93,-37.319 -4.422,-53.156c-10.351,-15.836 -27.994,-25.381 -46.913,-25.381c-26.379,-0 -53.47,-0 -72.584,-0c-15.886,-0 -30.269,9.393 -36.655,23.938c-63.887,145.508 -401.084,913.501 -513.699,1169.99c-6.387,14.546 -20.77,23.939 -36.656,23.939c-15.886,-0 -30.269,-9.393 -36.655,-23.939c-112.615,-256.491 -449.812,-1024.49 -513.699,-1169.99c-6.387,-14.545 -20.769,-23.938 -36.655,-23.938c-19.114,-0 -46.202,-0 -72.58,-0c-18.919,-0 -36.561,9.545 -46.913,25.381c-10.351,15.837 -12.014,35.826 -4.421,53.155c120.706,275.509 522.01,1191.47 603.987,1378.58c8.931,20.385 29.079,33.554 51.335,33.554c32.246,0 78.957,0 111.203,0c22.256,0 42.403,-13.169 51.335,-33.554c81.978,-187.11 483.285,-1103.07 603.992,-1378.58Z" style="fill:#fff;"/><path id="Diamond" d="M4841.6,2640.55c-8.581,14.864 -24.44,24.02 -41.603,24.02c-17.163,0 -33.022,-9.156 -41.603,-24.02c-87.097,-150.856 -287.752,-498.4 -374.849,-649.256c-8.581,-14.864 -8.581,-33.176 0,-48.04c8.582,-14.863 24.441,-24.019 41.603,-24.019c174.194,-0 575.504,-0 749.698,-0c17.162,-0 33.021,9.156 41.603,24.019c8.581,14.864 8.581,33.176 -0,48.04c-87.097,150.856 -287.752,498.4 -374.849,649.256Z" style="fill:url(#_Linear3);"/></g></g><defs><linearGradient id="_Linear2" x1="0" y1="0" x2="1" y2="0" gradientUnits="userSpaceOnUse" gradientTransform="matrix(2.51012e-13,4099.34,-4099.34,2.51012e-13,4800,614.902)"><stop offset="0" style="stop-color:#f04e98;stop-opacity:1"/><stop offset="0.5" style="stop-color:#5f65d4;stop-opacity:1"/><stop offset="1" style="stop-color:#4e98f0;stop-opacity:1"/></linearGradient><linearGradient id="_Linear3" x1="0" y1="0" x2="1" y2="0" gradientUnits="userSpaceOnUse" gradientTransform="matrix(9.07776e-14,1482.51,-1447.76,8.86499e-14,4800,1919.24)"><stop offset="0" style="stop-color:#f04e98;stop-opacity:1"/><stop offset="0.5" style="stop-color:#5f65d4;stop-opacity:1"/><stop offset="1" style="stop-color:#4e98f0;stop-opacity:1"/></linearGradient></defs></svg> \ No newline at end of file diff --git a/assets/revanced-headline/revanced-headline-vertical-light.svg b/assets/revanced-headline/revanced-headline-vertical-light.svg new file mode 100644 index 0000000000..3c5eeccc70 --- /dev/null +++ b/assets/revanced-headline/revanced-headline-vertical-light.svg @@ -0,0 +1 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg width="100%" height="100%" viewBox="0 0 9600 7249" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;"><clipPath id="_clip1"><rect id="Wordmark" x="614.902" y="5329.15" width="8370.2" height="1304.34"/></clipPath><g clip-path="url(#_clip1)"><path id="Wordmark1" serif:id="Wordmark" d="M614.902,6604.05l-0,-1274.9l538.293,0c12.394,0 29.069,0.443 50.022,1.328c20.953,0.885 39.693,2.804 56.22,5.755c76.14,11.805 138.557,36.889 187.251,75.255c48.694,38.365 84.551,86.616 107.57,144.754c23.019,58.138 34.529,122.916 34.529,194.335c-0,106.832 -26.561,198.171 -79.682,274.016c-53.121,75.845 -136.344,122.621 -249.668,140.328l-106.242,7.082l-297.478,0l-0,432.051l-240.815,0Zm718.904,0l-251.439,-518.815l247.898,-47.809l276.229,566.624l-272.688,0Zm-478.089,-656.93l286.853,0c12.395,0 25.971,-0.59 40.726,-1.77c14.756,-1.181 28.332,-3.542 40.727,-7.083c32.462,-8.854 57.547,-23.757 75.254,-44.71c17.707,-20.954 29.955,-44.268 36.742,-69.943c6.788,-25.675 10.182,-50.022 10.182,-73.041c-0,-23.019 -3.394,-47.367 -10.182,-73.042c-6.787,-25.675 -19.035,-48.989 -36.742,-69.942c-17.707,-20.954 -42.792,-35.857 -75.254,-44.711c-12.395,-3.541 -25.971,-5.902 -40.727,-7.082c-14.755,-1.181 -28.331,-1.771 -40.726,-1.771l-286.853,-0l-0,393.095Z" style="fill-rule:nonzero;"/><path d="M2245.72,6630.61c-97.979,-0 -184.301,-21.101 -258.965,-63.303c-74.665,-42.202 -133.098,-100.34 -175.3,-174.414c-42.201,-74.074 -63.302,-158.92 -63.302,-254.538c-0,-104.471 20.658,-195.367 61.974,-272.688c41.317,-77.32 98.274,-137.377 170.873,-180.169c72.599,-42.791 156.117,-64.187 250.554,-64.187c100.34,-0 185.628,23.609 255.866,70.828c70.238,47.218 122.178,113.62 155.822,199.203c33.643,85.584 45.448,186.219 35.414,301.905l-238.159,-0l-0,-88.535c-0,-97.389 -15.494,-167.479 -46.481,-210.271c-30.988,-42.792 -81.6,-64.188 -151.838,-64.188c-82.042,0 -142.394,24.938 -181.054,74.812c-38.66,49.875 -57.99,123.507 -57.99,220.895c-0,89.125 19.33,158.035 57.99,206.729c38.66,48.695 95.47,73.042 170.43,73.042c47.219,-0 87.65,-10.329 121.293,-30.988c33.643,-20.658 59.318,-50.464 77.025,-89.42l240.816,69.057c-36.005,87.355 -92.815,155.232 -170.43,203.631c-77.616,48.399 -162.462,72.599 -254.538,72.599Zm-316.956,-437.363l0,-178.841l633.911,0l-0,178.841l-633.911,-0Z" style="fill-rule:nonzero;"/><path d="M3099.19,6604.05l-389.554,-1274.9l247.898,0l318.726,1048.26l324.038,-1048.26l247.898,0l-389.554,1274.9l-359.452,0Z" style="fill-rule:nonzero;"/><path d="M4166.93,6630.61c-68.467,-0 -126.457,-13.133 -173.971,-39.398c-47.514,-26.266 -83.518,-61.385 -108.013,-105.357c-24.495,-43.972 -36.742,-92.519 -36.742,-145.64c0,-44.268 6.788,-84.699 20.363,-121.293c13.576,-36.595 35.562,-68.91 65.959,-96.946c30.397,-28.036 71.27,-51.498 122.621,-70.385c35.414,-12.985 77.615,-24.495 126.605,-34.529c48.989,-10.034 104.471,-19.625 166.446,-28.774c61.974,-9.148 130.146,-19.33 204.515,-30.544l-86.764,47.809c0,-56.663 -13.575,-98.274 -40.726,-124.835c-27.151,-26.56 -72.599,-39.841 -136.344,-39.841c-35.414,0 -72.303,8.559 -110.669,25.676c-38.365,17.116 -65.22,47.513 -80.566,91.191l-217.797,-69.058c24.2,-79.091 69.648,-143.426 136.344,-193.006c66.697,-49.58 157.593,-74.369 272.688,-74.369c84.404,-0 159.363,12.985 224.879,38.955c65.516,25.97 115.096,70.828 148.739,134.573c18.887,35.414 30.102,70.828 33.643,106.242c3.542,35.414 5.312,74.96 5.312,118.637l0,584.331l-210.713,0l-0,-196.548l30.102,40.727c-46.629,64.335 -96.946,110.816 -150.952,139.442c-54.007,28.626 -122.326,42.94 -204.959,42.94Zm51.35,-189.465c44.268,-0 81.6,-7.821 111.997,-23.462c30.397,-15.641 54.597,-33.496 72.599,-53.564c18.002,-20.068 30.249,-36.889 36.742,-50.465c12.395,-25.97 19.625,-56.219 21.691,-90.748c2.066,-34.529 3.099,-63.303 3.099,-86.322l70.828,17.707c-71.418,11.805 -129.261,21.691 -173.529,29.66c-44.267,7.968 -79.976,15.198 -107.127,21.691c-27.151,6.492 -51.055,13.575 -71.714,21.248c-23.609,9.444 -42.644,19.625 -57.105,30.545c-14.46,10.919 -25.085,22.871 -31.872,35.856c-6.788,12.985 -10.182,27.446 -10.182,43.382c0,21.839 5.46,40.579 16.379,56.22c10.92,15.641 26.413,27.594 46.481,35.857c20.068,8.263 43.972,12.395 71.713,12.395Z" style="fill-rule:nonzero;"/><path d="M5585.26,6604.05l-0,-451.528c-0,-21.839 -1.181,-49.728 -3.541,-83.666c-2.361,-33.938 -9.739,-68.024 -22.134,-102.258c-12.395,-34.233 -32.611,-62.86 -60.647,-85.879c-28.036,-23.019 -67.729,-34.529 -119.079,-34.529c-20.658,0 -42.792,3.247 -66.402,9.739c-23.609,6.493 -45.743,19.035 -66.401,37.628c-20.658,18.592 -37.627,45.89 -50.907,81.895c-13.281,36.004 -19.921,83.813 -19.921,143.426l-138.114,-65.516c-0,-75.549 15.346,-146.377 46.038,-212.484c30.692,-66.106 76.878,-119.522 138.557,-160.248c61.679,-40.726 139.443,-61.089 233.29,-61.089c74.959,-0 136.049,12.69 183.267,38.07c47.219,25.38 83.961,57.548 110.226,96.503c26.266,38.955 45.006,79.534 56.22,121.736c11.215,42.201 18.002,80.714 20.363,115.538c2.361,34.824 3.542,60.204 3.542,76.14l-0,536.522l-244.357,0Zm-653.388,0l-0,-956.178l214.254,0l0,316.955l30.102,0l0,639.223l-244.356,0Z" style="fill-rule:nonzero;"/><path d="M6463.52,6630.61c-99.159,-0 -184.153,-22.134 -254.981,-66.402c-70.828,-44.267 -125.129,-104.471 -162.904,-180.611c-37.775,-76.14 -56.662,-162.019 -56.662,-257.637c-0,-96.798 19.625,-183.267 58.875,-259.407c39.251,-76.141 94.585,-136.049 166.003,-179.727c71.419,-43.677 155.822,-65.515 253.211,-65.515c112.734,-0 207.319,28.478 283.754,85.436c76.436,56.957 125.277,134.721 146.526,233.29l-240.815,63.745c-14.166,-49.58 -38.808,-88.24 -73.927,-115.981c-35.119,-27.741 -74.812,-41.612 -119.08,-41.612c-50.76,0 -92.371,12.248 -124.834,36.743c-32.463,24.494 -56.367,57.842 -71.714,100.044c-15.346,42.202 -23.019,89.863 -23.019,142.984c0,83.223 18.445,150.657 55.335,202.303c36.889,51.645 91.634,77.468 164.232,77.468c54.302,-0 95.618,-12.395 123.949,-37.185c28.331,-24.79 49.58,-60.204 63.745,-106.242l246.128,51.35c-27.151,101.52 -78.501,179.726 -154.051,234.618c-75.55,54.892 -168.807,82.338 -279.771,82.338Z" style="fill-rule:nonzero;"/><path d="M7492.3,6630.61c-97.979,-0 -184.3,-21.101 -258.965,-63.303c-74.664,-42.202 -133.098,-100.34 -175.299,-174.414c-42.202,-74.074 -63.303,-158.92 -63.303,-254.538c0,-104.471 20.658,-195.367 61.975,-272.688c41.316,-77.32 98.274,-137.377 170.872,-180.169c72.599,-42.791 156.117,-64.187 250.554,-64.187c100.34,-0 185.629,23.609 255.867,70.828c70.237,47.218 122.178,113.62 155.821,199.203c33.643,85.584 45.448,186.219 35.414,301.905l-238.159,-0l-0,-88.535c-0,-97.389 -15.494,-167.479 -46.481,-210.271c-30.987,-42.792 -81.6,-64.188 -151.837,-64.188c-82.043,0 -142.394,24.938 -181.055,74.812c-38.66,49.875 -57.99,123.507 -57.99,220.895c0,89.125 19.33,158.035 57.99,206.729c38.661,48.695 95.471,73.042 170.43,73.042c47.219,-0 87.65,-10.329 121.293,-30.988c33.644,-20.658 59.319,-50.464 77.026,-89.42l240.815,69.057c-36.004,87.355 -92.814,155.232 -170.43,203.631c-77.616,48.399 -162.462,72.599 -254.538,72.599Zm-316.955,-437.363l-0,-178.841l633.91,0l0,178.841l-633.91,-0Z" style="fill-rule:nonzero;"/><path d="M8512.22,6630.61c-87.945,-0 -164.97,-22.134 -231.076,-66.402c-66.106,-44.267 -117.604,-104.471 -154.494,-180.611c-36.889,-76.14 -55.334,-162.019 -55.334,-257.637c-0,-97.388 18.74,-184.005 56.22,-259.85c37.479,-75.845 90.158,-135.606 158.035,-179.284c67.876,-43.677 147.558,-65.515 239.044,-65.515c90.896,-0 167.331,22.133 229.306,66.401c61.974,44.267 108.898,104.471 140.77,180.611c31.873,76.14 47.809,162.019 47.809,257.637c0,95.618 -16.083,181.497 -48.251,257.637c-32.168,76.14 -80.124,136.344 -143.87,180.611c-63.745,44.268 -143.131,66.402 -238.159,66.402Zm38.956,-214.255c53.711,-0 96.65,-12.1 128.818,-36.299c32.168,-24.2 55.334,-58.138 69.5,-101.816c14.166,-43.677 21.248,-94.437 21.248,-152.28c0,-57.843 -7.082,-108.603 -21.248,-152.28c-14.166,-43.677 -36.742,-77.616 -67.729,-101.815c-30.987,-24.2 -71.566,-36.3 -121.736,-36.3c-53.711,0 -97.831,13.133 -132.36,39.398c-34.528,26.266 -60.056,61.385 -76.582,105.357c-16.527,43.972 -24.79,92.519 -24.79,145.64c-0,53.711 7.968,102.553 23.904,146.526c15.936,43.972 40.431,78.943 73.484,104.914c33.053,25.97 75.55,38.955 127.491,38.955Zm219.566,187.694l0,-655.159l-30.101,0l-0,-619.745l242.585,0l0,1274.9l-212.484,0Z" style="fill-rule:nonzero;"/></g><g id="Logo"><g id="Ring"><circle id="Ring-Background" serif:id="Ring Background" cx="4800" cy="2664.57" r="2049.67" style="fill:#1b1b1b;"/><path id="Ring1" serif:id="Ring" d="M4800,614.902c1131.25,-0 2049.67,918.427 2049.67,2049.67c0,1131.25 -918.427,2049.67 -2049.67,2049.67c-1131.25,0 -2049.67,-918.427 -2049.67,-2049.67c-0,-1131.25 918.427,-2049.67 2049.67,-2049.67Zm-0,184.47c1029.43,0 1865.2,835.769 1865.2,1865.2c-0,1029.43 -835.769,1865.2 -1865.2,1865.2c-1029.43,-0 -1865.2,-835.769 -1865.2,-1865.2c0,-1029.43 835.769,-1865.2 1865.2,-1865.2Z" style="fill:url(#_Linear2);"/></g><g id="Shape"><path id="V-Shape" serif:id="V Shape" d="M5510.93,1997.78c7.593,-17.329 5.93,-37.319 -4.422,-53.156c-10.351,-15.836 -27.994,-25.381 -46.913,-25.381c-26.379,-0 -53.47,-0 -72.584,-0c-15.886,-0 -30.269,9.393 -36.655,23.938c-63.887,145.508 -401.084,913.501 -513.699,1169.99c-6.387,14.546 -20.77,23.939 -36.656,23.939c-15.886,-0 -30.269,-9.393 -36.655,-23.939c-112.615,-256.491 -449.812,-1024.49 -513.699,-1169.99c-6.387,-14.545 -20.769,-23.938 -36.655,-23.938c-19.114,-0 -46.202,-0 -72.58,-0c-18.919,-0 -36.561,9.545 -46.913,25.381c-10.351,15.837 -12.014,35.826 -4.421,53.155c120.706,275.509 522.01,1191.47 603.987,1378.58c8.931,20.385 29.079,33.554 51.335,33.554c32.246,0 78.957,0 111.203,0c22.256,0 42.403,-13.169 51.335,-33.554c81.978,-187.11 483.285,-1103.07 603.992,-1378.58Z" style="fill:#fff;"/><path id="Diamond" d="M4841.6,2640.55c-8.581,14.864 -24.44,24.02 -41.603,24.02c-17.163,0 -33.022,-9.156 -41.603,-24.02c-87.097,-150.856 -287.752,-498.4 -374.849,-649.256c-8.581,-14.864 -8.581,-33.176 0,-48.04c8.582,-14.863 24.441,-24.019 41.603,-24.019c174.194,-0 575.504,-0 749.698,-0c17.162,-0 33.021,9.156 41.603,24.019c8.581,14.864 8.581,33.176 -0,48.04c-87.097,150.856 -287.752,498.4 -374.849,649.256Z" style="fill:url(#_Linear3);"/></g></g><defs><linearGradient id="_Linear2" x1="0" y1="0" x2="1" y2="0" gradientUnits="userSpaceOnUse" gradientTransform="matrix(2.51012e-13,4099.34,-4099.34,2.51012e-13,4800,614.902)"><stop offset="0" style="stop-color:#f04e98;stop-opacity:1"/><stop offset="0.5" style="stop-color:#5f65d4;stop-opacity:1"/><stop offset="1" style="stop-color:#4e98f0;stop-opacity:1"/></linearGradient><linearGradient id="_Linear3" x1="0" y1="0" x2="1" y2="0" gradientUnits="userSpaceOnUse" gradientTransform="matrix(9.07776e-14,1482.51,-1447.76,8.86499e-14,4800,1919.24)"><stop offset="0" style="stop-color:#f04e98;stop-opacity:1"/><stop offset="0.5" style="stop-color:#5f65d4;stop-opacity:1"/><stop offset="1" style="stop-color:#4e98f0;stop-opacity:1"/></linearGradient></defs></svg> \ No newline at end of file diff --git a/assets/revanced-logo/revanced-logo-round.svg b/assets/revanced-logo/revanced-logo-round.svg new file mode 100644 index 0000000000..901e1914b4 --- /dev/null +++ b/assets/revanced-logo/revanced-logo-round.svg @@ -0,0 +1 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg width="100%" height="100%" viewBox="0 0 800 800" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;"><g id="Logo"><g id="Ring"><circle id="Ring-Background" serif:id="Ring Background" cx="400" cy="400" r="400" style="fill:#1b1b1b;"/><path id="Ring1" serif:id="Ring" d="M400,0c220.766,0 400,179.234 400,400c-0,220.766 -179.234,400 -400,400c-220.766,-0 -400,-179.234 -400,-400c0,-220.766 179.234,-400 400,-400Zm-0,36c200.897,-0 364,163.103 364,364c0,200.897 -163.103,364 -364,364c-200.897,0 -364,-163.103 -364,-364c-0,-200.897 163.103,-364 364,-364Z" style="fill:url(#_Linear1);"/></g><g id="Shape"><path id="V-Shape" serif:id="V Shape" d="M538.74,269.872c1.481,-3.382 1.157,-7.283 -0.863,-10.373c-2.021,-3.091 -5.464,-4.954 -9.156,-4.954c-5.148,0 -10.435,0 -14.165,0c-3.1,0 -5.907,1.834 -7.153,4.672c-12.468,28.396 -78.273,178.273 -100.25,228.328c-1.246,2.838 -4.053,4.671 -7.154,4.671c-3.1,0 -5.907,-1.833 -7.153,-4.671c-21.977,-50.055 -87.782,-199.932 -100.25,-228.328c-1.246,-2.838 -4.053,-4.672 -7.153,-4.672c-3.73,0 -9.017,0 -14.164,0c-3.693,0 -7.135,1.863 -9.156,4.954c-2.02,3.09 -2.344,6.991 -0.863,10.373c23.557,53.766 101.872,232.519 117.871,269.034c1.743,3.979 5.674,6.549 10.018,6.549c6.293,-0 15.408,-0 21.701,-0c4.344,-0 8.275,-2.57 10.018,-6.549c15.999,-36.515 94.315,-215.268 117.872,-269.034Z" style="fill:#fff;"/><path id="Diamond" d="M408.119,395.312c-1.675,2.901 -4.77,4.688 -8.119,4.688c-3.349,-0 -6.444,-1.787 -8.119,-4.688c-16.997,-29.44 -56.156,-97.264 -73.153,-126.704c-1.675,-2.901 -1.675,-6.474 0,-9.375c1.675,-2.901 4.77,-4.688 8.119,-4.688c33.995,0 112.311,0 146.306,0c3.349,0 6.444,1.787 8.119,4.688c1.675,2.901 1.675,6.474 -0,9.375c-16.997,29.44 -56.156,97.264 -73.153,126.704Z" style="fill:url(#_Linear2);"/></g></g><defs><linearGradient id="_Linear1" x1="0" y1="0" x2="1" y2="0" gradientUnits="userSpaceOnUse" gradientTransform="matrix(4.89859e-14,800,-800,4.89859e-14,400.001,3.31681e-10)"><stop offset="0" style="stop-color:#f04e98;stop-opacity:1"/><stop offset="0.5" style="stop-color:#5f65d4;stop-opacity:1"/><stop offset="1" style="stop-color:#4e98f0;stop-opacity:1"/></linearGradient><linearGradient id="_Linear2" x1="0" y1="0" x2="1" y2="0" gradientUnits="userSpaceOnUse" gradientTransform="matrix(1.77155e-14,289.317,-282.535,1.73003e-14,400,254.545)"><stop offset="0" style="stop-color:#f04e98;stop-opacity:1"/><stop offset="0.5" style="stop-color:#5f65d4;stop-opacity:1"/><stop offset="1" style="stop-color:#4e98f0;stop-opacity:1"/></linearGradient></defs></svg> \ No newline at end of file diff --git a/build.gradle.kts b/build.gradle.kts new file mode 100644 index 0000000000..8ed32e0238 --- /dev/null +++ b/build.gradle.kts @@ -0,0 +1,16 @@ +plugins { + alias(libs.plugins.android.application) apply false + alias(libs.plugins.android.library) apply false + alias(libs.plugins.devtools) apply false + alias(libs.plugins.kotlin.android) apply false + alias(libs.plugins.kotlin.serialization) apply false + alias(libs.plugins.kotlin.parcelize) apply false + alias(libs.plugins.about.libraries) apply false + alias(libs.plugins.compose.compiler) apply false + alias(libs.plugins.binary.compatibility.validator) +} + +apiValidation { + ignoredProjects.addAll(listOf("app", "example-downloader-plugin")) + nonPublicMarkers += "app.revanced.manager.plugin.downloader.PluginHostApi" +} \ No newline at end of file diff --git a/crowdin.yml b/crowdin.yml deleted file mode 100644 index 177682aae4..0000000000 --- a/crowdin.yml +++ /dev/null @@ -1,9 +0,0 @@ -project_id_env: CROWDIN_PROJECT_ID -api_token_env: CROWDIN_PERSONAL_TOKEN - -commit_message: 'chore(i18n): sync translations' - -preserve_hierarchy: true -files: - - source: assets/i18n/en_US.json - translation: assets/i18n/%locale_with_underscore%.json \ No newline at end of file diff --git a/docs/0_prerequisites.md b/docs/0_prerequisites.md index a53b46fb9c..9d86cd5438 100644 --- a/docs/0_prerequisites.md +++ b/docs/0_prerequisites.md @@ -5,9 +5,6 @@ In order to use ReVanced Manager, certain requirements must be met. ## 🤝 Requirements - An Android device running Android 8 or higher -- Any device architecture except ARMv7[^1] - -[^1]: This constraint only applies to patches, that require patching APK resources which is why some patches may or may not work on ARMv7 architecture. You can find out, which architectures your device supports here: [⚙️ Configuring ReVanced Manager](2_4_settings.md#%E2%84%B9%EF%B8%8F-about). ## ⏭️ What's next diff --git a/docs/2_1_patching.md b/docs/2_1_patching.md index 428660cebf..7203affa2a 100644 --- a/docs/2_1_patching.md +++ b/docs/2_1_patching.md @@ -4,21 +4,19 @@ The following pages will guide you through using ReVanced Manager to patch apps. ## ✅ Steps to patch apps -1. Navigate to the **Patcher** tab from the bottom navigation bar -2. Tap on the **Select an app** card +1. Navigate to the Apps tab from the top navigation bar +2. Tap the + button in the bottom right corner 3. Choose an app to patch[^1] - > **Note**: The suggested version is visible in each app's card. -4. Tap on the **Select patches** card and select the patches you want to apply[^2] - > **Warning**: If you see a warning you can click on it for more information. -5. Tap on the **Done** then **Patch** button - > **Warning**: The patching process may take ~5 minutes. Exiting the app may increase the time it takes to patch. -6. Tap on the **Install** button +4. Tap on the version of the app you want to patch[^2] +5. Select the patches you want to apply +6. Tap the Patch button +7. Tap on the **Install** button > **Note**: If you are rooted, you can mount the patched app on top of the original app.[^3] > Optionally, you may export the patched app to storage using the options in the top right corner. [^1]: Non-root users may be prompted to select an APK from storage, in which case you have to source the APK file yourself. ReVanced does not provide any APK files. -[^2]: It is suggested to use the default set of patches by tapping on the **Default** button above the list of patches. -[^3]: Mounting the patched app on top of the original app will only work if the installed app version matches the version of the app selected in step 3. above. +[^2]: It is suggested to use the version with the most patches to get the most out of ReVanced. +[^3]: Mounting the patched app on top of the original app will only work if the installed app version matches the version of the app selected in step 4. above. ## ⏭️ What's next diff --git a/docs/2_2_managing.md b/docs/2_2_managing.md index 1ad02229b1..29ec56fc4f 100644 --- a/docs/2_2_managing.md +++ b/docs/2_2_managing.md @@ -4,12 +4,12 @@ After patching an app, you may want to manage it. This page will guide you throu ## ✅ Steps to manage patched apps -1. Tap on the **Dashboard** tab in the bottom navigation bar -2. Tap on the **Info** button for the app you want to manage -3. Choose one of the options from the menu - +1. Navigate to the Apps tab from the top navigation bar +2. Select the app you want to manage +3. ## ⏭️ What's next The next page will bring you back to the usage page. Continue: [🛠️ Usage](2_usage.md) + diff --git a/docs/2_3_updating.md b/docs/2_3_updating.md index 9851ac900a..2b42104cf1 100644 --- a/docs/2_3_updating.md +++ b/docs/2_3_updating.md @@ -4,8 +4,7 @@ In order to keep up with the latest features and bug fixes, it is recommended to ## ✅ Updating steps -1. Navigate to the **Dashboard** tab from the bottom navigation bar -2. Tap on the **Update** button in the **Updates** section +> Currently not implemented ## ⏭️ What's next diff --git a/docs/4_building.md b/docs/4_building.md index d4c0887cc8..56917e5fad 100644 --- a/docs/4_building.md +++ b/docs/4_building.md @@ -2,7 +2,7 @@ This page will guide you through building ReVanced Manager from source. -1. Setup the Flutter environment for your [platform](https://docs.flutter.dev/get-started/install) +1. Download Java SDK 17 ([Azul JDK](https://www.azul.com/downloads/?version=java-17-lts&package=jdk#zulu) or [OpenJDK](https://jdk.java.net/java-se-ri/17)) and add it to path 2. Clone the repository @@ -19,22 +19,20 @@ This page will guide you through building ReVanced Manager from source. gpr.key = ghp_longrandomkey ``` -5. Get dependencies +5. Set the `sdk.dir` property in `local.properties` to your Android SDK location - ```sh - flutter pub get + ```properties + sdk.dir = /path/to/android/sdk ``` -6. Delete conflicting outputs +6. Build the APK + Debug: ```sh - flutter packages pub run build_runner build --delete-conflicting-outputs + ./gradlew assembleDebug ``` - > **Note**: Must be run every time you sync your local repository with the remote repository. - -7. Build the APK - + Release: ```sh - flutter build apk + ./gradlew assembleRelease -Psign ``` diff --git a/downloader-plugin/.gitignore b/downloader-plugin/.gitignore new file mode 100644 index 0000000000..42afabfd2a --- /dev/null +++ b/downloader-plugin/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/downloader-plugin/api/downloader-plugin.api b/downloader-plugin/api/downloader-plugin.api new file mode 100644 index 0000000000..d3a22653f1 --- /dev/null +++ b/downloader-plugin/api/downloader-plugin.api @@ -0,0 +1,171 @@ +public abstract interface class app/revanced/manager/plugin/downloader/BaseDownloadScope : app/revanced/manager/plugin/downloader/Scope { +} + +public final class app/revanced/manager/plugin/downloader/ConstantsKt { + public static final field PLUGIN_HOST_PERMISSION Ljava/lang/String; +} + +public final class app/revanced/manager/plugin/downloader/DownloadUrl : android/os/Parcelable { + public static final field CREATOR Landroid/os/Parcelable$Creator; + public fun <init> (Ljava/lang/String;Ljava/util/Map;)V + public synthetic fun <init> (Ljava/lang/String;Ljava/util/Map;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun component1 ()Ljava/lang/String; + public final fun component2 ()Ljava/util/Map; + public final fun copy (Ljava/lang/String;Ljava/util/Map;)Lapp/revanced/manager/plugin/downloader/DownloadUrl; + public static synthetic fun copy$default (Lapp/revanced/manager/plugin/downloader/DownloadUrl;Ljava/lang/String;Ljava/util/Map;ILjava/lang/Object;)Lapp/revanced/manager/plugin/downloader/DownloadUrl; + public final fun describeContents ()I + public fun equals (Ljava/lang/Object;)Z + public final fun getHeaders ()Ljava/util/Map; + public final fun getUrl ()Ljava/lang/String; + public fun hashCode ()I + public final fun toDownloadResult ()Lkotlin/Pair; + public fun toString ()Ljava/lang/String; + public final fun writeToParcel (Landroid/os/Parcel;I)V +} + +public final class app/revanced/manager/plugin/downloader/DownloadUrl$Creator : android/os/Parcelable$Creator { + public fun <init> ()V + public final fun createFromParcel (Landroid/os/Parcel;)Lapp/revanced/manager/plugin/downloader/DownloadUrl; + public synthetic fun createFromParcel (Landroid/os/Parcel;)Ljava/lang/Object; + public final fun newArray (I)[Lapp/revanced/manager/plugin/downloader/DownloadUrl; + public synthetic fun newArray (I)[Ljava/lang/Object; +} + +public final class app/revanced/manager/plugin/downloader/Downloader { +} + +public final class app/revanced/manager/plugin/downloader/DownloaderBuilder { +} + +public final class app/revanced/manager/plugin/downloader/DownloaderKt { + public static final fun Downloader (Lkotlin/jvm/functions/Function1;)Lapp/revanced/manager/plugin/downloader/DownloaderBuilder; +} + +public final class app/revanced/manager/plugin/downloader/DownloaderScope : app/revanced/manager/plugin/downloader/Scope { + public final fun download (Lkotlin/jvm/functions/Function3;)V + public final fun get (Lkotlin/jvm/functions/Function4;)V + public fun getHostPackageName ()Ljava/lang/String; + public fun getPluginPackageName ()Ljava/lang/String; + public final fun useService (Landroid/content/Intent;Lkotlin/jvm/functions/Function2;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; +} + +public final class app/revanced/manager/plugin/downloader/ExtensionsKt { + public static final fun download (Lapp/revanced/manager/plugin/downloader/DownloaderScope;Lkotlin/jvm/functions/Function4;)V +} + +public abstract interface class app/revanced/manager/plugin/downloader/GetScope : app/revanced/manager/plugin/downloader/Scope { + public abstract fun requestStartActivity (Landroid/content/Intent;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; +} + +public abstract interface class app/revanced/manager/plugin/downloader/InputDownloadScope : app/revanced/manager/plugin/downloader/BaseDownloadScope { +} + +public abstract interface class app/revanced/manager/plugin/downloader/OutputDownloadScope : app/revanced/manager/plugin/downloader/BaseDownloadScope { + public abstract fun reportSize (JLkotlin/coroutines/Continuation;)Ljava/lang/Object; +} + +public final class app/revanced/manager/plugin/downloader/Package : android/os/Parcelable { + public static final field CREATOR Landroid/os/Parcelable$Creator; + public fun <init> (Ljava/lang/String;Ljava/lang/String;)V + public final fun component1 ()Ljava/lang/String; + public final fun component2 ()Ljava/lang/String; + public final fun copy (Ljava/lang/String;Ljava/lang/String;)Lapp/revanced/manager/plugin/downloader/Package; + public static synthetic fun copy$default (Lapp/revanced/manager/plugin/downloader/Package;Ljava/lang/String;Ljava/lang/String;ILjava/lang/Object;)Lapp/revanced/manager/plugin/downloader/Package; + public final fun describeContents ()I + public fun equals (Ljava/lang/Object;)Z + public final fun getName ()Ljava/lang/String; + public final fun getVersion ()Ljava/lang/String; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; + public final fun writeToParcel (Landroid/os/Parcel;I)V +} + +public final class app/revanced/manager/plugin/downloader/Package$Creator : android/os/Parcelable$Creator { + public fun <init> ()V + public final fun createFromParcel (Landroid/os/Parcel;)Lapp/revanced/manager/plugin/downloader/Package; + public synthetic fun createFromParcel (Landroid/os/Parcel;)Ljava/lang/Object; + public final fun newArray (I)[Lapp/revanced/manager/plugin/downloader/Package; + public synthetic fun newArray (I)[Ljava/lang/Object; +} + +public abstract interface annotation class app/revanced/manager/plugin/downloader/PluginHostApi : java/lang/annotation/Annotation { +} + +public abstract interface class app/revanced/manager/plugin/downloader/Scope { + public abstract fun getHostPackageName ()Ljava/lang/String; + public abstract fun getPluginPackageName ()Ljava/lang/String; +} + +public abstract class app/revanced/manager/plugin/downloader/UserInteractionException : java/lang/Exception { + public synthetic fun <init> (Ljava/lang/String;Lkotlin/jvm/internal/DefaultConstructorMarker;)V +} + +public abstract class app/revanced/manager/plugin/downloader/UserInteractionException$Activity : app/revanced/manager/plugin/downloader/UserInteractionException { + public synthetic fun <init> (Ljava/lang/String;Lkotlin/jvm/internal/DefaultConstructorMarker;)V +} + +public final class app/revanced/manager/plugin/downloader/UserInteractionException$Activity$Cancelled : app/revanced/manager/plugin/downloader/UserInteractionException$Activity { +} + +public final class app/revanced/manager/plugin/downloader/UserInteractionException$Activity$NotCompleted : app/revanced/manager/plugin/downloader/UserInteractionException$Activity { + public final fun getIntent ()Landroid/content/Intent; + public final fun getResultCode ()I +} + +public final class app/revanced/manager/plugin/downloader/UserInteractionException$RequestDenied : app/revanced/manager/plugin/downloader/UserInteractionException { +} + +public final class app/revanced/manager/plugin/downloader/webview/APIKt { + public static final fun WebViewDownloader (Lkotlin/jvm/functions/Function4;)Lapp/revanced/manager/plugin/downloader/DownloaderBuilder; + public static final fun runWebView (Lapp/revanced/manager/plugin/downloader/GetScope;Ljava/lang/String;Lkotlin/jvm/functions/Function2;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; +} + +public class app/revanced/manager/plugin/downloader/webview/IWebView$Default : app/revanced/manager/plugin/downloader/webview/IWebView { + public fun <init> ()V + public fun asBinder ()Landroid/os/IBinder; + public fun finish ()V + public fun load (Ljava/lang/String;)V +} + +public abstract class app/revanced/manager/plugin/downloader/webview/IWebView$Stub : android/os/Binder, app/revanced/manager/plugin/downloader/webview/IWebView { + public fun <init> ()V + public fun asBinder ()Landroid/os/IBinder; + public static fun asInterface (Landroid/os/IBinder;)Lapp/revanced/manager/plugin/downloader/webview/IWebView; + public fun onTransact (ILandroid/os/Parcel;Landroid/os/Parcel;I)Z +} + +public class app/revanced/manager/plugin/downloader/webview/IWebViewEvents$Default : app/revanced/manager/plugin/downloader/webview/IWebViewEvents { + public fun <init> ()V + public fun asBinder ()Landroid/os/IBinder; + public fun download (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)V + public fun pageLoad (Ljava/lang/String;)V + public fun ready (Lapp/revanced/manager/plugin/downloader/webview/IWebView;)V +} + +public abstract class app/revanced/manager/plugin/downloader/webview/IWebViewEvents$Stub : android/os/Binder, app/revanced/manager/plugin/downloader/webview/IWebViewEvents { + public fun <init> ()V + public fun asBinder ()Landroid/os/IBinder; + public static fun asInterface (Landroid/os/IBinder;)Lapp/revanced/manager/plugin/downloader/webview/IWebViewEvents; + public fun onTransact (ILandroid/os/Parcel;Landroid/os/Parcel;I)Z +} + +public final class app/revanced/manager/plugin/downloader/webview/WebViewActivity$Parameters$Creator : android/os/Parcelable$Creator { + public fun <init> ()V + public final fun createFromParcel (Landroid/os/Parcel;)Lapp/revanced/manager/plugin/downloader/webview/WebViewActivity$Parameters; + public synthetic fun createFromParcel (Landroid/os/Parcel;)Ljava/lang/Object; + public final fun newArray (I)[Lapp/revanced/manager/plugin/downloader/webview/WebViewActivity$Parameters; + public synthetic fun newArray (I)[Ljava/lang/Object; +} + +public abstract interface class app/revanced/manager/plugin/downloader/webview/WebViewCallbackScope : app/revanced/manager/plugin/downloader/Scope { + public abstract fun finish (Ljava/lang/Object;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public abstract fun load (Ljava/lang/String;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; +} + +public final class app/revanced/manager/plugin/downloader/webview/WebViewScope : app/revanced/manager/plugin/downloader/Scope { + public final fun download (Lkotlin/jvm/functions/Function5;)V + public fun getHostPackageName ()Ljava/lang/String; + public fun getPluginPackageName ()Ljava/lang/String; + public final fun pageLoad (Lkotlin/jvm/functions/Function3;)V +} + diff --git a/downloader-plugin/build.gradle.kts b/downloader-plugin/build.gradle.kts new file mode 100644 index 0000000000..9d66a6e01c --- /dev/null +++ b/downloader-plugin/build.gradle.kts @@ -0,0 +1,61 @@ +plugins { + alias(libs.plugins.android.library) + alias(libs.plugins.kotlin.android) + alias(libs.plugins.kotlin.parcelize) + `maven-publish` +} + +android { + namespace = "app.revanced.manager.plugin.downloader" + compileSdk = 35 + + defaultConfig { + minSdk = 26 + + consumerProguardFiles("consumer-rules.pro") + } + + buildTypes { + release { + isMinifyEnabled = false + proguardFiles( + getDefaultProguardFile("proguard-android-optimize.txt"), + "proguard-rules.pro" + ) + } + } + compileOptions { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 + } + kotlinOptions { + jvmTarget = "17" + } + buildFeatures { + aidl = true + } +} +dependencies { + implementation(libs.androidx.ktx) + implementation(libs.activity.ktx) + implementation(libs.runtime.ktx) + implementation(libs.appcompat) +} + +publishing { + repositories { + mavenLocal() + } + + publications { + create<MavenPublication>("release") { + groupId = "app.revanced" + artifactId = "manager-downloader-plugin" + version = "1.0" + + afterEvaluate { + from(components["release"]) + } + } + } +} \ No newline at end of file diff --git a/downloader-plugin/consumer-rules.pro b/downloader-plugin/consumer-rules.pro new file mode 100644 index 0000000000..e69de29bb2 diff --git a/downloader-plugin/proguard-rules.pro b/downloader-plugin/proguard-rules.pro new file mode 100644 index 0000000000..481bb43481 --- /dev/null +++ b/downloader-plugin/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# 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 *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/downloader-plugin/src/main/AndroidManifest.xml b/downloader-plugin/src/main/AndroidManifest.xml new file mode 100644 index 0000000000..74b7379f73 --- /dev/null +++ b/downloader-plugin/src/main/AndroidManifest.xml @@ -0,0 +1,3 @@ +<?xml version="1.0" encoding="utf-8"?> +<manifest xmlns:android="http://schemas.android.com/apk/res/android"> +</manifest> \ No newline at end of file diff --git a/downloader-plugin/src/main/aidl/app/revanced/manager/plugin/downloader/webview/IWebView.aidl b/downloader-plugin/src/main/aidl/app/revanced/manager/plugin/downloader/webview/IWebView.aidl new file mode 100644 index 0000000000..d657fcc3c3 --- /dev/null +++ b/downloader-plugin/src/main/aidl/app/revanced/manager/plugin/downloader/webview/IWebView.aidl @@ -0,0 +1,8 @@ +// IWebView.aidl +package app.revanced.manager.plugin.downloader.webview; + +@JavaPassthrough(annotation="@app.revanced.manager.plugin.downloader.PluginHostApi") +oneway interface IWebView { + void load(String url); + void finish(); +} \ No newline at end of file diff --git a/downloader-plugin/src/main/aidl/app/revanced/manager/plugin/downloader/webview/IWebViewEvents.aidl b/downloader-plugin/src/main/aidl/app/revanced/manager/plugin/downloader/webview/IWebViewEvents.aidl new file mode 100644 index 0000000000..b0237de2a7 --- /dev/null +++ b/downloader-plugin/src/main/aidl/app/revanced/manager/plugin/downloader/webview/IWebViewEvents.aidl @@ -0,0 +1,11 @@ +// IWebViewEvents.aidl +package app.revanced.manager.plugin.downloader.webview; + +import app.revanced.manager.plugin.downloader.webview.IWebView; + +@JavaPassthrough(annotation="@app.revanced.manager.plugin.downloader.PluginHostApi") +oneway interface IWebViewEvents { + void ready(IWebView iface); + void pageLoad(String url); + void download(String url, String mimetype, String userAgent); +} \ No newline at end of file diff --git a/downloader-plugin/src/main/java/app/revanced/manager/plugin/downloader/Constants.kt b/downloader-plugin/src/main/java/app/revanced/manager/plugin/downloader/Constants.kt new file mode 100644 index 0000000000..469daaaec3 --- /dev/null +++ b/downloader-plugin/src/main/java/app/revanced/manager/plugin/downloader/Constants.kt @@ -0,0 +1,7 @@ +package app.revanced.manager.plugin.downloader + +/** + * The permission ID of the special plugin host permission. Only ReVanced Manager will have this permission. + * Plugin UI activities and internal services can be protected using this permission. + */ +const val PLUGIN_HOST_PERMISSION = "app.revanced.manager.permission.PLUGIN_HOST" \ No newline at end of file diff --git a/downloader-plugin/src/main/java/app/revanced/manager/plugin/downloader/Downloader.kt b/downloader-plugin/src/main/java/app/revanced/manager/plugin/downloader/Downloader.kt new file mode 100644 index 0000000000..bf0a219b58 --- /dev/null +++ b/downloader-plugin/src/main/java/app/revanced/manager/plugin/downloader/Downloader.kt @@ -0,0 +1,165 @@ +package app.revanced.manager.plugin.downloader + +import android.content.ComponentName +import android.content.Context +import android.content.Intent +import android.content.ServiceConnection +import android.os.IBinder +import android.app.Activity +import android.os.Parcelable +import kotlinx.coroutines.withTimeout +import java.io.InputStream +import java.io.OutputStream +import kotlin.coroutines.resume +import kotlin.coroutines.suspendCoroutine + +@RequiresOptIn( + level = RequiresOptIn.Level.ERROR, + message = "This API is only intended for plugin hosts, don't use it in a plugin.", +) +@Retention(AnnotationRetention.BINARY) +annotation class PluginHostApi + +/** + * The base interface for all DSL scopes. + */ +interface Scope { + /** + * The package name of ReVanced Manager. + */ + val hostPackageName: String + + /** + * The package name of the plugin. + */ + val pluginPackageName: String +} + +/** + * The scope of [DownloaderScope.get]. + */ +interface GetScope : Scope { + /** + * Ask the user to perform some required interaction in the activity specified by the provided [Intent]. + * This function returns normally with the resulting [Intent] when the activity finishes with code [Activity.RESULT_OK]. + * + * @throws UserInteractionException.RequestDenied User decided to skip this plugin. + * @throws UserInteractionException.Activity.Cancelled The activity was cancelled. + * @throws UserInteractionException.Activity.NotCompleted The activity finished with an unknown result code. + */ + suspend fun requestStartActivity(intent: Intent): Intent? +} + +interface BaseDownloadScope : Scope + +/** + * The scope for [DownloaderScope.download]. + */ +interface InputDownloadScope : BaseDownloadScope + +typealias Size = Long +typealias DownloadResult = Pair<InputStream, Size?> + +typealias Version = String +typealias GetResult<T> = Pair<T, Version?> + +class DownloaderScope<T : Parcelable> internal constructor( + private val scopeImpl: Scope, + internal val context: Context +) : Scope by scopeImpl { + // Returning an InputStream is the primary way for plugins to implement the download function, but we also want to offer an OutputStream API since using InputStream might not be convenient in all cases. + // It is much easier to implement the main InputStream API on top of OutputStreams compared to doing it the other way around, which is why we are using OutputStream here. This detail is not visible to plugins. + internal var download: (suspend OutputDownloadScope.(T, OutputStream) -> Unit)? = null + internal var get: (suspend GetScope.(String, String?) -> GetResult<T>?)? = null + private val inputDownloadScopeImpl = object : InputDownloadScope, Scope by scopeImpl {} + + /** + * Define the download block of the plugin. + */ + fun download(block: suspend InputDownloadScope.(data: T) -> DownloadResult) { + download = { app, outputStream -> + val (inputStream, size) = inputDownloadScopeImpl.block(app) + + inputStream.use { + if (size != null) reportSize(size) + it.copyTo(outputStream) + } + } + } + + /** + * Define the get block of the plugin. + * The block should return null if the app cannot be found. The version in the result must match the version argument unless it is null. + */ + fun get(block: suspend GetScope.(packageName: String, version: String?) -> GetResult<T>?) { + get = block + } + + /** + * Utilize the service specified by the provided [Intent]. The service will be unbound when the scope ends. + */ + suspend fun <R : Any?> useService(intent: Intent, block: suspend (IBinder) -> R): R { + var onBind: ((IBinder) -> Unit)? = null + val serviceConn = object : ServiceConnection { + override fun onServiceConnected(name: ComponentName?, service: IBinder?) = + onBind!!(service!!) + + override fun onServiceDisconnected(name: ComponentName?) {} + } + + return try { + val binder = withTimeout(10000L) { + suspendCoroutine { continuation -> + onBind = continuation::resume + context.bindService(intent, serviceConn, Context.BIND_AUTO_CREATE) + } + } + block(binder) + } finally { + onBind = null + context.unbindService(serviceConn) + } + } +} + +class DownloaderBuilder<T : Parcelable> internal constructor(private val block: DownloaderScope<T>.() -> Unit) { + @PluginHostApi + fun build(scopeImpl: Scope, context: Context) = + with(DownloaderScope<T>(scopeImpl, context)) { + block() + + Downloader( + download = download!!, + get = get!! + ) + } +} + +class Downloader<T : Parcelable> internal constructor( + @property:PluginHostApi val get: suspend GetScope.(packageName: String, version: String?) -> GetResult<T>?, + @property:PluginHostApi val download: suspend OutputDownloadScope.(data: T, outputStream: OutputStream) -> Unit +) + +/** + * Define a downloader plugin. + */ +fun <T : Parcelable> Downloader(block: DownloaderScope<T>.() -> Unit) = DownloaderBuilder(block) + +/** + * @see GetScope.requestStartActivity + */ +sealed class UserInteractionException(message: String) : Exception(message) { + class RequestDenied @PluginHostApi constructor() : + UserInteractionException("Request denied by user") + + sealed class Activity(message: String) : UserInteractionException(message) { + class Cancelled @PluginHostApi constructor() : Activity("Interaction cancelled") + + /** + * @param resultCode The result code of the activity. + * @param intent The [Intent] of the activity. + */ + class NotCompleted @PluginHostApi constructor(val resultCode: Int, val intent: Intent?) : + Activity("Unexpected activity result code: $resultCode") + } +} \ No newline at end of file diff --git a/downloader-plugin/src/main/java/app/revanced/manager/plugin/downloader/Extensions.kt b/downloader-plugin/src/main/java/app/revanced/manager/plugin/downloader/Extensions.kt new file mode 100644 index 0000000000..a1e6bf795b --- /dev/null +++ b/downloader-plugin/src/main/java/app/revanced/manager/plugin/downloader/Extensions.kt @@ -0,0 +1,42 @@ +package app.revanced.manager.plugin.downloader + +import android.app.Activity +import android.app.Service +import android.content.Intent +import android.os.IBinder +import android.os.Parcelable +import java.io.OutputStream + +/** + * The scope of the [OutputStream] version of [DownloaderScope.download]. + */ +interface OutputDownloadScope : BaseDownloadScope { + suspend fun reportSize(size: Long) +} + +/** + * A replacement for [DownloaderScope.download] that uses [OutputStream]. + * The provided [OutputStream] does not need to be closed manually. + */ +fun <T : Parcelable> DownloaderScope<T>.download(block: suspend OutputDownloadScope.(T, OutputStream) -> Unit) { + download = block +} + +/** + * Performs [GetScope.requestStartActivity] with an [Intent] created using the type information of [ACTIVITY]. + * @see [GetScope.requestStartActivity] + */ +suspend inline fun <reified ACTIVITY : Activity> GetScope.requestStartActivity() = + requestStartActivity( + Intent().apply { setClassName(pluginPackageName, ACTIVITY::class.qualifiedName!!) } + ) + +/** + * Performs [DownloaderScope.useService] with an [Intent] created using the type information of [SERVICE]. + * @see [DownloaderScope.useService] + */ +suspend inline fun <reified SERVICE : Service, R : Any?> DownloaderScope<*>.useService( + noinline block: suspend (IBinder) -> R +) = useService( + Intent().apply { setClassName(pluginPackageName, SERVICE::class.qualifiedName!!) }, block +) \ No newline at end of file diff --git a/downloader-plugin/src/main/java/app/revanced/manager/plugin/downloader/Parcelables.kt b/downloader-plugin/src/main/java/app/revanced/manager/plugin/downloader/Parcelables.kt new file mode 100644 index 0000000000..414ad88947 --- /dev/null +++ b/downloader-plugin/src/main/java/app/revanced/manager/plugin/downloader/Parcelables.kt @@ -0,0 +1,39 @@ +package app.revanced.manager.plugin.downloader + +import android.os.Parcelable +import kotlinx.parcelize.Parcelize +import java.net.HttpURLConnection +import java.net.URI + +/** + * A simple parcelable data class for storing a package name and version. + * This can be used as the data type for plugins that only need a name and version to implement their [DownloaderScope.download] function. + * + * @param name The package name. + * @param version The version. + */ +@Parcelize +data class Package(val name: String, val version: String) : Parcelable + +/** + * A data class for storing a download URL. + * + * @param url The download URL. + * @param headers The headers to use for the request. + */ +@Parcelize +data class DownloadUrl(val url: String, val headers: Map<String, String> = emptyMap()) : Parcelable { + /** + * Converts this into a [DownloadResult]. + */ + fun toDownloadResult(): DownloadResult = with(URI.create(url).toURL().openConnection() as HttpURLConnection) { + useCaches = false + allowUserInteraction = false + headers.forEach(::setRequestProperty) + + connectTimeout = 10_000 + connect() + + inputStream to getHeaderField("Content-Length").toLong() + } +} \ No newline at end of file diff --git a/downloader-plugin/src/main/java/app/revanced/manager/plugin/downloader/webview/API.kt b/downloader-plugin/src/main/java/app/revanced/manager/plugin/downloader/webview/API.kt new file mode 100644 index 0000000000..2e5034e189 --- /dev/null +++ b/downloader-plugin/src/main/java/app/revanced/manager/plugin/downloader/webview/API.kt @@ -0,0 +1,176 @@ +package app.revanced.manager.plugin.downloader.webview + +import android.content.Intent +import app.revanced.manager.plugin.downloader.DownloadUrl +import app.revanced.manager.plugin.downloader.DownloaderScope +import app.revanced.manager.plugin.downloader.GetScope +import app.revanced.manager.plugin.downloader.Scope +import app.revanced.manager.plugin.downloader.Downloader +import app.revanced.manager.plugin.downloader.PluginHostApi +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.cancelChildren +import kotlinx.coroutines.launch +import kotlinx.coroutines.supervisorScope +import kotlinx.coroutines.withContext +import kotlin.properties.Delegates + +typealias InitialUrl = String +typealias PageLoadCallback<T> = suspend WebViewCallbackScope<T>.(url: String) -> Unit +typealias DownloadCallback<T> = suspend WebViewCallbackScope<T>.(url: String, mimeType: String, userAgent: String) -> Unit + +interface WebViewCallbackScope<T> : Scope { + /** + * Finishes the activity and returns the [result]. + */ + suspend fun finish(result: T) + + /** + * Tells the WebView to load the specified [url]. + */ + suspend fun load(url: String) +} + +@OptIn(PluginHostApi::class) +class WebViewScope<T> internal constructor( + coroutineScope: CoroutineScope, + private val scopeImpl: Scope, + setResult: (T) -> Unit +) : Scope by scopeImpl { + private var onPageLoadCallback: PageLoadCallback<T> = {} + private var onDownloadCallback: DownloadCallback<T> = { _, _, _ -> } + + @OptIn(ExperimentalCoroutinesApi::class) + private val dispatcher = Dispatchers.Default.limitedParallelism(1) + private lateinit var webView: IWebView + internal lateinit var initialUrl: String + + internal val binder = object : IWebViewEvents.Stub() { + override fun ready(iface: IWebView?) { + coroutineScope.launch(dispatcher) { + webView = iface!!.also { + it.load(initialUrl) + } + } + } + + override fun pageLoad(url: String?) { + coroutineScope.launch(dispatcher) { onPageLoadCallback(callbackScope, url!!) } + } + + override fun download(url: String?, mimetype: String?, userAgent: String?) { + coroutineScope.launch(dispatcher) { + onDownloadCallback( + callbackScope, + url!!, + mimetype!!, + userAgent!! + ) + } + } + } + + private val callbackScope = object : WebViewCallbackScope<T>, Scope by scopeImpl { + override suspend fun finish(result: T) { + setResult(result) + // Tell the WebViewActivity to finish + webView.let { withContext(Dispatchers.IO) { it.finish() } } + } + + override suspend fun load(url: String) { + webView.let { withContext(Dispatchers.IO) { it.load(url) } } + } + + } + + /** + * Called when the WebView attempts to download a file to disk. + */ + fun download(block: DownloadCallback<T>) { + onDownloadCallback = block + } + + /** + * Called when the WebView finishes loading a page. + */ + fun pageLoad(block: PageLoadCallback<T>) { + onPageLoadCallback = block + } +} + +@JvmInline +private value class Container<U>(val value: U) + +/** + * Run a [android.webkit.WebView] Activity controlled by the provided code block. + * The activity will keep running until it is cancelled or an event handler calls [WebViewCallbackScope.finish]. + * The [block] defines the event handlers and returns the initial URL. + * + * @param title The string displayed in the action bar. + * @param block The control block. + */ +@OptIn(PluginHostApi::class) +suspend fun <T> GetScope.runWebView( + title: String, + block: suspend WebViewScope<T>.() -> InitialUrl +) = supervisorScope { + var result by Delegates.notNull<Container<T>>() + + val scope = WebViewScope<T>(this@supervisorScope, this@runWebView) { result = Container(it) } + scope.initialUrl = scope.block() + + // Start the webview activity and wait until it finishes. + requestStartActivity(Intent().apply { + putExtra( + WebViewActivity.KEY, + WebViewActivity.Parameters(title, scope.binder) + ) + setClassName( + hostPackageName, + WebViewActivity::class.qualifiedName!! + ) + }) + + // Return the result and cancel any leftover coroutines. + coroutineContext.cancelChildren() + result.value +} + +/** + * Implement a downloader using [runWebView] and [DownloadUrl]. This function will automatically define a handler for download events unlike [runWebView]. + * Returning null inside the [block] is equivalent to returning null inside [DownloaderScope.get]. + * + * @see runWebView + */ +fun WebViewDownloader(block: suspend WebViewScope<DownloadUrl>.(packageName: String, version: String?) -> InitialUrl?) = + Downloader<DownloadUrl> { + val label = context.applicationInfo.loadLabel( + context.packageManager + ).toString() + + get { packageName, version -> + class ReturnNull : Exception() + + try { + runWebView(label) { + download { url, _, userAgent -> + finish( + DownloadUrl( + url, + mapOf("User-Agent" to userAgent) + ) + ) + } + + block(this@runWebView, packageName, version) ?: throw ReturnNull() + } to version + } catch (_: ReturnNull) { + null + } + } + + download { + it.toDownloadResult() + } + } \ No newline at end of file diff --git a/downloader-plugin/src/main/java/app/revanced/manager/plugin/downloader/webview/WebViewActivity.kt b/downloader-plugin/src/main/java/app/revanced/manager/plugin/downloader/webview/WebViewActivity.kt new file mode 100644 index 0000000000..aff0133784 --- /dev/null +++ b/downloader-plugin/src/main/java/app/revanced/manager/plugin/downloader/webview/WebViewActivity.kt @@ -0,0 +1,161 @@ +package app.revanced.manager.plugin.downloader.webview + +import android.annotation.SuppressLint +import android.os.Bundle +import android.os.IBinder +import android.os.Parcelable +import android.view.MenuItem +import android.webkit.CookieManager +import android.webkit.WebSettings +import android.webkit.WebView +import android.webkit.WebViewClient +import androidx.activity.ComponentActivity +import androidx.activity.addCallback +import androidx.activity.enableEdgeToEdge +import androidx.activity.viewModels +import androidx.core.view.ViewCompat +import androidx.core.view.WindowInsetsCompat +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.ViewModel +import androidx.lifecycle.lifecycleScope +import androidx.lifecycle.repeatOnLifecycle +import androidx.lifecycle.viewModelScope +import app.revanced.manager.plugin.downloader.PluginHostApi +import app.revanced.manager.plugin.downloader.R +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.flow.receiveAsFlow +import kotlinx.coroutines.launch +import kotlinx.parcelize.Parcelize + +@OptIn(PluginHostApi::class) +@PluginHostApi +class WebViewActivity : ComponentActivity() { + @SuppressLint("SetJavaScriptEnabled") + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + val vm by viewModels<WebViewModel>() + enableEdgeToEdge() + setContentView(R.layout.activity_webview) + + ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main)) { v, insets -> + val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars()) + v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom) + insets + } + val webView = findViewById<WebView>(R.id.webview) + onBackPressedDispatcher.addCallback { + if (webView.canGoBack()) webView.goBack() + else cancelActivity() + } + + val params = intent.getParcelableExtra<Parameters>(KEY)!! + actionBar?.apply { + title = params.title + setHomeAsUpIndicator(android.R.drawable.ic_menu_close_clear_cancel) + setDisplayHomeAsUpEnabled(true) + } + + val events = IWebViewEvents.Stub.asInterface(params.events)!! + vm.setup(events) + + webView.apply { + settings.apply { + cacheMode = WebSettings.LOAD_NO_CACHE + allowContentAccess = false + domStorageEnabled = true + javaScriptEnabled = true + } + + webViewClient = vm.webViewClient + setDownloadListener { url, userAgent, _, mimetype, _ -> + vm.onDownload(url, mimetype, userAgent) + } + } + + lifecycleScope.launch { + repeatOnLifecycle(Lifecycle.State.STARTED) { + vm.commands.collect { + when (it) { + is WebViewModel.Command.Finish -> { + setResult(RESULT_OK) + finish() + } + + is WebViewModel.Command.Load -> webView.loadUrl(it.url) + } + } + } + } + } + + private fun cancelActivity() { + setResult(RESULT_CANCELED) + finish() + } + + override fun onOptionsItemSelected(item: MenuItem) = if (item.itemId == android.R.id.home) { + cancelActivity() + + true + } else super.onOptionsItemSelected(item) + + @Parcelize + internal class Parameters( + val title: String, val events: IBinder + ) : Parcelable + + internal companion object { + const val KEY = "params" + } +} + +@OptIn(PluginHostApi::class) +internal class WebViewModel : ViewModel() { + init { + CookieManager.getInstance().apply { + removeAllCookies(null) + setAcceptCookie(true) + } + } + + private val commandChannel = Channel<Command>() + val commands = commandChannel.receiveAsFlow() + + private var eventBinder: IWebViewEvents? = null + private val ctrlBinder = object : IWebView.Stub() { + override fun load(url: String?) { + viewModelScope.launch { + commandChannel.send(Command.Load(url!!)) + } + } + + override fun finish() { + viewModelScope.launch { + commandChannel.send(Command.Finish) + } + } + } + + val webViewClient = object : WebViewClient() { + override fun onPageFinished(view: WebView?, url: String?) { + super.onPageFinished(view, url) + eventBinder!!.pageLoad(url) + } + } + + fun onDownload(url: String, mimeType: String, userAgent: String) { + eventBinder!!.download(url, mimeType, userAgent) + } + + fun setup(binder: IWebViewEvents) { + if (eventBinder != null) return + eventBinder = binder + binder.ready(ctrlBinder) + } + + sealed interface Command { + data class Load(val url: String) : Command + data object Finish : Command + } +} \ No newline at end of file diff --git a/downloader-plugin/src/main/res/layout/activity_webview.xml b/downloader-plugin/src/main/res/layout/activity_webview.xml new file mode 100644 index 0000000000..51f761d993 --- /dev/null +++ b/downloader-plugin/src/main/res/layout/activity_webview.xml @@ -0,0 +1,11 @@ +<?xml version="1.0" encoding="utf-8"?> +<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:id="@+id/main"> + + <WebView + android:id="@+id/webview" + android:layout_width="match_parent" + android:layout_height="match_parent" /> +</LinearLayout> \ No newline at end of file diff --git a/downloader-plugin/src/main/res/values/strings.xml b/downloader-plugin/src/main/res/values/strings.xml new file mode 100644 index 0000000000..73862c416f --- /dev/null +++ b/downloader-plugin/src/main/res/values/strings.xml @@ -0,0 +1 @@ +<resources></resources> \ No newline at end of file diff --git a/downloader-plugin/src/main/res/values/themes.xml b/downloader-plugin/src/main/res/values/themes.xml new file mode 100644 index 0000000000..495cde8e34 --- /dev/null +++ b/downloader-plugin/src/main/res/values/themes.xml @@ -0,0 +1,7 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <style name="Theme.WebViewActivity" parent="Theme.AppCompat.DayNight"> + <item name="android:windowActionBar">true</item> + <item name="android:windowNoTitle">false</item> + </style> +</resources> \ No newline at end of file diff --git a/example-downloader-plugin/.gitignore b/example-downloader-plugin/.gitignore new file mode 100644 index 0000000000..42afabfd2a --- /dev/null +++ b/example-downloader-plugin/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/example-downloader-plugin/build.gradle.kts b/example-downloader-plugin/build.gradle.kts new file mode 100644 index 0000000000..b480add929 --- /dev/null +++ b/example-downloader-plugin/build.gradle.kts @@ -0,0 +1,53 @@ +plugins { + alias(libs.plugins.android.application) + alias(libs.plugins.kotlin.android) + alias(libs.plugins.kotlin.parcelize) + alias(libs.plugins.compose.compiler) +} + +android { + val packageName = "app.revanced.manager.plugin.downloader.example" + + namespace = packageName + compileSdk = 35 + + defaultConfig { + applicationId = packageName + minSdk = 26 + targetSdk = 35 + versionCode = 1 + versionName = "1.0" + } + + buildTypes { + release { + isMinifyEnabled = false + proguardFiles( + getDefaultProguardFile("proguard-android-optimize.txt"), + "proguard-rules.pro" + ) + + if (project.hasProperty("signAsDebug")) { + signingConfig = signingConfigs.getByName("debug") + } + } + } + compileOptions { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 + } + kotlinOptions { + jvmTarget = "17" + } + buildFeatures.compose = true +} + +dependencies { + implementation(libs.activity.compose) + implementation(platform(libs.compose.bom)) + implementation(libs.compose.ui) + implementation(libs.compose.ui.tooling) + implementation(libs.compose.material3) + + compileOnly(project(":downloader-plugin")) +} \ No newline at end of file diff --git a/example-downloader-plugin/proguard-rules.pro b/example-downloader-plugin/proguard-rules.pro new file mode 100644 index 0000000000..481bb43481 --- /dev/null +++ b/example-downloader-plugin/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# 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 *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/example-downloader-plugin/src/main/AndroidManifest.xml b/example-downloader-plugin/src/main/AndroidManifest.xml new file mode 100644 index 0000000000..e10b2d28b4 --- /dev/null +++ b/example-downloader-plugin/src/main/AndroidManifest.xml @@ -0,0 +1,23 @@ +<?xml version="1.0" encoding="utf-8"?> +<manifest xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:tools="http://schemas.android.com/tools"> + + <uses-feature android:name="app.revanced.manager.plugin.downloader" /> + + <application + android:icon="@mipmap/ic_launcher" + android:label="@string/app_name" + tools:targetApi="34"> + + <activity + android:name=".InteractionActivity" + android:exported="true" + android:permission="app.revanced.manager.permission.PLUGIN_HOST" + android:theme="@android:style/Theme.DeviceDefault" /> + + <meta-data + android:name="app.revanced.manager.plugin.downloader.class" + android:value="app.revanced.manager.plugin.downloader.example.ExamplePluginKt" /> + </application> + +</manifest> \ No newline at end of file diff --git a/example-downloader-plugin/src/main/java/app/revanced/manager/plugin/downloader/example/ExamplePlugin.kt b/example-downloader-plugin/src/main/java/app/revanced/manager/plugin/downloader/example/ExamplePlugin.kt new file mode 100644 index 0000000000..dd2b26c5e3 --- /dev/null +++ b/example-downloader-plugin/src/main/java/app/revanced/manager/plugin/downloader/example/ExamplePlugin.kt @@ -0,0 +1,69 @@ +@file:Suppress("Unused") + +package app.revanced.manager.plugin.downloader.example + +import android.app.Application +import android.content.pm.PackageManager +import android.net.Uri +import android.os.Parcelable +import app.revanced.manager.plugin.downloader.Downloader +import app.revanced.manager.plugin.downloader.requestStartActivity +import app.revanced.manager.plugin.downloader.webview.WebViewDownloader +import kotlinx.parcelize.Parcelize +import kotlin.io.path.* + +val apkMirrorDownloader = WebViewDownloader { packageName, version -> + with(Uri.Builder()) { + scheme("https") + authority("www.apkmirror.com") + mapOf( + "post_type" to "app_release", + "searchtype" to "apk", + "s" to (version?.let { "$packageName $it" } ?: packageName), + "bundles%5B%5D" to "apk_files" // bundles[] + ).forEach { (key, value) -> + appendQueryParameter(key, value) + } + + build().toString() + } +} + +@Parcelize +class InstalledApp(val path: String) : Parcelable + +private val application by lazy { + // Don't do this in a real plugin. + val clazz = Class.forName("android.app.ActivityThread") + val activityThread = clazz.getMethod("currentActivityThread")(null) + clazz.getMethod("getApplication")(activityThread) as Application +} + +val installedAppDownloader = Downloader<InstalledApp> { + val pm = application.packageManager + + get { packageName, version -> + val packageInfo = try { + pm.getPackageInfo(packageName, 0) + } catch (_: PackageManager.NameNotFoundException) { + return@get null + } + if (version != null && packageInfo.versionName != version) return@get null + + requestStartActivity<InteractionActivity>() + + InstalledApp(packageInfo.applicationInfo!!.sourceDir) to packageInfo.versionName + } + + + download { app -> + with(Path(app.path)) { inputStream() to fileSize() } + } + + /* + download { app, outputStream -> + val path = Path(app.path) + reportSize(path.fileSize()) + Files.copy(path, outputStream) + }*/ +} diff --git a/example-downloader-plugin/src/main/java/app/revanced/manager/plugin/downloader/example/InteractionActivity.kt b/example-downloader-plugin/src/main/java/app/revanced/manager/plugin/downloader/example/InteractionActivity.kt new file mode 100644 index 0000000000..0390f3bd7a --- /dev/null +++ b/example-downloader-plugin/src/main/java/app/revanced/manager/plugin/downloader/example/InteractionActivity.kt @@ -0,0 +1,65 @@ +package app.revanced.manager.plugin.downloader.example + +import android.os.Bundle +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.material3.TopAppBar +import androidx.compose.material3.darkColorScheme +import androidx.compose.material3.lightColorScheme +import androidx.compose.ui.Modifier + +class InteractionActivity : ComponentActivity() { + @OptIn(ExperimentalMaterial3Api::class) + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + setContent { + val isDarkTheme = isSystemInDarkTheme() + + val colorScheme = if (isDarkTheme) darkColorScheme() else lightColorScheme() + + MaterialTheme(colorScheme) { + Scaffold( + topBar = { + TopAppBar( + title = { Text("User interaction example") } + ) + } + ) { paddingValues -> + Column(modifier = Modifier.padding(paddingValues)) { + Text("This is an example interaction.") + Row { + TextButton( + onClick = { + setResult(RESULT_CANCELED) + finish() + } + ) { + Text("Cancel") + } + + TextButton( + onClick = { + setResult(RESULT_OK) + finish() + } + ) { + Text("Continue") + } + } + } + } + } + + } + } +} \ No newline at end of file diff --git a/example-downloader-plugin/src/main/res/drawable/ic_launcher_background.xml b/example-downloader-plugin/src/main/res/drawable/ic_launcher_background.xml new file mode 100644 index 0000000000..07d5da9cbf --- /dev/null +++ b/example-downloader-plugin/src/main/res/drawable/ic_launcher_background.xml @@ -0,0 +1,170 @@ +<?xml version="1.0" encoding="utf-8"?> +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="108dp" + android:height="108dp" + android:viewportWidth="108" + android:viewportHeight="108"> + <path + android:fillColor="#3DDC84" + android:pathData="M0,0h108v108h-108z" /> + <path + android:fillColor="#00000000" + android:pathData="M9,0L9,108" + android:strokeWidth="0.8" + android:strokeColor="#33FFFFFF" /> + <path + android:fillColor="#00000000" + android:pathData="M19,0L19,108" + android:strokeWidth="0.8" + android:strokeColor="#33FFFFFF" /> + <path + android:fillColor="#00000000" + android:pathData="M29,0L29,108" + android:strokeWidth="0.8" + android:strokeColor="#33FFFFFF" /> + <path + android:fillColor="#00000000" + android:pathData="M39,0L39,108" + android:strokeWidth="0.8" + android:strokeColor="#33FFFFFF" /> + <path + android:fillColor="#00000000" + android:pathData="M49,0L49,108" + android:strokeWidth="0.8" + android:strokeColor="#33FFFFFF" /> + <path + android:fillColor="#00000000" + android:pathData="M59,0L59,108" + android:strokeWidth="0.8" + android:strokeColor="#33FFFFFF" /> + <path + android:fillColor="#00000000" + android:pathData="M69,0L69,108" + android:strokeWidth="0.8" + android:strokeColor="#33FFFFFF" /> + <path + android:fillColor="#00000000" + android:pathData="M79,0L79,108" + android:strokeWidth="0.8" + android:strokeColor="#33FFFFFF" /> + <path + android:fillColor="#00000000" + android:pathData="M89,0L89,108" + android:strokeWidth="0.8" + android:strokeColor="#33FFFFFF" /> + <path + android:fillColor="#00000000" + android:pathData="M99,0L99,108" + android:strokeWidth="0.8" + android:strokeColor="#33FFFFFF" /> + <path + android:fillColor="#00000000" + android:pathData="M0,9L108,9" + android:strokeWidth="0.8" + android:strokeColor="#33FFFFFF" /> + <path + android:fillColor="#00000000" + android:pathData="M0,19L108,19" + android:strokeWidth="0.8" + android:strokeColor="#33FFFFFF" /> + <path + android:fillColor="#00000000" + android:pathData="M0,29L108,29" + android:strokeWidth="0.8" + android:strokeColor="#33FFFFFF" /> + <path + android:fillColor="#00000000" + android:pathData="M0,39L108,39" + android:strokeWidth="0.8" + android:strokeColor="#33FFFFFF" /> + <path + android:fillColor="#00000000" + android:pathData="M0,49L108,49" + android:strokeWidth="0.8" + android:strokeColor="#33FFFFFF" /> + <path + android:fillColor="#00000000" + android:pathData="M0,59L108,59" + android:strokeWidth="0.8" + android:strokeColor="#33FFFFFF" /> + <path + android:fillColor="#00000000" + android:pathData="M0,69L108,69" + android:strokeWidth="0.8" + android:strokeColor="#33FFFFFF" /> + <path + android:fillColor="#00000000" + android:pathData="M0,79L108,79" + android:strokeWidth="0.8" + android:strokeColor="#33FFFFFF" /> + <path + android:fillColor="#00000000" + android:pathData="M0,89L108,89" + android:strokeWidth="0.8" + android:strokeColor="#33FFFFFF" /> + <path + android:fillColor="#00000000" + android:pathData="M0,99L108,99" + android:strokeWidth="0.8" + android:strokeColor="#33FFFFFF" /> + <path + android:fillColor="#00000000" + android:pathData="M19,29L89,29" + android:strokeWidth="0.8" + android:strokeColor="#33FFFFFF" /> + <path + android:fillColor="#00000000" + android:pathData="M19,39L89,39" + android:strokeWidth="0.8" + android:strokeColor="#33FFFFFF" /> + <path + android:fillColor="#00000000" + android:pathData="M19,49L89,49" + android:strokeWidth="0.8" + android:strokeColor="#33FFFFFF" /> + <path + android:fillColor="#00000000" + android:pathData="M19,59L89,59" + android:strokeWidth="0.8" + android:strokeColor="#33FFFFFF" /> + <path + android:fillColor="#00000000" + android:pathData="M19,69L89,69" + android:strokeWidth="0.8" + android:strokeColor="#33FFFFFF" /> + <path + android:fillColor="#00000000" + android:pathData="M19,79L89,79" + android:strokeWidth="0.8" + android:strokeColor="#33FFFFFF" /> + <path + android:fillColor="#00000000" + android:pathData="M29,19L29,89" + android:strokeWidth="0.8" + android:strokeColor="#33FFFFFF" /> + <path + android:fillColor="#00000000" + android:pathData="M39,19L39,89" + android:strokeWidth="0.8" + android:strokeColor="#33FFFFFF" /> + <path + android:fillColor="#00000000" + android:pathData="M49,19L49,89" + android:strokeWidth="0.8" + android:strokeColor="#33FFFFFF" /> + <path + android:fillColor="#00000000" + android:pathData="M59,19L59,89" + android:strokeWidth="0.8" + android:strokeColor="#33FFFFFF" /> + <path + android:fillColor="#00000000" + android:pathData="M69,19L69,89" + android:strokeWidth="0.8" + android:strokeColor="#33FFFFFF" /> + <path + android:fillColor="#00000000" + android:pathData="M79,19L79,89" + android:strokeWidth="0.8" + android:strokeColor="#33FFFFFF" /> +</vector> diff --git a/example-downloader-plugin/src/main/res/drawable/ic_launcher_foreground.xml b/example-downloader-plugin/src/main/res/drawable/ic_launcher_foreground.xml new file mode 100644 index 0000000000..2b068d1146 --- /dev/null +++ b/example-downloader-plugin/src/main/res/drawable/ic_launcher_foreground.xml @@ -0,0 +1,30 @@ +<vector xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:aapt="http://schemas.android.com/aapt" + android:width="108dp" + android:height="108dp" + android:viewportWidth="108" + android:viewportHeight="108"> + <path android:pathData="M31,63.928c0,0 6.4,-11 12.1,-13.1c7.2,-2.6 26,-1.4 26,-1.4l38.1,38.1L107,108.928l-32,-1L31,63.928z"> + <aapt:attr name="android:fillColor"> + <gradient + android:endX="85.84757" + android:endY="92.4963" + android:startX="42.9492" + android:startY="49.59793" + android:type="linear"> + <item + android:color="#44000000" + android:offset="0.0" /> + <item + android:color="#00000000" + android:offset="1.0" /> + </gradient> + </aapt:attr> + </path> + <path + android:fillColor="#FFFFFF" + android:fillType="nonZero" + android:pathData="M65.3,45.828l3.8,-6.6c0.2,-0.4 0.1,-0.9 -0.3,-1.1c-0.4,-0.2 -0.9,-0.1 -1.1,0.3l-3.9,6.7c-6.3,-2.8 -13.4,-2.8 -19.7,0l-3.9,-6.7c-0.2,-0.4 -0.7,-0.5 -1.1,-0.3C38.8,38.328 38.7,38.828 38.9,39.228l3.8,6.6C36.2,49.428 31.7,56.028 31,63.928h46C76.3,56.028 71.8,49.428 65.3,45.828zM43.4,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2c-0.3,-0.7 -0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C45.3,56.528 44.5,57.328 43.4,57.328L43.4,57.328zM64.6,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2s-0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C66.5,56.528 65.6,57.328 64.6,57.328L64.6,57.328z" + android:strokeWidth="1" + android:strokeColor="#00000000" /> +</vector> \ No newline at end of file diff --git a/example-downloader-plugin/src/main/res/mipmap-anydpi/ic_launcher.xml b/example-downloader-plugin/src/main/res/mipmap-anydpi/ic_launcher.xml new file mode 100644 index 0000000000..6f3b755bf5 --- /dev/null +++ b/example-downloader-plugin/src/main/res/mipmap-anydpi/ic_launcher.xml @@ -0,0 +1,6 @@ +<?xml version="1.0" encoding="utf-8"?> +<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android"> + <background android:drawable="@drawable/ic_launcher_background" /> + <foreground android:drawable="@drawable/ic_launcher_foreground" /> + <monochrome android:drawable="@drawable/ic_launcher_foreground" /> +</adaptive-icon> \ No newline at end of file diff --git a/example-downloader-plugin/src/main/res/mipmap-anydpi/ic_launcher_round.xml b/example-downloader-plugin/src/main/res/mipmap-anydpi/ic_launcher_round.xml new file mode 100644 index 0000000000..6f3b755bf5 --- /dev/null +++ b/example-downloader-plugin/src/main/res/mipmap-anydpi/ic_launcher_round.xml @@ -0,0 +1,6 @@ +<?xml version="1.0" encoding="utf-8"?> +<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android"> + <background android:drawable="@drawable/ic_launcher_background" /> + <foreground android:drawable="@drawable/ic_launcher_foreground" /> + <monochrome android:drawable="@drawable/ic_launcher_foreground" /> +</adaptive-icon> \ No newline at end of file diff --git a/example-downloader-plugin/src/main/res/values/strings.xml b/example-downloader-plugin/src/main/res/values/strings.xml new file mode 100644 index 0000000000..4006549c02 --- /dev/null +++ b/example-downloader-plugin/src/main/res/values/strings.xml @@ -0,0 +1,3 @@ +<resources> + <string name="app_name">Example Downloader Plugin</string> +</resources> \ No newline at end of file diff --git a/fastlane/Appfile b/fastlane/Appfile deleted file mode 100644 index 5f978f40e5..0000000000 --- a/fastlane/Appfile +++ /dev/null @@ -1,2 +0,0 @@ -json_key_file("") # Path to the json secret file - Follow https://docs.fastlane.tools/actions/supply/#setup to get one -package_name("app.revanced.manager.flutter") # e.g. com.krausefx.app diff --git a/fastlane/Fastfile b/fastlane/Fastfile deleted file mode 100644 index 19c557cc6e..0000000000 --- a/fastlane/Fastfile +++ /dev/null @@ -1,38 +0,0 @@ -# This file contains the fastlane.tools configuration -# You can find the documentation at https://docs.fastlane.tools -# -# For a list of all available actions, check out -# -# https://docs.fastlane.tools/actions -# -# For a list of all available plugins, check out -# -# https://docs.fastlane.tools/plugins/available-plugins -# - -# Uncomment the line if you want fastlane to automatically update itself -# update_fastlane - -default_platform(:android) - -platform :android do - desc "Runs all the tests" - lane :test do - gradle(task: "test") - end - - desc "Submit a new Beta Build to Crashlytics Beta" - lane :beta do - gradle(task: "clean assembleRelease") - crashlytics - - # sh "your_script.sh" - # You can also use other beta testing services here - end - - desc "Deploy a new version to the Google Play" - lane :deploy do - gradle(task: "clean assembleRelease") - upload_to_play_store - end -end diff --git a/fastlane/README.md b/fastlane/README.md deleted file mode 100644 index 7ec1207f1a..0000000000 --- a/fastlane/README.md +++ /dev/null @@ -1,48 +0,0 @@ -fastlane documentation ----- - -# Installation - -Make sure you have the latest version of the Xcode command line tools installed: - -```sh -xcode-select --install -``` - -For _fastlane_ installation instructions, see [Installing _fastlane_](https://docs.fastlane.tools/#installing-fastlane) - -# Available Actions - -## Android - -### android test - -```sh -[bundle exec] fastlane android test -``` - -Runs all the tests - -### android beta - -```sh -[bundle exec] fastlane android beta -``` - -Submit a new Beta Build to Crashlytics Beta - -### android deploy - -```sh -[bundle exec] fastlane android deploy -``` - -Deploy a new version to the Google Play - ----- - -This README.md is auto-generated and will be re-generated every time [_fastlane_](https://fastlane.tools) is run. - -More information about _fastlane_ can be found on [fastlane.tools](https://fastlane.tools). - -The documentation of _fastlane_ can be found on [docs.fastlane.tools](https://docs.fastlane.tools). diff --git a/fastlane/metadata/android/en-US/full_description.txt b/fastlane/metadata/android/en-US/full_description.txt deleted file mode 100644 index 673becbca2..0000000000 --- a/fastlane/metadata/android/en-US/full_description.txt +++ /dev/null @@ -1 +0,0 @@ -ReVanced Manager is an Android application that uses ReVanced Patcher to add, remove, and modify existing functionalities in Android applications diff --git a/fastlane/metadata/android/en-US/images/featureGraphic.png b/fastlane/metadata/android/en-US/images/featureGraphic.png deleted file mode 100644 index a872ceb726..0000000000 Binary files a/fastlane/metadata/android/en-US/images/featureGraphic.png and /dev/null differ diff --git a/fastlane/metadata/android/en-US/images/icon.png b/fastlane/metadata/android/en-US/images/icon.png deleted file mode 100644 index bef6f58706..0000000000 Binary files a/fastlane/metadata/android/en-US/images/icon.png and /dev/null differ diff --git a/fastlane/metadata/android/en-US/images/phoneScreenshots/1.jpg b/fastlane/metadata/android/en-US/images/phoneScreenshots/1.jpg deleted file mode 100644 index 4cc5f0c802..0000000000 Binary files a/fastlane/metadata/android/en-US/images/phoneScreenshots/1.jpg and /dev/null differ diff --git a/fastlane/metadata/android/en-US/images/phoneScreenshots/2.jpg b/fastlane/metadata/android/en-US/images/phoneScreenshots/2.jpg deleted file mode 100644 index cb85435142..0000000000 Binary files a/fastlane/metadata/android/en-US/images/phoneScreenshots/2.jpg and /dev/null differ diff --git a/fastlane/metadata/android/en-US/short_description.txt b/fastlane/metadata/android/en-US/short_description.txt deleted file mode 100644 index 15d7c30faf..0000000000 --- a/fastlane/metadata/android/en-US/short_description.txt +++ /dev/null @@ -1 +0,0 @@ -Patch your favorite apps, right on your device. diff --git a/fastlane/metadata/android/en-US/title.txt b/fastlane/metadata/android/en-US/title.txt deleted file mode 100644 index 897c96afe8..0000000000 --- a/fastlane/metadata/android/en-US/title.txt +++ /dev/null @@ -1 +0,0 @@ -ReVanced Manager \ No newline at end of file diff --git a/fastlane/report.xml b/fastlane/report.xml deleted file mode 100644 index 35ac130b7b..0000000000 --- a/fastlane/report.xml +++ /dev/null @@ -1,20 +0,0 @@ -<?xml version="1.0" encoding="UTF-8"?> -<testsuites> - <testsuite name="fastlane.lanes"> - - - - - <testcase classname="fastlane.lanes" name="0: default_platform" time="0.000332814"> - - </testcase> - - - <testcase classname="fastlane.lanes" name="1: test" time="295.947688054"> - - <failure message="/var/lib/gems/3.0.0/gems/fastlane-2.212.2/fastlane/lib/fastlane/actions/actions_helper.rb:67:in `execute_action' /var/lib/gems/3.0.0/gems/fastlane-2.212.2/fastlane/lib/fastlane/runner.rb:255:in `block in execute_action' /var/lib/gems/3.0.0/gems/fastlane-2.212.2/fastlane/lib/fastlane/runner.rb:229:in `chdir' /var/lib/gems/3.0.0/gems/fastlane-2.212.2/fastlane/lib/fastlane/runner.rb:229:in `execute_action' /var/lib/gems/3.0.0/gems/fastlane-2.212.2/fastlane/lib/fastlane/runner.rb:157:in `trigger_action_by_name' /var/lib/gems/3.0.0/gems/fastlane-2.212.2/fastlane/lib/fastlane/fast_file.rb:159:in `method_missing' Fastfile:21:in `block (2 levels) in parsing_binding' /var/lib/gems/3.0.0/gems/fastlane-2.212.2/fastlane/lib/fastlane/lane.rb:33:in `call' /var/lib/gems/3.0.0/gems/fastlane-2.212.2/fastlane/lib/fastlane/runner.rb:49:in `block in execute' /var/lib/gems/3.0.0/gems/fastlane-2.212.2/fastlane/lib/fastlane/runner.rb:45:in `chdir' /var/lib/gems/3.0.0/gems/fastlane-2.212.2/fastlane/lib/fastlane/runner.rb:45:in `execute' /var/lib/gems/3.0.0/gems/fastlane-2.212.2/fastlane/lib/fastlane/lane_manager.rb:47:in `cruise_lane' /var/lib/gems/3.0.0/gems/fastlane-2.212.2/fastlane/lib/fastlane/command_line_handler.rb:36:in `handle' /var/lib/gems/3.0.0/gems/fastlane-2.212.2/fastlane/lib/fastlane/commands_generator.rb:110:in `block (2 levels) in run' /var/lib/gems/3.0.0/gems/commander-4.6.0/lib/commander/command.rb:187:in `call' /var/lib/gems/3.0.0/gems/commander-4.6.0/lib/commander/command.rb:157:in `run' /var/lib/gems/3.0.0/gems/commander-4.6.0/lib/commander/runner.rb:444:in `run_active_command' /var/lib/gems/3.0.0/gems/fastlane-2.212.2/fastlane_core/lib/fastlane_core/ui/fastlane_runner.rb:124:in `run!' /var/lib/gems/3.0.0/gems/commander-4.6.0/lib/commander/delegates.rb:18:in `run!' /var/lib/gems/3.0.0/gems/fastlane-2.212.2/fastlane/lib/fastlane/commands_generator.rb:354:in `run' /var/lib/gems/3.0.0/gems/fastlane-2.212.2/fastlane/lib/fastlane/commands_generator.rb:43:in `start' /var/lib/gems/3.0.0/gems/fastlane-2.212.2/fastlane/lib/fastlane/cli_tools_distributor.rb:123:in `take_off' /var/lib/gems/3.0.0/gems/fastlane-2.212.2/bin/fastlane:23:in `<top (required)>' /usr/local/bin/fastlane:25:in `load' /usr/local/bin/fastlane:25:in `<top (required)>' /var/lib/gems/3.0.0/gems/bundler-2.4.13/lib/bundler/cli/exec.rb:58:in `load' /var/lib/gems/3.0.0/gems/bundler-2.4.13/lib/bundler/cli/exec.rb:58:in `kernel_load' /var/lib/gems/3.0.0/gems/bundler-2.4.13/lib/bundler/cli/exec.rb:23:in `run' /var/lib/gems/3.0.0/gems/bundler-2.4.13/lib/bundler/cli.rb:492:in `exec' /var/lib/gems/3.0.0/gems/bundler-2.4.13/lib/bundler/vendor/thor/lib/thor/command.rb:27:in `run' /var/lib/gems/3.0.0/gems/bundler-2.4.13/lib/bundler/vendor/thor/lib/thor/invocation.rb:127:in `invoke_command' /var/lib/gems/3.0.0/gems/bundler-2.4.13/lib/bundler/vendor/thor/lib/thor.rb:392:in `dispatch' /var/lib/gems/3.0.0/gems/bundler-2.4.13/lib/bundler/cli.rb:34:in `dispatch' /var/lib/gems/3.0.0/gems/bundler-2.4.13/lib/bundler/vendor/thor/lib/thor/base.rb:485:in `start' /var/lib/gems/3.0.0/gems/bundler-2.4.13/lib/bundler/cli.rb:28:in `start' /var/lib/gems/3.0.0/gems/bundler-2.4.13/exe/bundle:45:in `block in <top (required)>' /var/lib/gems/3.0.0/gems/bundler-2.4.13/lib/bundler/friendly_errors.rb:117:in `with_friendly_errors' /var/lib/gems/3.0.0/gems/bundler-2.4.13/exe/bundle:33:in `<top (required)>' /usr/local/bin/bundle:25:in `load' /usr/local/bin/bundle:25:in `<main>' Exit status of command '/home/aun/Programming/Projects/FlutterProjects/revanced-manager/android/gradlew test -p .' was 1 instead of 0. WARNING:We recommend using a newer Android Gradle plugin to use compileSdk = 33 This Android Gradle plugin (7.1.3) was tested up to compileSdk = 32 This warning can be suppressed by adding android.suppressUnsupportedCompileSdk=33 to this project's gradle.properties The build will continue, but you are strongly encouraged to update your project to use a newer Android Gradle Plugin that has been tested with compileSdk = 33 > Task :app:compileFlutterBuildDebug > Task :app:packLibsflutterBuildDebug > Task :app:preBuild UP-TO-DATE > Task :app:preDebugBuild UP-TO-DATE > Task :app_installer:preBuild UP-TO-DATE > Task :app_installer:preDebugBuild UP-TO-DATE > Task :app_installer:compileDebugAidl NO-SOURCE > Task :connectivity_plus:preBuild UP-TO-DATE > Task :connectivity_plus:preDebugBuild UP-TO-DATE > Task :connectivity_plus:compileDebugAidl NO-SOURCE > Task :cr_file_saver:preBuild UP-TO-DATE > Task :cr_file_saver:preDebugBuild UP-TO-DATE > Task :cr_file_saver:compileDebugAidl NO-SOURCE > Task :device_apps:preBuild UP-TO-DATE > Task :device_apps:preDebugBuild UP-TO-DATE > Task :device_apps:compileDebugAidl NO-SOURCE > Task :device_info_plus:preBuild UP-TO-DATE > Task :device_info_plus:preDebugBuild UP-TO-DATE > Task :device_info_plus:compileDebugAidl NO-SOURCE > Task :dynamic_color:preBuild UP-TO-DATE > Task :dynamic_color:preDebugBuild UP-TO-DATE > Task :dynamic_color:compileDebugAidl NO-SOURCE > Task :file_picker:preBuild UP-TO-DATE > Task :file_picker:preDebugBuild UP-TO-DATE > Task :flutter_plugin_android_lifecycle:preBuild UP-TO-DATE > Task :flutter_plugin_android_lifecycle:preDebugBuild UP-TO-DATE > Task :flutter_plugin_android_lifecycle:compileDebugAidl NO-SOURCE > Task :file_picker:compileDebugAidl NO-SOURCE > Task :flutter_background:preBuild UP-TO-DATE > Task :flutter_background:preDebugBuild UP-TO-DATE > Task :flutter_background:compileDebugAidl NO-SOURCE > Task :flutter_local_notifications:preBuild UP-TO-DATE > Task :flutter_local_notifications:preDebugBuild UP-TO-DATE > Task :flutter_local_notifications:compileDebugAidl NO-SOURCE > Task :flutter_statusbarcolor_ns:preBuild UP-TO-DATE > Task :flutter_statusbarcolor_ns:preDebugBuild UP-TO-DATE > Task :flutter_statusbarcolor_ns:compileDebugAidl NO-SOURCE > Task :fluttertoast:preBuild UP-TO-DATE > Task :fluttertoast:preDebugBuild UP-TO-DATE > Task :fluttertoast:compileDebugAidl NO-SOURCE > Task :logcat:preBuild UP-TO-DATE > Task :logcat:preDebugBuild UP-TO-DATE > Task :logcat:compileDebugAidl NO-SOURCE > Task :package_info_plus:preBuild UP-TO-DATE > Task :package_info_plus:preDebugBuild UP-TO-DATE > Task :package_info_plus:compileDebugAidl NO-SOURCE > Task :path_provider_android:preBuild UP-TO-DATE > Task :path_provider_android:preDebugBuild UP-TO-DATE > Task :path_provider_android:compileDebugAidl NO-SOURCE > Task :permission_handler_android:preBuild UP-TO-DATE > Task :permission_handler_android:preDebugBuild UP-TO-DATE > Task :permission_handler_android:compileDebugAidl NO-SOURCE > Task :root:preBuild UP-TO-DATE > Task :root:preDebugBuild UP-TO-DATE > Task :root:compileDebugAidl NO-SOURCE > Task :share_extend:preBuild UP-TO-DATE > Task :share_extend:preDebugBuild UP-TO-DATE > Task :share_extend:compileDebugAidl NO-SOURCE > Task :shared_preferences_android:preBuild UP-TO-DATE > Task :shared_preferences_android:preDebugBuild UP-TO-DATE > Task :shared_preferences_android:compileDebugAidl NO-SOURCE > Task :sqflite:preBuild UP-TO-DATE > Task :sqflite:preDebugBuild UP-TO-DATE > Task :sqflite:compileDebugAidl NO-SOURCE > Task :url_launcher_android:preBuild UP-TO-DATE > Task :url_launcher_android:preDebugBuild UP-TO-DATE > Task :url_launcher_android:compileDebugAidl NO-SOURCE > Task :wakelock:preBuild UP-TO-DATE > Task :wakelock:preDebugBuild UP-TO-DATE > Task :wakelock:compileDebugAidl NO-SOURCE > Task :app:compileDebugAidl NO-SOURCE > Task :app_installer:packageDebugRenderscript NO-SOURCE > Task :connectivity_plus:packageDebugRenderscript NO-SOURCE > Task :cr_file_saver:packageDebugRenderscript NO-SOURCE > Task :device_apps:packageDebugRenderscript NO-SOURCE > Task :device_info_plus:packageDebugRenderscript NO-SOURCE > Task :dynamic_color:packageDebugRenderscript NO-SOURCE > Task :file_picker:packageDebugRenderscript NO-SOURCE > Task :flutter_background:packageDebugRenderscript NO-SOURCE > Task :flutter_local_notifications:packageDebugRenderscript NO-SOURCE > Task :flutter_plugin_android_lifecycle:packageDebugRenderscript NO-SOURCE > Task :flutter_statusbarcolor_ns:packageDebugRenderscript NO-SOURCE > Task :fluttertoast:packageDebugRenderscript NO-SOURCE > Task :logcat:packageDebugRenderscript NO-SOURCE > Task :package_info_plus:packageDebugRenderscript NO-SOURCE > Task :path_provider_android:packageDebugRenderscript NO-SOURCE > Task :permission_handler_android:packageDebugRenderscript NO-SOURCE > Task :root:packageDebugRenderscript NO-SOURCE > Task :share_extend:packageDebugRenderscript NO-SOURCE > Task :shared_preferences_android:packageDebugRenderscript NO-SOURCE > Task :sqflite:packageDebugRenderscript NO-SOURCE > Task :url_launcher_android:packageDebugRenderscript NO-SOURCE > Task :wakelock:packageDebugRenderscript NO-SOURCE > Task :app:compileDebugRenderscript NO-SOURCE > Task :app:generateDebugBuildConfig > Task :app_installer:writeDebugAarMetadata > Task :connectivity_plus:writeDebugAarMetadata > Task :cr_file_saver:writeDebugAarMetadata > Task :device_apps:writeDebugAarMetadata > Task :device_info_plus:writeDebugAarMetadata > Task :dynamic_color:writeDebugAarMetadata > Task :file_picker:writeDebugAarMetadata > Task :flutter_background:writeDebugAarMetadata > Task :flutter_local_notifications:writeDebugAarMetadata > Task :flutter_plugin_android_lifecycle:writeDebugAarMetadata > Task :flutter_statusbarcolor_ns:writeDebugAarMetadata > Task :fluttertoast:writeDebugAarMetadata > Task :logcat:writeDebugAarMetadata > Task :package_info_plus:writeDebugAarMetadata > Task :path_provider_android:writeDebugAarMetadata > Task :permission_handler_android:writeDebugAarMetadata > Task :root:writeDebugAarMetadata > Task :share_extend:writeDebugAarMetadata > Task :shared_preferences_android:writeDebugAarMetadata > Task :sqflite:writeDebugAarMetadata > Task :url_launcher_android:writeDebugAarMetadata > Task :wakelock:writeDebugAarMetadata > Task :app:cleanMergeDebugAssets UP-TO-DATE > Task :app:mergeDebugShaders > Task :app:checkDebugAarMetadata > Task :app:compileDebugShaders NO-SOURCE > Task :app:generateDebugAssets UP-TO-DATE > Task :app_installer:mergeDebugShaders > Task :app_installer:compileDebugShaders NO-SOURCE > Task :app_installer:generateDebugAssets UP-TO-DATE > Task :app_installer:packageDebugAssets > Task :connectivity_plus:mergeDebugShaders > Task :connectivity_plus:compileDebugShaders NO-SOURCE > Task :connectivity_plus:generateDebugAssets UP-TO-DATE > Task :connectivity_plus:packageDebugAssets > Task :cr_file_saver:mergeDebugShaders > Task :cr_file_saver:compileDebugShaders NO-SOURCE > Task :cr_file_saver:generateDebugAssets UP-TO-DATE > Task :cr_file_saver:packageDebugAssets > Task :device_apps:mergeDebugShaders > Task :device_apps:compileDebugShaders NO-SOURCE > Task :device_apps:generateDebugAssets UP-TO-DATE > Task :device_apps:packageDebugAssets > Task :device_info_plus:mergeDebugShaders > Task :device_info_plus:compileDebugShaders NO-SOURCE > Task :device_info_plus:generateDebugAssets UP-TO-DATE > Task :device_info_plus:packageDebugAssets > Task :dynamic_color:mergeDebugShaders > Task :dynamic_color:compileDebugShaders NO-SOURCE > Task :dynamic_color:generateDebugAssets UP-TO-DATE > Task :dynamic_color:packageDebugAssets > Task :file_picker:mergeDebugShaders > Task :file_picker:compileDebugShaders NO-SOURCE > Task :file_picker:generateDebugAssets UP-TO-DATE > Task :file_picker:packageDebugAssets > Task :flutter_background:mergeDebugShaders > Task :flutter_background:compileDebugShaders NO-SOURCE > Task :flutter_background:generateDebugAssets UP-TO-DATE > Task :flutter_background:packageDebugAssets > Task :flutter_local_notifications:mergeDebugShaders > Task :flutter_local_notifications:compileDebugShaders NO-SOURCE > Task :flutter_local_notifications:generateDebugAssets UP-TO-DATE > Task :flutter_local_notifications:packageDebugAssets > Task :flutter_plugin_android_lifecycle:mergeDebugShaders > Task :flutter_plugin_android_lifecycle:compileDebugShaders NO-SOURCE > Task :flutter_plugin_android_lifecycle:generateDebugAssets UP-TO-DATE > Task :flutter_plugin_android_lifecycle:packageDebugAssets > Task :flutter_statusbarcolor_ns:mergeDebugShaders > Task :flutter_statusbarcolor_ns:compileDebugShaders NO-SOURCE > Task :flutter_statusbarcolor_ns:generateDebugAssets UP-TO-DATE > Task :flutter_statusbarcolor_ns:packageDebugAssets > Task :fluttertoast:mergeDebugShaders > Task :fluttertoast:compileDebugShaders NO-SOURCE > Task :fluttertoast:generateDebugAssets UP-TO-DATE > Task :fluttertoast:packageDebugAssets > Task :logcat:mergeDebugShaders > Task :logcat:compileDebugShaders NO-SOURCE > Task :logcat:generateDebugAssets UP-TO-DATE > Task :logcat:packageDebugAssets > Task :package_info_plus:mergeDebugShaders > Task :package_info_plus:compileDebugShaders NO-SOURCE > Task :package_info_plus:generateDebugAssets UP-TO-DATE > Task :package_info_plus:packageDebugAssets > Task :path_provider_android:mergeDebugShaders > Task :path_provider_android:compileDebugShaders NO-SOURCE > Task :path_provider_android:generateDebugAssets UP-TO-DATE > Task :path_provider_android:packageDebugAssets > Task :permission_handler_android:mergeDebugShaders > Task :permission_handler_android:compileDebugShaders NO-SOURCE > Task :permission_handler_android:generateDebugAssets UP-TO-DATE > Task :permission_handler_android:packageDebugAssets > Task :root:mergeDebugShaders > Task :root:compileDebugShaders NO-SOURCE > Task :root:generateDebugAssets UP-TO-DATE > Task :root:packageDebugAssets > Task :share_extend:mergeDebugShaders > Task :share_extend:compileDebugShaders NO-SOURCE > Task :share_extend:generateDebugAssets UP-TO-DATE > Task :share_extend:packageDebugAssets > Task :shared_preferences_android:mergeDebugShaders > Task :shared_preferences_android:compileDebugShaders NO-SOURCE > Task :shared_preferences_android:generateDebugAssets UP-TO-DATE > Task :shared_preferences_android:packageDebugAssets > Task :sqflite:mergeDebugShaders > Task :sqflite:compileDebugShaders NO-SOURCE > Task :sqflite:generateDebugAssets UP-TO-DATE > Task :sqflite:packageDebugAssets > Task :url_launcher_android:mergeDebugShaders > Task :url_launcher_android:compileDebugShaders NO-SOURCE > Task :url_launcher_android:generateDebugAssets UP-TO-DATE > Task :url_launcher_android:packageDebugAssets > Task :wakelock:mergeDebugShaders > Task :wakelock:compileDebugShaders NO-SOURCE > Task :wakelock:generateDebugAssets UP-TO-DATE > Task :wakelock:packageDebugAssets > Task :app:mergeDebugAssets > Task :app:copyFlutterAssetsDebug > Task :app:generateDebugResValues > Task :app:generateDebugResources > Task :app_installer:compileDebugRenderscript NO-SOURCE > Task :app_installer:generateDebugResValues > Task :app_installer:generateDebugResources > Task :app_installer:packageDebugResources > Task :connectivity_plus:compileDebugRenderscript NO-SOURCE > Task :connectivity_plus:generateDebugResValues > Task :connectivity_plus:generateDebugResources > Task :connectivity_plus:packageDebugResources > Task :cr_file_saver:compileDebugRenderscript NO-SOURCE > Task :cr_file_saver:generateDebugResValues > Task :cr_file_saver:generateDebugResources > Task :cr_file_saver:packageDebugResources > Task :device_apps:compileDebugRenderscript NO-SOURCE > Task :device_apps:generateDebugResValues > Task :device_apps:generateDebugResources > Task :device_apps:packageDebugResources > Task :device_info_plus:compileDebugRenderscript NO-SOURCE > Task :device_info_plus:generateDebugResValues > Task :device_info_plus:generateDebugResources > Task :device_info_plus:packageDebugResources > Task :dynamic_color:compileDebugRenderscript NO-SOURCE > Task :dynamic_color:generateDebugResValues > Task :dynamic_color:generateDebugResources > Task :dynamic_color:packageDebugResources > Task :file_picker:compileDebugRenderscript NO-SOURCE > Task :file_picker:generateDebugResValues > Task :file_picker:generateDebugResources > Task :file_picker:packageDebugResources > Task :flutter_background:compileDebugRenderscript NO-SOURCE > Task :flutter_background:generateDebugResValues > Task :flutter_background:generateDebugResources > Task :flutter_background:packageDebugResources > Task :flutter_local_notifications:compileDebugRenderscript NO-SOURCE > Task :flutter_local_notifications:generateDebugResValues > Task :flutter_local_notifications:generateDebugResources > Task :flutter_local_notifications:packageDebugResources > Task :flutter_plugin_android_lifecycle:compileDebugRenderscript NO-SOURCE > Task :flutter_plugin_android_lifecycle:generateDebugResValues > Task :flutter_plugin_android_lifecycle:generateDebugResources > Task :flutter_plugin_android_lifecycle:packageDebugResources > Task :flutter_statusbarcolor_ns:compileDebugRenderscript NO-SOURCE > Task :flutter_statusbarcolor_ns:generateDebugResValues > Task :flutter_statusbarcolor_ns:generateDebugResources > Task :flutter_statusbarcolor_ns:packageDebugResources > Task :fluttertoast:compileDebugRenderscript NO-SOURCE > Task :fluttertoast:generateDebugResValues > Task :fluttertoast:generateDebugResources > Task :fluttertoast:packageDebugResources > Task :logcat:compileDebugRenderscript NO-SOURCE > Task :logcat:generateDebugResValues > Task :logcat:generateDebugResources > Task :logcat:packageDebugResources > Task :package_info_plus:compileDebugRenderscript NO-SOURCE > Task :package_info_plus:generateDebugResValues > Task :package_info_plus:generateDebugResources > Task :package_info_plus:packageDebugResources > Task :path_provider_android:compileDebugRenderscript NO-SOURCE > Task :path_provider_android:generateDebugResValues > Task :path_provider_android:generateDebugResources > Task :path_provider_android:packageDebugResources > Task :permission_handler_android:compileDebugRenderscript NO-SOURCE > Task :permission_handler_android:generateDebugResValues > Task :permission_handler_android:generateDebugResources > Task :permission_handler_android:packageDebugResources > Task :root:compileDebugRenderscript NO-SOURCE > Task :root:generateDebugResValues > Task :root:generateDebugResources > Task :root:packageDebugResources > Task :share_extend:compileDebugRenderscript NO-SOURCE > Task :share_extend:generateDebugResValues > Task :share_extend:generateDebugResources > Task :share_extend:packageDebugResources > Task :shared_preferences_android:compileDebugRenderscript NO-SOURCE > Task :shared_preferences_android:generateDebugResValues > Task :shared_preferences_android:generateDebugResources > Task :shared_preferences_android:packageDebugResources > Task :sqflite:compileDebugRenderscript NO-SOURCE > Task :sqflite:generateDebugResValues > Task :sqflite:generateDebugResources > Task :sqflite:packageDebugResources > Task :url_launcher_android:compileDebugRenderscript NO-SOURCE > Task :url_launcher_android:generateDebugResValues > Task :url_launcher_android:generateDebugResources > Task :url_launcher_android:packageDebugResources > Task :wakelock:compileDebugRenderscript NO-SOURCE > Task :wakelock:generateDebugResValues > Task :wakelock:generateDebugResources > Task :wakelock:packageDebugResources > Task :app:createDebugCompatibleScreenManifests > Task :app:extractDeepLinksDebug > Task :app_installer:extractDeepLinksDebug > Task :connectivity_plus:extractDeepLinksDebug > Task :cr_file_saver:extractDeepLinksDebug > Task :device_apps:extractDeepLinksDebug > Task :connectivity_plus:processDebugManifest > Task :device_info_plus:extractDeepLinksDebug > Task :app_installer:processDebugManifest > Task :device_apps:processDebugManifest > Task :cr_file_saver:processDebugManifest > Task :device_info_plus:processDebugManifest > Task :dynamic_color:extractDeepLinksDebug > Task :file_picker:extractDeepLinksDebug > Task :flutter_background:extractDeepLinksDebug > Task :dynamic_color:processDebugManifest > Task :flutter_local_notifications:extractDeepLinksDebug > Task :file_picker:processDebugManifest > Task :flutter_background:processDebugManifest > Task :flutter_plugin_android_lifecycle:extractDeepLinksDebug > Task :app:mergeDebugResources > Task :flutter_statusbarcolor_ns:extractDeepLinksDebug > Task :flutter_local_notifications:processDebugManifest > Task :fluttertoast:extractDeepLinksDebug > Task :flutter_plugin_android_lifecycle:processDebugManifest > Task :flutter_statusbarcolor_ns:processDebugManifest > Task :logcat:extractDeepLinksDebug > Task :fluttertoast:processDebugManifest > Task :package_info_plus:extractDeepLinksDebug > Task :logcat:processDebugManifest > Task :path_provider_android:extractDeepLinksDebug > Task :package_info_plus:processDebugManifest > Task :permission_handler_android:extractDeepLinksDebug > Task :path_provider_android:processDebugManifest > Task :root:extractDeepLinksDebug > Task :permission_handler_android:processDebugManifest > Task :share_extend:extractDeepLinksDebug > Task :root:processDebugManifest > Task :shared_preferences_android:extractDeepLinksDebug > Task :sqflite:extractDeepLinksDebug > Task :share_extend:processDebugManifest > Task :shared_preferences_android:processDebugManifest > Task :url_launcher_android:extractDeepLinksDebug > Task :wakelock:extractDeepLinksDebug > Task :sqflite:processDebugManifest > Task :url_launcher_android:processDebugManifest > Task :wakelock:processDebugManifest > Task :app_installer:compileDebugLibraryResources > Task :app_installer:parseDebugLocalResources > Task :app:processDebugMainManifest > Task :app:processDebugManifest > Task :app:processDebugManifestForPackage > Task :connectivity_plus:compileDebugLibraryResources > Task :connectivity_plus:parseDebugLocalResources > Task :cr_file_saver:compileDebugLibraryResources > Task :app_installer:generateDebugRFile > Task :cr_file_saver:parseDebugLocalResources > Task :connectivity_plus:generateDebugRFile > Task :device_apps:compileDebugLibraryResources > Task :cr_file_saver:generateDebugRFile > Task :device_info_plus:compileDebugLibraryResources > Task :device_apps:parseDebugLocalResources > Task :device_info_plus:parseDebugLocalResources > Task :device_apps:generateDebugRFile > Task :dynamic_color:compileDebugLibraryResources > Task :dynamic_color:parseDebugLocalResources > Task :device_info_plus:generateDebugRFile > Task :file_picker:compileDebugLibraryResources > Task :file_picker:parseDebugLocalResources > Task :dynamic_color:generateDebugRFile > Task :flutter_plugin_android_lifecycle:parseDebugLocalResources > Task :flutter_background:compileDebugLibraryResources > Task :flutter_plugin_android_lifecycle:generateDebugRFile > Task :flutter_background:parseDebugLocalResources > Task :flutter_local_notifications:compileDebugLibraryResources > Task :file_picker:generateDebugRFile > Task :flutter_local_notifications:parseDebugLocalResources > Task :flutter_plugin_android_lifecycle:compileDebugLibraryResources > Task :flutter_statusbarcolor_ns:compileDebugLibraryResources > Task :flutter_background:generateDebugRFile > Task :flutter_statusbarcolor_ns:parseDebugLocalResources > Task :fluttertoast:compileDebugLibraryResources > Task :logcat:compileDebugLibraryResources > Task :flutter_local_notifications:generateDebugRFile > Task :fluttertoast:parseDebugLocalResources > Task :flutter_statusbarcolor_ns:generateDebugRFile > Task :logcat:parseDebugLocalResources > Task :fluttertoast:generateDebugRFile > Task :package_info_plus:compileDebugLibraryResources > Task :logcat:generateDebugRFile > Task :package_info_plus:parseDebugLocalResources > Task :path_provider_android:compileDebugLibraryResources > Task :package_info_plus:generateDebugRFile > Task :path_provider_android:parseDebugLocalResources > Task :permission_handler_android:compileDebugLibraryResources > Task :path_provider_android:generateDebugRFile > Task :root:compileDebugLibraryResources > Task :permission_handler_android:parseDebugLocalResources > Task :root:parseDebugLocalResources > Task :permission_handler_android:generateDebugRFile > Task :share_extend:compileDebugLibraryResources > Task :root:generateDebugRFile > Task :shared_preferences_android:compileDebugLibraryResources > Task :share_extend:parseDebugLocalResources > Task :shared_preferences_android:parseDebugLocalResources > Task :share_extend:generateDebugRFile > Task :sqflite:compileDebugLibraryResources > Task :shared_preferences_android:generateDebugRFile > Task :url_launcher_android:compileDebugLibraryResources > Task :sqflite:parseDebugLocalResources > Task :url_launcher_android:parseDebugLocalResources > Task :sqflite:generateDebugRFile > Task :wakelock:compileDebugLibraryResources > Task :url_launcher_android:generateDebugRFile > Task :app_installer:generateDebugBuildConfig > Task :wakelock:parseDebugLocalResources > Task :app_installer:javaPreCompileDebug > Task :wakelock:generateDebugRFile > Task :app_installer:compileDebugJavaWithJavac Note: /home/aun/.pub-cache/hosted/pub.dev/app_installer-1.1.0/android/src/main/java/com/zero/app_installer/AppInstallerPlugin.java uses or overrides a deprecated API. Note: Recompile with -Xlint:deprecation for details. > Task :app_installer:bundleLibCompileToJarDebug > Task :connectivity_plus:generateDebugBuildConfig > Task :connectivity_plus:javaPreCompileDebug > Task :cr_file_saver:generateDebugBuildConfig > Task :connectivity_plus:compileDebugJavaWithJavac > Task :connectivity_plus:bundleLibCompileToJarDebug > Task :cr_file_saver:javaPreCompileDebug > Task :device_apps:generateDebugBuildConfig > Task :device_apps:javaPreCompileDebug > Task :device_info_plus:generateDebugBuildConfig > Task :device_apps:compileDebugJavaWithJavac Note: /home/aun/.pub-cache/git/flutter_plugin_device_apps-0f0558df6fb2c9a3ed62529835704df010eeb56c/android/src/main/java/fr/g123k/deviceapps/DeviceAppsPlugin.java uses or overrides a deprecated API. Note: Recompile with -Xlint:deprecation for details. > Task :device_apps:bundleLibCompileToJarDebug > Task :device_info_plus:javaPreCompileDebug > Task :dynamic_color:generateDebugBuildConfig > Task :dynamic_color:javaPreCompileDebug > Task :file_picker:generateDebugBuildConfig > Task :file_picker:javaPreCompileDebug > Task :flutter_plugin_android_lifecycle:generateDebugBuildConfig > Task :flutter_background:generateDebugBuildConfig > Task :flutter_plugin_android_lifecycle:javaPreCompileDebug > Task :flutter_plugin_android_lifecycle:compileDebugJavaWithJavac > Task :flutter_plugin_android_lifecycle:bundleLibCompileToJarDebug > Task :flutter_background:javaPreCompileDebug > Task :file_picker:compileDebugJavaWithJavac Note: /home/aun/.pub-cache/git/flutter_file_picker-f4a8faac83df23506f310a1b49a1861df228a54a/android/src/main/java/com/mr/flutter/plugin/filepicker/FilePickerDelegate.java uses or overrides a deprecated API. Note: Recompile with -Xlint:deprecation for details. > Task :flutter_local_notifications:generateDebugBuildConfig > Task :file_picker:bundleLibCompileToJarDebug > Task :flutter_statusbarcolor_ns:generateDebugBuildConfig > Task :flutter_local_notifications:javaPreCompileDebug > Task :app:processDebugResources > Task :dynamic_color:compileDebugKotlin > Task :flutter_local_notifications:compileDebugJavaWithJavac > Task :dynamic_color:compileDebugJavaWithJavac > Task :flutter_statusbarcolor_ns:javaPreCompileDebug > Task :fluttertoast:generateDebugBuildConfig > Task :dynamic_color:bundleLibCompileToJarDebug > Task :flutter_local_notifications:bundleLibCompileToJarDebug > Task :fluttertoast:javaPreCompileDebug > Task :logcat:generateDebugBuildConfig > Task :logcat:javaPreCompileDebug > Task :package_info_plus:generateDebugBuildConfig > Task :flutter_statusbarcolor_ns:compileDebugKotlin w: /home/aun/.pub-cache/hosted/pub.dev/flutter_statusbarcolor_ns-0.5.0/android/src/main/kotlin/com/sameer/flutterstatusbarcolor/flutterstatusbarcolor/FlutterStatusbarcolorPlugin.kt: (67, 53): 'setter for systemUiVisibility: Int' is deprecated. Deprecated in Java w: /home/aun/.pub-cache/hosted/pub.dev/flutter_statusbarcolor_ns-0.5.0/android/src/main/kotlin/com/sameer/flutterstatusbarcolor/flutterstatusbarcolor/FlutterStatusbarcolorPlugin.kt: (67, 102): 'getter for systemUiVisibility: Int' is deprecated. Deprecated in Java w: /home/aun/.pub-cache/hosted/pub.dev/flutter_statusbarcolor_ns-0.5.0/android/src/main/kotlin/com/sameer/flutterstatusbarcolor/flutterstatusbarcolor/FlutterStatusbarcolorPlugin.kt: (67, 130): 'SYSTEM_UI_FLAG_LIGHT_STATUS_BAR: Int' is deprecated. Deprecated in Java w: /home/aun/.pub-cache/hosted/pub.dev/flutter_statusbarcolor_ns-0.5.0/android/src/main/kotlin/com/sameer/flutterstatusbarcolor/flutterstatusbarcolor/FlutterStatusbarcolorPlugin.kt: (69, 53): 'setter for systemUiVisibility: Int' is deprecated. Deprecated in Java w: /home/aun/.pub-cache/hosted/pub.dev/flutter_statusbarcolor_ns-0.5.0/android/src/main/kotlin/com/sameer/flutterstatusbarcolor/flutterstatusbarcolor/FlutterStatusbarcolorPlugin.kt: (69, 102): 'getter for systemUiVisibility: Int' is deprecated. Deprecated in Java w: /home/aun/.pub-cache/hosted/pub.dev/flutter_statusbarcolor_ns-0.5.0/android/src/main/kotlin/com/sameer/flutterstatusbarcolor/flutterstatusbarcolor/FlutterStatusbarcolorPlugin.kt: (69, 129): 'SYSTEM_UI_FLAG_LIGHT_STATUS_BAR: Int' is deprecated. Deprecated in Java w: /home/aun/.pub-cache/hosted/pub.dev/flutter_statusbarcolor_ns-0.5.0/android/src/main/kotlin/com/sameer/flutterstatusbarcolor/flutterstatusbarcolor/FlutterStatusbarcolorPlugin.kt: (100, 53): 'setter for systemUiVisibility: Int' is deprecated. Deprecated in Java w: /home/aun/.pub-cache/hosted/pub.dev/flutter_statusbarcolor_ns-0.5.0/android/src/main/kotlin/com/sameer/flutterstatusbarcolor/flutterstatusbarcolor/FlutterStatusbarcolorPlugin.kt: (100, 102): 'getter for systemUiVisibility: Int' is deprecated. Deprecated in Java w: /home/aun/.pub-cache/hosted/pub.dev/flutter_statusbarcolor_ns-0.5.0/android/src/main/kotlin/com/sameer/flutterstatusbarcolor/flutterstatusbarcolor/FlutterStatusbarcolorPlugin.kt: (100, 130): 'SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR: Int' is deprecated. Deprecated in Java w: /home/aun/.pub-cache/hosted/pub.dev/flutter_statusbarcolor_ns-0.5.0/android/src/main/kotlin/com/sameer/flutterstatusbarcolor/flutterstatusbarcolor/FlutterStatusbarcolorPlugin.kt: (102, 53): 'setter for systemUiVisibility: Int' is deprecated. Deprecated in Java w: /home/aun/.pub-cache/hosted/pub.dev/flutter_statusbarcolor_ns-0.5.0/android/src/main/kotlin/com/sameer/flutterstatusbarcolor/flutterstatusbarcolor/FlutterStatusbarcolorPlugin.kt: (102, 102): 'getter for systemUiVisibility: Int' is deprecated. Deprecated in Java w: /home/aun/.pub-cache/hosted/pub.dev/flutter_statusbarcolor_ns-0.5.0/android/src/main/kotlin/com/sameer/flutterstatusbarcolor/flutterstatusbarcolor/FlutterStatusbarcolorPlugin.kt: (102, 129): 'SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR: Int' is deprecated. Deprecated in Java > Task :logcat:compileDebugJavaWithJavac > Task :logcat:bundleLibCompileToJarDebug > Task :flutter_statusbarcolor_ns:compileDebugJavaWithJavac > Task :device_info_plus:compileDebugKotlin w: /home/aun/.pub-cache/hosted/pub.dev/device_info_plus-8.2.2/android/src/main/kotlin/dev/fluttercommunity/plus/device_info/MethodCallHandlerImpl.kt: (67, 50): 'getter for defaultDisplay: Display!' is deprecated. Deprecated in Java w: /home/aun/.pub-cache/hosted/pub.dev/device_info_plus-8.2.2/android/src/main/kotlin/dev/fluttercommunity/plus/device_info/MethodCallHandlerImpl.kt: (70, 25): 'getRealMetrics(DisplayMetrics!): Unit' is deprecated. Deprecated in Java w: /home/aun/.pub-cache/hosted/pub.dev/device_info_plus-8.2.2/android/src/main/kotlin/dev/fluttercommunity/plus/device_info/MethodCallHandlerImpl.kt: (72, 25): 'getMetrics(DisplayMetrics!): Unit' is deprecated. Deprecated in Java w: /home/aun/.pub-cache/hosted/pub.dev/device_info_plus-8.2.2/android/src/main/kotlin/dev/fluttercommunity/plus/device_info/MethodCallHandlerImpl.kt: (89, 47): 'SERIAL: String!' is deprecated. Deprecated in Java > Task :path_provider_android:generateDebugBuildConfig > Task :flutter_statusbarcolor_ns:bundleLibCompileToJarDebug > Task :package_info_plus:javaPreCompileDebug > Task :path_provider_android:javaPreCompileDebug > Task :permission_handler_android:generateDebugBuildConfig > Task :path_provider_android:compileDebugJavaWithJavac > Task :device_info_plus:compileDebugJavaWithJavac > Task :path_provider_android:bundleLibCompileToJarDebug > Task :device_info_plus:bundleLibCompileToJarDebug > Task :root:generateDebugBuildConfig > Task :permission_handler_android:javaPreCompileDebug > Task :root:javaPreCompileDebug > Task :permission_handler_android:compileDebugJavaWithJavac > Task :permission_handler_android:bundleLibCompileToJarDebug > Task :root:compileDebugJavaWithJavac > Task :share_extend:generateDebugBuildConfig > Task :root:bundleLibCompileToJarDebug > Task :share_extend:javaPreCompileDebug > Task :shared_preferences_android:generateDebugBuildConfig > Task :cr_file_saver:compileDebugKotlin > Task :fluttertoast:compileDebugKotlin w: /home/aun/.pub-cache/hosted/pub.dev/fluttertoast-8.2.2/android/src/main/kotlin/io/github/ponnamkarthik/toast/fluttertoast/FlutterToastPlugin.kt: (9, 48): 'Registrar' is deprecated. Deprecated in Java w: /home/aun/.pub-cache/hosted/pub.dev/fluttertoast-8.2.2/android/src/main/kotlin/io/github/ponnamkarthik/toast/fluttertoast/MethodCallHandlerImpl.kt: (55, 40): 'setColorFilter(Int, PorterDuff.Mode): Unit' is deprecated. Deprecated in Java w: /home/aun/.pub-cache/hosted/pub.dev/fluttertoast-8.2.2/android/src/main/kotlin/io/github/ponnamkarthik/toast/fluttertoast/MethodCallHandlerImpl.kt: (66, 29): 'setter for view: View?' is deprecated. Deprecated in Java w: /home/aun/.pub-cache/hosted/pub.dev/fluttertoast-8.2.2/android/src/main/kotlin/io/github/ponnamkarthik/toast/fluttertoast/MethodCallHandlerImpl.kt: (71, 62): 'getter for view: View?' is deprecated. Deprecated in Java > Task :share_extend:compileDebugJavaWithJavac Note: Some input files use or override a deprecated API. Note: Recompile with -Xlint:deprecation for details. > Task :cr_file_saver:compileDebugJavaWithJavac > Task :flutter_background:compileDebugKotlin w: /home/aun/.pub-cache/hosted/pub.dev/flutter_background-1.2.0/android/src/main/kotlin/de/julianassmann/flutter_background/FlutterBackgroundPlugin.kt: (18, 48): 'Registrar' is deprecated. Deprecated in Java w: /home/aun/.pub-cache/hosted/pub.dev/flutter_background-1.2.0/android/src/main/kotlin/de/julianassmann/flutter_background/FlutterBackgroundPlugin.kt: (28, 33): 'Registrar' is deprecated. Deprecated in Java w: /home/aun/.pub-cache/hosted/pub.dev/flutter_background-1.2.0/android/src/main/kotlin/de/julianassmann/flutter_background/IsolateHolderService.kt: (122, 51): 'WIFI_MODE_FULL: Int' is deprecated. Deprecated in Java > Task :cr_file_saver:bundleLibCompileToJarDebug > Task :fluttertoast:compileDebugJavaWithJavac > Task :package_info_plus:compileDebugKotlin > Task :flutter_background:compileDebugJavaWithJavac > Task :flutter_background:bundleLibCompileToJarDebug > Task :fluttertoast:bundleLibCompileToJarDebug > Task :package_info_plus:compileDebugJavaWithJavac > Task :package_info_plus:bundleLibCompileToJarDebug > Task :share_extend:bundleLibCompileToJarDebug > Task :shared_preferences_android:javaPreCompileDebug > Task :sqflite:generateDebugBuildConfig > Task :shared_preferences_android:compileDebugJavaWithJavac > Task :sqflite:javaPreCompileDebug > Task :url_launcher_android:generateDebugBuildConfig > Task :shared_preferences_android:bundleLibCompileToJarDebug > Task :sqflite:compileDebugJavaWithJavac > Task :url_launcher_android:javaPreCompileDebug > Task :wakelock:generateDebugBuildConfig > Task :sqflite:bundleLibCompileToJarDebug > Task :url_launcher_android:compileDebugJavaWithJavac > Task :url_launcher_android:bundleLibCompileToJarDebug > Task :wakelock:javaPreCompileDebug > Task :app:preDebugUnitTestBuild UP-TO-DATE > Task :app:javaPreCompileDebug > Task :app:processDebugJavaRes NO-SOURCE > Task :app:javaPreCompileDebugUnitTest > Task :app:processDebugUnitTestJavaRes NO-SOURCE > Task :app_installer:processDebugJavaRes NO-SOURCE > Task :app_installer:bundleLibResDebug NO-SOURCE > Task :connectivity_plus:processDebugJavaRes NO-SOURCE > Task :app_installer:bundleLibRuntimeToJarDebug > Task :connectivity_plus:bundleLibResDebug NO-SOURCE > Task :cr_file_saver:processDebugJavaRes NO-SOURCE > Task :connectivity_plus:bundleLibRuntimeToJarDebug > Task :cr_file_saver:bundleLibResDebug > Task :device_apps:processDebugJavaRes NO-SOURCE > Task :device_apps:bundleLibResDebug NO-SOURCE > Task :cr_file_saver:bundleLibRuntimeToJarDebug > Task :device_info_plus:processDebugJavaRes NO-SOURCE > Task :device_apps:bundleLibRuntimeToJarDebug > Task :device_info_plus:bundleLibResDebug > Task :dynamic_color:processDebugJavaRes NO-SOURCE > Task :device_info_plus:bundleLibRuntimeToJarDebug > Task :dynamic_color:bundleLibResDebug > Task :file_picker:processDebugJavaRes NO-SOURCE > Task :file_picker:bundleLibResDebug NO-SOURCE > Task :flutter_background:processDebugJavaRes NO-SOURCE > Task :dynamic_color:bundleLibRuntimeToJarDebug > Task :file_picker:bundleLibRuntimeToJarDebug > Task :flutter_local_notifications:processDebugJavaRes NO-SOURCE > Task :flutter_local_notifications:bundleLibResDebug NO-SOURCE > Task :flutter_plugin_android_lifecycle:processDebugJavaRes NO-SOURCE > Task :flutter_background:bundleLibRuntimeToJarDebug > Task :flutter_plugin_android_lifecycle:bundleLibResDebug NO-SOURCE > Task :flutter_background:bundleLibResDebug > Task :flutter_statusbarcolor_ns:processDebugJavaRes NO-SOURCE > Task :flutter_plugin_android_lifecycle:bundleLibRuntimeToJarDebug > Task :flutter_local_notifications:bundleLibRuntimeToJarDebug > Task :flutter_statusbarcolor_ns:bundleLibResDebug > Task :fluttertoast:processDebugJavaRes NO-SOURCE > Task :flutter_statusbarcolor_ns:bundleLibRuntimeToJarDebug > Task :fluttertoast:bundleLibResDebug > Task :logcat:processDebugJavaRes NO-SOURCE > Task :fluttertoast:bundleLibRuntimeToJarDebug > Task :logcat:bundleLibResDebug NO-SOURCE > Task :package_info_plus:processDebugJavaRes NO-SOURCE > Task :logcat:bundleLibRuntimeToJarDebug > Task :package_info_plus:bundleLibResDebug > Task :path_provider_android:processDebugJavaRes NO-SOURCE > Task :path_provider_android:bundleLibResDebug NO-SOURCE > Task :package_info_plus:bundleLibRuntimeToJarDebug > Task :path_provider_android:bundleLibRuntimeToJarDebug > Task :permission_handler_android:processDebugJavaRes NO-SOURCE > Task :permission_handler_android:bundleLibResDebug NO-SOURCE > Task :root:processDebugJavaRes NO-SOURCE > Task :root:bundleLibResDebug NO-SOURCE > Task :share_extend:processDebugJavaRes NO-SOURCE > Task :share_extend:bundleLibResDebug NO-SOURCE > Task :permission_handler_android:bundleLibRuntimeToJarDebug > Task :shared_preferences_android:processDebugJavaRes NO-SOURCE > Task :root:bundleLibRuntimeToJarDebug > Task :shared_preferences_android:bundleLibResDebug NO-SOURCE > Task :share_extend:bundleLibRuntimeToJarDebug > Task :sqflite:processDebugJavaRes NO-SOURCE > Task :shared_preferences_android:bundleLibRuntimeToJarDebug > Task :sqflite:bundleLibResDebug NO-SOURCE > Task :url_launcher_android:processDebugJavaRes NO-SOURCE > Task :url_launcher_android:bundleLibResDebug NO-SOURCE > Task :sqflite:bundleLibRuntimeToJarDebug > Task :wakelock:processDebugJavaRes NO-SOURCE > Task :url_launcher_android:bundleLibRuntimeToJarDebug > Task :wakelock:compileDebugKotlin > Task :app:compileFlutterBuildProfile > Task :wakelock:compileDebugJavaWithJavac > Task :wakelock:bundleLibCompileToJarDebug > Task :wakelock:bundleLibResDebug > Task :wakelock:bundleLibRuntimeToJarDebug > Task :app:compileDebugKotlin w: /home/aun/Programming/Projects/FlutterProjects/revanced-manager/android/app/src/main/kotlin/app/revanced/manager/flutter/MainActivity.kt: (313, 28): The corresponding parameter in the supertype 'Logger' is named 'msg'. This may cause problems when calling this function with named arguments. > Task :app:packLibsflutterBuildProfile > Task :app:compileDebugJavaWithJavac > Task :app:preProfileBuild UP-TO-DATE > Task :app_installer:preProfileBuild UP-TO-DATE > Task :app_installer:compileProfileAidl NO-SOURCE > Task :connectivity_plus:preProfileBuild UP-TO-DATE > Task :connectivity_plus:compileProfileAidl NO-SOURCE > Task :cr_file_saver:preProfileBuild UP-TO-DATE > Task :cr_file_saver:compileProfileAidl NO-SOURCE > Task :device_apps:preProfileBuild UP-TO-DATE > Task :device_apps:compileProfileAidl NO-SOURCE > Task :device_info_plus:preProfileBuild UP-TO-DATE > Task :device_info_plus:compileProfileAidl NO-SOURCE > Task :dynamic_color:preProfileBuild UP-TO-DATE > Task :dynamic_color:compileProfileAidl NO-SOURCE > Task :file_picker:preProfileBuild UP-TO-DATE > Task :flutter_plugin_android_lifecycle:preProfileBuild UP-TO-DATE > Task :flutter_plugin_android_lifecycle:compileProfileAidl NO-SOURCE > Task :app:bundleDebugClasses > Task :file_picker:compileProfileAidl NO-SOURCE > Task :app:compileDebugUnitTestKotlin NO-SOURCE > Task :app:compileDebugUnitTestJavaWithJavac NO-SOURCE > Task :flutter_background:preProfileBuild UP-TO-DATE > Task :flutter_background:compileProfileAidl NO-SOURCE > Task :app:bundleDebugClassesToRuntimeJar > Task :app:testDebugUnitTest NO-SOURCE > Task :flutter_local_notifications:preProfileBuild UP-TO-DATE > Task :flutter_local_notifications:compileProfileAidl NO-SOURCE > Task :flutter_statusbarcolor_ns:preProfileBuild UP-TO-DATE > Task :flutter_statusbarcolor_ns:compileProfileAidl NO-SOURCE > Task :fluttertoast:preProfileBuild UP-TO-DATE > Task :fluttertoast:compileProfileAidl NO-SOURCE > Task :logcat:preProfileBuild UP-TO-DATE > Task :logcat:compileProfileAidl NO-SOURCE > Task :package_info_plus:preProfileBuild UP-TO-DATE > Task :package_info_plus:compileProfileAidl NO-SOURCE > Task :path_provider_android:preProfileBuild UP-TO-DATE > Task :path_provider_android:compileProfileAidl NO-SOURCE > Task :permission_handler_android:preProfileBuild UP-TO-DATE > Task :permission_handler_android:compileProfileAidl NO-SOURCE > Task :root:preProfileBuild UP-TO-DATE > Task :root:compileProfileAidl NO-SOURCE > Task :share_extend:preProfileBuild UP-TO-DATE > Task :share_extend:compileProfileAidl NO-SOURCE > Task :shared_preferences_android:preProfileBuild UP-TO-DATE > Task :shared_preferences_android:compileProfileAidl NO-SOURCE > Task :sqflite:preProfileBuild UP-TO-DATE > Task :sqflite:compileProfileAidl NO-SOURCE > Task :url_launcher_android:preProfileBuild UP-TO-DATE > Task :url_launcher_android:compileProfileAidl NO-SOURCE > Task :wakelock:preProfileBuild UP-TO-DATE > Task :wakelock:compileProfileAidl NO-SOURCE > Task :app:compileProfileAidl NO-SOURCE > Task :app_installer:packageProfileRenderscript NO-SOURCE > Task :connectivity_plus:packageProfileRenderscript NO-SOURCE > Task :cr_file_saver:packageProfileRenderscript NO-SOURCE > Task :device_apps:packageProfileRenderscript NO-SOURCE > Task :device_info_plus:packageProfileRenderscript NO-SOURCE > Task :dynamic_color:packageProfileRenderscript NO-SOURCE > Task :file_picker:packageProfileRenderscript NO-SOURCE > Task :flutter_background:packageProfileRenderscript NO-SOURCE > Task :flutter_local_notifications:packageProfileRenderscript NO-SOURCE > Task :flutter_plugin_android_lifecycle:packageProfileRenderscript NO-SOURCE > Task :flutter_statusbarcolor_ns:packageProfileRenderscript NO-SOURCE > Task :fluttertoast:packageProfileRenderscript NO-SOURCE > Task :logcat:packageProfileRenderscript NO-SOURCE > Task :package_info_plus:packageProfileRenderscript NO-SOURCE > Task :path_provider_android:packageProfileRenderscript NO-SOURCE > Task :permission_handler_android:packageProfileRenderscript NO-SOURCE > Task :root:packageProfileRenderscript NO-SOURCE > Task :share_extend:packageProfileRenderscript NO-SOURCE > Task :shared_preferences_android:packageProfileRenderscript NO-SOURCE > Task :sqflite:packageProfileRenderscript NO-SOURCE > Task :url_launcher_android:packageProfileRenderscript NO-SOURCE > Task :wakelock:packageProfileRenderscript NO-SOURCE > Task :app:compileProfileRenderscript NO-SOURCE > Task :app:generateProfileBuildConfig > Task :app_installer:writeProfileAarMetadata > Task :connectivity_plus:writeProfileAarMetadata > Task :cr_file_saver:writeProfileAarMetadata > Task :device_apps:writeProfileAarMetadata > Task :device_info_plus:writeProfileAarMetadata > Task :dynamic_color:writeProfileAarMetadata > Task :file_picker:writeProfileAarMetadata > Task :flutter_background:writeProfileAarMetadata > Task :flutter_local_notifications:writeProfileAarMetadata > Task :flutter_plugin_android_lifecycle:writeProfileAarMetadata > Task :flutter_statusbarcolor_ns:writeProfileAarMetadata > Task :fluttertoast:writeProfileAarMetadata > Task :logcat:writeProfileAarMetadata > Task :package_info_plus:writeProfileAarMetadata > Task :path_provider_android:writeProfileAarMetadata > Task :permission_handler_android:writeProfileAarMetadata > Task :root:writeProfileAarMetadata > Task :share_extend:writeProfileAarMetadata > Task :shared_preferences_android:writeProfileAarMetadata > Task :url_launcher_android:writeProfileAarMetadata > Task :sqflite:writeProfileAarMetadata > Task :app:cleanMergeProfileAssets UP-TO-DATE > Task :wakelock:writeProfileAarMetadata > Task :app:mergeProfileShaders > Task :app:compileProfileShaders NO-SOURCE > Task :app:generateProfileAssets UP-TO-DATE > Task :app:checkProfileAarMetadata > Task :app_installer:mergeProfileShaders > Task :app_installer:compileProfileShaders NO-SOURCE > Task :app_installer:generateProfileAssets UP-TO-DATE > Task :app_installer:packageProfileAssets > Task :connectivity_plus:mergeProfileShaders > Task :connectivity_plus:compileProfileShaders NO-SOURCE > Task :connectivity_plus:generateProfileAssets UP-TO-DATE > Task :connectivity_plus:packageProfileAssets > Task :cr_file_saver:mergeProfileShaders > Task :cr_file_saver:compileProfileShaders NO-SOURCE > Task :cr_file_saver:generateProfileAssets UP-TO-DATE > Task :cr_file_saver:packageProfileAssets > Task :device_apps:mergeProfileShaders > Task :device_apps:compileProfileShaders NO-SOURCE > Task :device_apps:generateProfileAssets UP-TO-DATE > Task :device_apps:packageProfileAssets > Task :device_info_plus:mergeProfileShaders > Task :device_info_plus:compileProfileShaders NO-SOURCE > Task :device_info_plus:generateProfileAssets UP-TO-DATE > Task :device_info_plus:packageProfileAssets > Task :dynamic_color:mergeProfileShaders > Task :dynamic_color:compileProfileShaders NO-SOURCE > Task :dynamic_color:generateProfileAssets UP-TO-DATE > Task :dynamic_color:packageProfileAssets > Task :file_picker:mergeProfileShaders > Task :file_picker:compileProfileShaders NO-SOURCE > Task :file_picker:generateProfileAssets UP-TO-DATE > Task :file_picker:packageProfileAssets > Task :flutter_background:mergeProfileShaders > Task :flutter_background:compileProfileShaders NO-SOURCE > Task :flutter_background:generateProfileAssets UP-TO-DATE > Task :flutter_background:packageProfileAssets > Task :flutter_local_notifications:mergeProfileShaders > Task :flutter_local_notifications:compileProfileShaders NO-SOURCE > Task :flutter_local_notifications:generateProfileAssets UP-TO-DATE > Task :flutter_local_notifications:packageProfileAssets > Task :flutter_plugin_android_lifecycle:mergeProfileShaders > Task :flutter_plugin_android_lifecycle:compileProfileShaders NO-SOURCE > Task :flutter_plugin_android_lifecycle:generateProfileAssets UP-TO-DATE > Task :flutter_plugin_android_lifecycle:packageProfileAssets > Task :flutter_statusbarcolor_ns:mergeProfileShaders > Task :flutter_statusbarcolor_ns:compileProfileShaders NO-SOURCE > Task :flutter_statusbarcolor_ns:generateProfileAssets UP-TO-DATE > Task :flutter_statusbarcolor_ns:packageProfileAssets > Task :fluttertoast:mergeProfileShaders > Task :fluttertoast:compileProfileShaders NO-SOURCE > Task :fluttertoast:generateProfileAssets UP-TO-DATE > Task :fluttertoast:packageProfileAssets > Task :logcat:mergeProfileShaders > Task :logcat:compileProfileShaders NO-SOURCE > Task :logcat:generateProfileAssets UP-TO-DATE > Task :logcat:packageProfileAssets > Task :package_info_plus:mergeProfileShaders > Task :package_info_plus:compileProfileShaders NO-SOURCE > Task :package_info_plus:generateProfileAssets UP-TO-DATE > Task :package_info_plus:packageProfileAssets > Task :path_provider_android:mergeProfileShaders > Task :path_provider_android:compileProfileShaders NO-SOURCE > Task :path_provider_android:generateProfileAssets UP-TO-DATE > Task :path_provider_android:packageProfileAssets > Task :permission_handler_android:mergeProfileShaders > Task :permission_handler_android:compileProfileShaders NO-SOURCE > Task :permission_handler_android:generateProfileAssets UP-TO-DATE > Task :permission_handler_android:packageProfileAssets > Task :root:mergeProfileShaders > Task :root:compileProfileShaders NO-SOURCE > Task :root:generateProfileAssets UP-TO-DATE > Task :root:packageProfileAssets > Task :share_extend:mergeProfileShaders > Task :share_extend:compileProfileShaders NO-SOURCE > Task :share_extend:generateProfileAssets UP-TO-DATE > Task :share_extend:packageProfileAssets > Task :shared_preferences_android:mergeProfileShaders > Task :shared_preferences_android:compileProfileShaders NO-SOURCE > Task :shared_preferences_android:generateProfileAssets UP-TO-DATE > Task :shared_preferences_android:packageProfileAssets > Task :sqflite:mergeProfileShaders > Task :sqflite:compileProfileShaders NO-SOURCE > Task :sqflite:generateProfileAssets UP-TO-DATE > Task :sqflite:packageProfileAssets > Task :url_launcher_android:mergeProfileShaders > Task :url_launcher_android:compileProfileShaders NO-SOURCE > Task :url_launcher_android:generateProfileAssets UP-TO-DATE > Task :url_launcher_android:packageProfileAssets > Task :wakelock:mergeProfileShaders > Task :wakelock:compileProfileShaders NO-SOURCE > Task :wakelock:generateProfileAssets UP-TO-DATE > Task :wakelock:packageProfileAssets > Task :app:mergeProfileAssets > Task :app:copyFlutterAssetsProfile > Task :app:generateProfileResValues > Task :app:generateProfileResources > Task :app_installer:compileProfileRenderscript NO-SOURCE > Task :app_installer:generateProfileResValues > Task :app_installer:generateProfileResources > Task :app_installer:packageProfileResources > Task :connectivity_plus:compileProfileRenderscript NO-SOURCE > Task :connectivity_plus:generateProfileResValues > Task :connectivity_plus:generateProfileResources > Task :connectivity_plus:packageProfileResources > Task :cr_file_saver:compileProfileRenderscript NO-SOURCE > Task :cr_file_saver:generateProfileResValues > Task :cr_file_saver:generateProfileResources > Task :cr_file_saver:packageProfileResources > Task :device_apps:compileProfileRenderscript NO-SOURCE > Task :device_apps:generateProfileResValues > Task :device_apps:generateProfileResources > Task :device_apps:packageProfileResources > Task :device_info_plus:compileProfileRenderscript NO-SOURCE > Task :device_info_plus:generateProfileResValues > Task :device_info_plus:generateProfileResources > Task :device_info_plus:packageProfileResources > Task :dynamic_color:compileProfileRenderscript NO-SOURCE > Task :dynamic_color:generateProfileResValues > Task :dynamic_color:generateProfileResources > Task :dynamic_color:packageProfileResources > Task :file_picker:compileProfileRenderscript NO-SOURCE > Task :file_picker:generateProfileResValues > Task :file_picker:generateProfileResources > Task :file_picker:packageProfileResources > Task :flutter_background:compileProfileRenderscript NO-SOURCE > Task :flutter_background:generateProfileResValues > Task :flutter_background:generateProfileResources > Task :flutter_background:packageProfileResources > Task :flutter_local_notifications:compileProfileRenderscript NO-SOURCE > Task :flutter_local_notifications:generateProfileResValues > Task :flutter_local_notifications:generateProfileResources > Task :flutter_local_notifications:packageProfileResources > Task :flutter_plugin_android_lifecycle:compileProfileRenderscript NO-SOURCE > Task :flutter_plugin_android_lifecycle:generateProfileResValues > Task :flutter_plugin_android_lifecycle:generateProfileResources > Task :flutter_plugin_android_lifecycle:packageProfileResources > Task :flutter_statusbarcolor_ns:compileProfileRenderscript NO-SOURCE > Task :flutter_statusbarcolor_ns:generateProfileResValues > Task :flutter_statusbarcolor_ns:generateProfileResources > Task :flutter_statusbarcolor_ns:packageProfileResources > Task :fluttertoast:compileProfileRenderscript NO-SOURCE > Task :fluttertoast:generateProfileResValues > Task :fluttertoast:generateProfileResources > Task :fluttertoast:packageProfileResources > Task :logcat:compileProfileRenderscript NO-SOURCE > Task :logcat:generateProfileResValues > Task :logcat:generateProfileResources > Task :logcat:packageProfileResources > Task :package_info_plus:compileProfileRenderscript NO-SOURCE > Task :package_info_plus:generateProfileResValues > Task :package_info_plus:generateProfileResources > Task :package_info_plus:packageProfileResources > Task :path_provider_android:compileProfileRenderscript NO-SOURCE > Task :path_provider_android:generateProfileResValues > Task :path_provider_android:generateProfileResources > Task :path_provider_android:packageProfileResources > Task :permission_handler_android:compileProfileRenderscript NO-SOURCE > Task :permission_handler_android:generateProfileResValues > Task :permission_handler_android:generateProfileResources > Task :permission_handler_android:packageProfileResources > Task :root:compileProfileRenderscript NO-SOURCE > Task :root:generateProfileResValues > Task :root:generateProfileResources > Task :root:packageProfileResources > Task :share_extend:compileProfileRenderscript NO-SOURCE > Task :share_extend:generateProfileResValues > Task :share_extend:generateProfileResources > Task :share_extend:packageProfileResources > Task :shared_preferences_android:compileProfileRenderscript NO-SOURCE > Task :shared_preferences_android:generateProfileResValues > Task :shared_preferences_android:generateProfileResources > Task :shared_preferences_android:packageProfileResources > Task :sqflite:compileProfileRenderscript NO-SOURCE > Task :sqflite:generateProfileResValues > Task :sqflite:generateProfileResources > Task :sqflite:packageProfileResources > Task :url_launcher_android:compileProfileRenderscript NO-SOURCE > Task :url_launcher_android:generateProfileResValues > Task :url_launcher_android:generateProfileResources > Task :url_launcher_android:packageProfileResources > Task :wakelock:compileProfileRenderscript NO-SOURCE > Task :wakelock:generateProfileResValues > Task :wakelock:generateProfileResources > Task :wakelock:packageProfileResources > Task :app:createProfileCompatibleScreenManifests > Task :app:extractDeepLinksProfile > Task :app_installer:extractDeepLinksProfile > Task :connectivity_plus:extractDeepLinksProfile > Task :cr_file_saver:extractDeepLinksProfile > Task :app_installer:processProfileManifest > Task :connectivity_plus:processProfileManifest > Task :cr_file_saver:processProfileManifest > Task :device_apps:extractDeepLinksProfile > Task :device_info_plus:extractDeepLinksProfile > Task :dynamic_color:extractDeepLinksProfile > Task :device_apps:processProfileManifest > Task :device_info_plus:processProfileManifest > Task :file_picker:extractDeepLinksProfile > Task :dynamic_color:processProfileManifest > Task :flutter_background:extractDeepLinksProfile > Task :file_picker:processProfileManifest > Task :flutter_local_notifications:extractDeepLinksProfile > Task :flutter_background:processProfileManifest > Task :flutter_plugin_android_lifecycle:extractDeepLinksProfile > Task :app:mergeProfileResources > Task :flutter_statusbarcolor_ns:extractDeepLinksProfile > Task :flutter_local_notifications:processProfileManifest > Task :fluttertoast:extractDeepLinksProfile > Task :flutter_plugin_android_lifecycle:processProfileManifest > Task :flutter_statusbarcolor_ns:processProfileManifest > Task :logcat:extractDeepLinksProfile > Task :fluttertoast:processProfileManifest > Task :package_info_plus:extractDeepLinksProfile > Task :logcat:processProfileManifest > Task :path_provider_android:extractDeepLinksProfile > Task :package_info_plus:processProfileManifest > Task :permission_handler_android:extractDeepLinksProfile > Task :path_provider_android:processProfileManifest > Task :root:extractDeepLinksProfile > Task :permission_handler_android:processProfileManifest > Task :share_extend:extractDeepLinksProfile > Task :root:processProfileManifest > Task :shared_preferences_android:extractDeepLinksProfile > Task :share_extend:processProfileManifest > Task :sqflite:extractDeepLinksProfile > Task :url_launcher_android:extractDeepLinksProfile > Task :shared_preferences_android:processProfileManifest > Task :sqflite:processProfileManifest > Task :wakelock:extractDeepLinksProfile > Task :url_launcher_android:processProfileManifest > Task :app_installer:compileProfileLibraryResources > Task :wakelock:processProfileManifest > Task :connectivity_plus:compileProfileLibraryResources > Task :app_installer:parseProfileLocalResources > Task :connectivity_plus:parseProfileLocalResources > Task :app:processProfileMainManifest > Task :app:processProfileManifest > Task :app_installer:generateProfileRFile > Task :cr_file_saver:compileProfileLibraryResources > Task :app:processProfileManifestForPackage > Task :cr_file_saver:parseProfileLocalResources > Task :device_apps:compileProfileLibraryResources > Task :connectivity_plus:generateProfileRFile > Task :device_apps:parseProfileLocalResources > Task :cr_file_saver:generateProfileRFile > Task :device_info_plus:compileProfileLibraryResources > Task :device_info_plus:parseProfileLocalResources > Task :dynamic_color:compileProfileLibraryResources > Task :device_apps:generateProfileRFile > Task :dynamic_color:parseProfileLocalResources > Task :device_info_plus:generateProfileRFile > Task :file_picker:compileProfileLibraryResources > Task :file_picker:parseProfileLocalResources > Task :dynamic_color:generateProfileRFile > Task :flutter_background:compileProfileLibraryResources > Task :flutter_plugin_android_lifecycle:parseProfileLocalResources > Task :flutter_background:parseProfileLocalResources > Task :flutter_plugin_android_lifecycle:generateProfileRFile > Task :flutter_background:generateProfileRFile > Task :flutter_local_notifications:compileProfileLibraryResources > Task :flutter_plugin_android_lifecycle:compileProfileLibraryResources > Task :flutter_local_notifications:parseProfileLocalResources > Task :file_picker:generateProfileRFile > Task :flutter_statusbarcolor_ns:compileProfileLibraryResources > Task :fluttertoast:compileProfileLibraryResources > Task :flutter_statusbarcolor_ns:parseProfileLocalResources > Task :flutter_local_notifications:generateProfileRFile > Task :fluttertoast:parseProfileLocalResources > Task :flutter_statusbarcolor_ns:generateProfileRFile > Task :fluttertoast:generateProfileRFile > Task :logcat:compileProfileLibraryResources > Task :package_info_plus:compileProfileLibraryResources > Task :logcat:parseProfileLocalResources > Task :package_info_plus:parseProfileLocalResources > Task :path_provider_android:compileProfileLibraryResources > Task :logcat:generateProfileRFile > Task :path_provider_android:parseProfileLocalResources > Task :permission_handler_android:compileProfileLibraryResources > Task :permission_handler_android:parseProfileLocalResources > Task :package_info_plus:generateProfileRFile > Task :path_provider_android:generateProfileRFile > Task :root:compileProfileLibraryResources > Task :root:parseProfileLocalResources > Task :share_extend:compileProfileLibraryResources > Task :permission_handler_android:generateProfileRFile > Task :share_extend:parseProfileLocalResources > Task :root:generateProfileRFile > Task :shared_preferences_android:compileProfileLibraryResources > Task :share_extend:generateProfileRFile > Task :sqflite:compileProfileLibraryResources > Task :shared_preferences_android:parseProfileLocalResources > Task :sqflite:parseProfileLocalResources > Task :shared_preferences_android:generateProfileRFile > Task :url_launcher_android:compileProfileLibraryResources > Task :wakelock:compileProfileLibraryResources > Task :url_launcher_android:parseProfileLocalResources > Task :sqflite:generateProfileRFile > Task :wakelock:parseProfileLocalResources > Task :app_installer:generateProfileBuildConfig > Task :url_launcher_android:generateProfileRFile > Task :app_installer:javaPreCompileProfile > Task :connectivity_plus:generateProfileBuildConfig > Task :wakelock:generateProfileRFile > Task :app_installer:compileProfileJavaWithJavac Note: /home/aun/.pub-cache/hosted/pub.dev/app_installer-1.1.0/android/src/main/java/com/zero/app_installer/AppInstallerPlugin.java uses or overrides a deprecated API. Note: Recompile with -Xlint:deprecation for details. > Task :app_installer:bundleLibCompileToJarProfile > Task :connectivity_plus:javaPreCompileProfile > Task :cr_file_saver:generateProfileBuildConfig > Task :connectivity_plus:compileProfileJavaWithJavac > Task :connectivity_plus:bundleLibCompileToJarProfile > Task :cr_file_saver:javaPreCompileProfile > Task :device_apps:generateProfileBuildConfig > Task :device_apps:javaPreCompileProfile > Task :device_info_plus:generateProfileBuildConfig > Task :device_apps:compileProfileJavaWithJavac Note: /home/aun/.pub-cache/git/flutter_plugin_device_apps-0f0558df6fb2c9a3ed62529835704df010eeb56c/android/src/main/java/fr/g123k/deviceapps/DeviceAppsPlugin.java uses or overrides a deprecated API. Note: Recompile with -Xlint:deprecation for details. > Task :device_apps:bundleLibCompileToJarProfile > Task :device_info_plus:javaPreCompileProfile > Task :dynamic_color:generateProfileBuildConfig > Task :file_picker:generateProfileBuildConfig > Task :dynamic_color:javaPreCompileProfile > Task :flutter_plugin_android_lifecycle:generateProfileBuildConfig > Task :file_picker:javaPreCompileProfile > Task :flutter_plugin_android_lifecycle:javaPreCompileProfile > Task :flutter_background:generateProfileBuildConfig > Task :flutter_plugin_android_lifecycle:compileProfileJavaWithJavac > Task :flutter_plugin_android_lifecycle:bundleLibCompileToJarProfile > Task :file_picker:compileProfileJavaWithJavac Note: /home/aun/.pub-cache/git/flutter_file_picker-f4a8faac83df23506f310a1b49a1861df228a54a/android/src/main/java/com/mr/flutter/plugin/filepicker/FilePickerDelegate.java uses or overrides a deprecated API. Note: Recompile with -Xlint:deprecation for details. > Task :flutter_local_notifications:generateProfileBuildConfig > Task :flutter_background:javaPreCompileProfile > Task :file_picker:bundleLibCompileToJarProfile > Task :flutter_statusbarcolor_ns:generateProfileBuildConfig > Task :flutter_local_notifications:javaPreCompileProfile > Task :app:processProfileResources > Task :dynamic_color:compileProfileKotlin > Task :device_info_plus:compileProfileKotlin w: /home/aun/.pub-cache/hosted/pub.dev/device_info_plus-8.2.2/android/src/main/kotlin/dev/fluttercommunity/plus/device_info/MethodCallHandlerImpl.kt: (67, 50): 'getter for defaultDisplay: Display!' is deprecated. Deprecated in Java w: /home/aun/.pub-cache/hosted/pub.dev/device_info_plus-8.2.2/android/src/main/kotlin/dev/fluttercommunity/plus/device_info/MethodCallHandlerImpl.kt: (70, 25): 'getRealMetrics(DisplayMetrics!): Unit' is deprecated. Deprecated in Java w: /home/aun/.pub-cache/hosted/pub.dev/device_info_plus-8.2.2/android/src/main/kotlin/dev/fluttercommunity/plus/device_info/MethodCallHandlerImpl.kt: (72, 25): 'getMetrics(DisplayMetrics!): Unit' is deprecated. Deprecated in Java w: /home/aun/.pub-cache/hosted/pub.dev/device_info_plus-8.2.2/android/src/main/kotlin/dev/fluttercommunity/plus/device_info/MethodCallHandlerImpl.kt: (89, 47): 'SERIAL: String!' is deprecated. Deprecated in Java > Task :flutter_statusbarcolor_ns:compileProfileKotlin w: /home/aun/.pub-cache/hosted/pub.dev/flutter_statusbarcolor_ns-0.5.0/android/src/main/kotlin/com/sameer/flutterstatusbarcolor/flutterstatusbarcolor/FlutterStatusbarcolorPlugin.kt: (67, 53): 'setter for systemUiVisibility: Int' is deprecated. Deprecated in Java w: /home/aun/.pub-cache/hosted/pub.dev/flutter_statusbarcolor_ns-0.5.0/android/src/main/kotlin/com/sameer/flutterstatusbarcolor/flutterstatusbarcolor/FlutterStatusbarcolorPlugin.kt: (67, 102): 'getter for systemUiVisibility: Int' is deprecated. Deprecated in Java w: /home/aun/.pub-cache/hosted/pub.dev/flutter_statusbarcolor_ns-0.5.0/android/src/main/kotlin/com/sameer/flutterstatusbarcolor/flutterstatusbarcolor/FlutterStatusbarcolorPlugin.kt: (67, 130): 'SYSTEM_UI_FLAG_LIGHT_STATUS_BAR: Int' is deprecated. Deprecated in Java w: /home/aun/.pub-cache/hosted/pub.dev/flutter_statusbarcolor_ns-0.5.0/android/src/main/kotlin/com/sameer/flutterstatusbarcolor/flutterstatusbarcolor/FlutterStatusbarcolorPlugin.kt: (69, 53): 'setter for systemUiVisibility: Int' is deprecated. Deprecated in Java w: /home/aun/.pub-cache/hosted/pub.dev/flutter_statusbarcolor_ns-0.5.0/android/src/main/kotlin/com/sameer/flutterstatusbarcolor/flutterstatusbarcolor/FlutterStatusbarcolorPlugin.kt: (69, 102): 'getter for systemUiVisibility: Int' is deprecated. Deprecated in Java w: /home/aun/.pub-cache/hosted/pub.dev/flutter_statusbarcolor_ns-0.5.0/android/src/main/kotlin/com/sameer/flutterstatusbarcolor/flutterstatusbarcolor/FlutterStatusbarcolorPlugin.kt: (69, 129): 'SYSTEM_UI_FLAG_LIGHT_STATUS_BAR: Int' is deprecated. Deprecated in Java w: /home/aun/.pub-cache/hosted/pub.dev/flutter_statusbarcolor_ns-0.5.0/android/src/main/kotlin/com/sameer/flutterstatusbarcolor/flutterstatusbarcolor/FlutterStatusbarcolorPlugin.kt: (100, 53): 'setter for systemUiVisibility: Int' is deprecated. Deprecated in Java w: /home/aun/.pub-cache/hosted/pub.dev/flutter_statusbarcolor_ns-0.5.0/android/src/main/kotlin/com/sameer/flutterstatusbarcolor/flutterstatusbarcolor/FlutterStatusbarcolorPlugin.kt: (100, 102): 'getter for systemUiVisibility: Int' is deprecated. Deprecated in Java w: /home/aun/.pub-cache/hosted/pub.dev/flutter_statusbarcolor_ns-0.5.0/android/src/main/kotlin/com/sameer/flutterstatusbarcolor/flutterstatusbarcolor/FlutterStatusbarcolorPlugin.kt: (100, 130): 'SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR: Int' is deprecated. Deprecated in Java w: /home/aun/.pub-cache/hosted/pub.dev/flutter_statusbarcolor_ns-0.5.0/android/src/main/kotlin/com/sameer/flutterstatusbarcolor/flutterstatusbarcolor/FlutterStatusbarcolorPlugin.kt: (102, 53): 'setter for systemUiVisibility: Int' is deprecated. Deprecated in Java w: /home/aun/.pub-cache/hosted/pub.dev/flutter_statusbarcolor_ns-0.5.0/android/src/main/kotlin/com/sameer/flutterstatusbarcolor/flutterstatusbarcolor/FlutterStatusbarcolorPlugin.kt: (102, 102): 'getter for systemUiVisibility: Int' is deprecated. Deprecated in Java w: /home/aun/.pub-cache/hosted/pub.dev/flutter_statusbarcolor_ns-0.5.0/android/src/main/kotlin/com/sameer/flutterstatusbarcolor/flutterstatusbarcolor/FlutterStatusbarcolorPlugin.kt: (102, 129): 'SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR: Int' is deprecated. Deprecated in Java > Task :flutter_local_notifications:compileProfileJavaWithJavac > Task :device_info_plus:compileProfileJavaWithJavac > Task :device_info_plus:bundleLibCompileToJarProfile > Task :dynamic_color:compileProfileJavaWithJavac > Task :dynamic_color:bundleLibCompileToJarProfile > Task :flutter_statusbarcolor_ns:javaPreCompileProfile > Task :fluttertoast:generateProfileBuildConfig > Task :flutter_local_notifications:bundleLibCompileToJarProfile > Task :flutter_statusbarcolor_ns:compileProfileJavaWithJavac > Task :flutter_statusbarcolor_ns:bundleLibCompileToJarProfile > Task :cr_file_saver:compileProfileKotlin > Task :cr_file_saver:compileProfileJavaWithJavac > Task :cr_file_saver:bundleLibCompileToJarProfile > Task :logcat:generateProfileBuildConfig > Task :fluttertoast:javaPreCompileProfile > Task :package_info_plus:generateProfileBuildConfig > Task :logcat:javaPreCompileProfile > Task :logcat:compileProfileJavaWithJavac > Task :logcat:bundleLibCompileToJarProfile > Task :package_info_plus:javaPreCompileProfile > Task :path_provider_android:generateProfileBuildConfig > Task :path_provider_android:javaPreCompileProfile > Task :permission_handler_android:generateProfileBuildConfig > Task :flutter_background:compileProfileKotlin w: /home/aun/.pub-cache/hosted/pub.dev/flutter_background-1.2.0/android/src/main/kotlin/de/julianassmann/flutter_background/FlutterBackgroundPlugin.kt: (18, 48): 'Registrar' is deprecated. Deprecated in Java w: /home/aun/.pub-cache/hosted/pub.dev/flutter_background-1.2.0/android/src/main/kotlin/de/julianassmann/flutter_background/FlutterBackgroundPlugin.kt: (28, 33): 'Registrar' is deprecated. Deprecated in Java w: /home/aun/.pub-cache/hosted/pub.dev/flutter_background-1.2.0/android/src/main/kotlin/de/julianassmann/flutter_background/IsolateHolderService.kt: (122, 51): 'WIFI_MODE_FULL: Int' is deprecated. Deprecated in Java > Task :path_provider_android:compileProfileJavaWithJavac > Task :flutter_background:compileProfileJavaWithJavac > Task :root:generateProfileBuildConfig > Task :path_provider_android:bundleLibCompileToJarProfile > Task :flutter_background:bundleLibCompileToJarProfile > Task :permission_handler_android:javaPreCompileProfile > Task :root:javaPreCompileProfile > Task :share_extend:generateProfileBuildConfig > Task :fluttertoast:compileProfileKotlin w: /home/aun/.pub-cache/hosted/pub.dev/fluttertoast-8.2.2/android/src/main/kotlin/io/github/ponnamkarthik/toast/fluttertoast/FlutterToastPlugin.kt: (9, 48): 'Registrar' is deprecated. Deprecated in Java w: /home/aun/.pub-cache/hosted/pub.dev/fluttertoast-8.2.2/android/src/main/kotlin/io/github/ponnamkarthik/toast/fluttertoast/MethodCallHandlerImpl.kt: (55, 40): 'setColorFilter(Int, PorterDuff.Mode): Unit' is deprecated. Deprecated in Java w: /home/aun/.pub-cache/hosted/pub.dev/fluttertoast-8.2.2/android/src/main/kotlin/io/github/ponnamkarthik/toast/fluttertoast/MethodCallHandlerImpl.kt: (66, 29): 'setter for view: View?' is deprecated. Deprecated in Java w: /home/aun/.pub-cache/hosted/pub.dev/fluttertoast-8.2.2/android/src/main/kotlin/io/github/ponnamkarthik/toast/fluttertoast/MethodCallHandlerImpl.kt: (71, 62): 'getter for view: View?' is deprecated. Deprecated in Java > Task :permission_handler_android:compileProfileJavaWithJavac > Task :fluttertoast:compileProfileJavaWithJavac > Task :package_info_plus:compileProfileKotlin > Task :fluttertoast:bundleLibCompileToJarProfile > Task :permission_handler_android:bundleLibCompileToJarProfile > Task :package_info_plus:compileProfileJavaWithJavac > Task :package_info_plus:bundleLibCompileToJarProfile > Task :root:compileProfileJavaWithJavac > Task :share_extend:javaPreCompileProfile > Task :root:bundleLibCompileToJarProfile > Task :shared_preferences_android:generateProfileBuildConfig > Task :share_extend:compileProfileJavaWithJavac Note: Some input files use or override a deprecated API. Note: Recompile with -Xlint:deprecation for details. > Task :shared_preferences_android:javaPreCompileProfile > Task :sqflite:generateProfileBuildConfig > Task :share_extend:bundleLibCompileToJarProfile > Task :shared_preferences_android:compileProfileJavaWithJavac > Task :shared_preferences_android:bundleLibCompileToJarProfile > Task :sqflite:javaPreCompileProfile > Task :url_launcher_android:generateProfileBuildConfig > Task :sqflite:compileProfileJavaWithJavac > Task :url_launcher_android:javaPreCompileProfile > Task :wakelock:generateProfileBuildConfig > Task :sqflite:bundleLibCompileToJarProfile > Task :url_launcher_android:compileProfileJavaWithJavac > Task :url_launcher_android:bundleLibCompileToJarProfile > Task :wakelock:javaPreCompileProfile > Task :app:preProfileUnitTestBuild UP-TO-DATE > Task :app:javaPreCompileProfile > Task :app:processProfileJavaRes NO-SOURCE > Task :app:javaPreCompileProfileUnitTest > Task :app:processProfileUnitTestJavaRes NO-SOURCE > Task :app_installer:processProfileJavaRes NO-SOURCE > Task :app_installer:bundleLibResProfile NO-SOURCE > Task :app_installer:bundleLibRuntimeToJarProfile > Task :connectivity_plus:processProfileJavaRes NO-SOURCE > Task :connectivity_plus:bundleLibResProfile NO-SOURCE > Task :cr_file_saver:processProfileJavaRes NO-SOURCE > Task :connectivity_plus:bundleLibRuntimeToJarProfile > Task :cr_file_saver:bundleLibResProfile > Task :device_apps:processProfileJavaRes NO-SOURCE > Task :device_apps:bundleLibResProfile NO-SOURCE > Task :device_info_plus:processProfileJavaRes NO-SOURCE > Task :cr_file_saver:bundleLibRuntimeToJarProfile > Task :device_apps:bundleLibRuntimeToJarProfile > Task :device_info_plus:bundleLibResProfile > Task :dynamic_color:processProfileJavaRes NO-SOURCE > Task :device_info_plus:bundleLibRuntimeToJarProfile > Task :dynamic_color:bundleLibResProfile > Task :file_picker:processProfileJavaRes NO-SOURCE > Task :file_picker:bundleLibResProfile NO-SOURCE > Task :dynamic_color:bundleLibRuntimeToJarProfile > Task :flutter_background:processProfileJavaRes NO-SOURCE > Task :file_picker:bundleLibRuntimeToJarProfile > Task :flutter_background:bundleLibResProfile > Task :flutter_local_notifications:processProfileJavaRes NO-SOURCE > Task :flutter_local_notifications:bundleLibResProfile NO-SOURCE > Task :flutter_background:bundleLibRuntimeToJarProfile > Task :flutter_plugin_android_lifecycle:processProfileJavaRes NO-SOURCE > Task :flutter_plugin_android_lifecycle:bundleLibResProfile NO-SOURCE > Task :flutter_local_notifications:bundleLibRuntimeToJarProfile > Task :flutter_statusbarcolor_ns:processProfileJavaRes NO-SOURCE > Task :flutter_plugin_android_lifecycle:bundleLibRuntimeToJarProfile > Task :fluttertoast:processProfileJavaRes NO-SOURCE > Task :flutter_statusbarcolor_ns:bundleLibResProfile > Task :flutter_statusbarcolor_ns:bundleLibRuntimeToJarProfile > Task :fluttertoast:bundleLibResProfile > Task :logcat:processProfileJavaRes NO-SOURCE > Task :fluttertoast:bundleLibRuntimeToJarProfile > Task :logcat:bundleLibResProfile NO-SOURCE > Task :package_info_plus:processProfileJavaRes NO-SOURCE > Task :logcat:bundleLibRuntimeToJarProfile > Task :package_info_plus:bundleLibResProfile > Task :path_provider_android:processProfileJavaRes NO-SOURCE > Task :path_provider_android:bundleLibResProfile NO-SOURCE > Task :package_info_plus:bundleLibRuntimeToJarProfile > Task :path_provider_android:bundleLibRuntimeToJarProfile > Task :permission_handler_android:processProfileJavaRes NO-SOURCE > Task :permission_handler_android:bundleLibResProfile NO-SOURCE > Task :root:processProfileJavaRes NO-SOURCE > Task :root:bundleLibResProfile NO-SOURCE > Task :permission_handler_android:bundleLibRuntimeToJarProfile > Task :share_extend:processProfileJavaRes NO-SOURCE > Task :share_extend:bundleLibResProfile NO-SOURCE > Task :root:bundleLibRuntimeToJarProfile > Task :shared_preferences_android:processProfileJavaRes NO-SOURCE > Task :shared_preferences_android:bundleLibResProfile NO-SOURCE > Task :share_extend:bundleLibRuntimeToJarProfile > Task :shared_preferences_android:bundleLibRuntimeToJarProfile > Task :sqflite:processProfileJavaRes NO-SOURCE > Task :sqflite:bundleLibResProfile NO-SOURCE > Task :url_launcher_android:processProfileJavaRes NO-SOURCE > Task :url_launcher_android:bundleLibResProfile NO-SOURCE > Task :wakelock:processProfileJavaRes NO-SOURCE > Task :url_launcher_android:bundleLibRuntimeToJarProfile > Task :app:buildKotlinToolingMetadata UP-TO-DATE > Task :sqflite:bundleLibRuntimeToJarProfile > Task :wakelock:compileProfileKotlin > Task :app:compileFlutterBuildRelease > Task :wakelock:compileProfileJavaWithJavac > Task :wakelock:bundleLibResProfile > Task :wakelock:bundleLibCompileToJarProfile > Task :app:packLibsflutterBuildRelease UP-TO-DATE > Task :wakelock:bundleLibRuntimeToJarProfile > Task :app:preReleaseBuild UP-TO-DATE > Task :app_installer:preReleaseBuild UP-TO-DATE > Task :app_installer:compileReleaseAidl NO-SOURCE > Task :connectivity_plus:preReleaseBuild UP-TO-DATE > Task :connectivity_plus:compileReleaseAidl NO-SOURCE > Task :cr_file_saver:preReleaseBuild UP-TO-DATE > Task :cr_file_saver:compileReleaseAidl NO-SOURCE > Task :device_apps:preReleaseBuild UP-TO-DATE > Task :device_apps:compileReleaseAidl NO-SOURCE > Task :device_info_plus:preReleaseBuild UP-TO-DATE > Task :device_info_plus:compileReleaseAidl NO-SOURCE > Task :dynamic_color:preReleaseBuild UP-TO-DATE > Task :dynamic_color:compileReleaseAidl NO-SOURCE > Task :file_picker:preReleaseBuild UP-TO-DATE > Task :flutter_plugin_android_lifecycle:preReleaseBuild UP-TO-DATE > Task :flutter_plugin_android_lifecycle:compileReleaseAidl NO-SOURCE > Task :file_picker:compileReleaseAidl NO-SOURCE > Task :flutter_background:preReleaseBuild UP-TO-DATE > Task :flutter_background:compileReleaseAidl NO-SOURCE > Task :flutter_local_notifications:preReleaseBuild UP-TO-DATE > Task :flutter_local_notifications:compileReleaseAidl NO-SOURCE > Task :flutter_statusbarcolor_ns:preReleaseBuild UP-TO-DATE > Task :flutter_statusbarcolor_ns:compileReleaseAidl NO-SOURCE > Task :fluttertoast:preReleaseBuild UP-TO-DATE > Task :fluttertoast:compileReleaseAidl NO-SOURCE > Task :logcat:preReleaseBuild UP-TO-DATE > Task :logcat:compileReleaseAidl NO-SOURCE > Task :package_info_plus:preReleaseBuild UP-TO-DATE > Task :package_info_plus:compileReleaseAidl NO-SOURCE > Task :path_provider_android:preReleaseBuild UP-TO-DATE > Task :path_provider_android:compileReleaseAidl NO-SOURCE > Task :permission_handler_android:preReleaseBuild UP-TO-DATE > Task :permission_handler_android:compileReleaseAidl NO-SOURCE > Task :root:preReleaseBuild UP-TO-DATE > Task :root:compileReleaseAidl NO-SOURCE > Task :share_extend:preReleaseBuild UP-TO-DATE > Task :share_extend:compileReleaseAidl NO-SOURCE > Task :shared_preferences_android:preReleaseBuild UP-TO-DATE > Task :shared_preferences_android:compileReleaseAidl NO-SOURCE > Task :sqflite:preReleaseBuild UP-TO-DATE > Task :sqflite:compileReleaseAidl NO-SOURCE > Task :url_launcher_android:preReleaseBuild UP-TO-DATE > Task :url_launcher_android:compileReleaseAidl NO-SOURCE > Task :wakelock:preReleaseBuild UP-TO-DATE > Task :wakelock:compileReleaseAidl NO-SOURCE > Task :app:compileReleaseAidl NO-SOURCE > Task :app_installer:packageReleaseRenderscript NO-SOURCE > Task :connectivity_plus:packageReleaseRenderscript NO-SOURCE > Task :cr_file_saver:packageReleaseRenderscript NO-SOURCE > Task :device_apps:packageReleaseRenderscript NO-SOURCE > Task :device_info_plus:packageReleaseRenderscript NO-SOURCE > Task :dynamic_color:packageReleaseRenderscript NO-SOURCE > Task :file_picker:packageReleaseRenderscript NO-SOURCE > Task :flutter_background:packageReleaseRenderscript NO-SOURCE > Task :flutter_local_notifications:packageReleaseRenderscript NO-SOURCE > Task :flutter_plugin_android_lifecycle:packageReleaseRenderscript NO-SOURCE > Task :flutter_statusbarcolor_ns:packageReleaseRenderscript NO-SOURCE > Task :fluttertoast:packageReleaseRenderscript NO-SOURCE > Task :logcat:packageReleaseRenderscript NO-SOURCE > Task :package_info_plus:packageReleaseRenderscript NO-SOURCE > Task :path_provider_android:packageReleaseRenderscript NO-SOURCE > Task :permission_handler_android:packageReleaseRenderscript NO-SOURCE > Task :root:packageReleaseRenderscript NO-SOURCE > Task :share_extend:packageReleaseRenderscript NO-SOURCE > Task :shared_preferences_android:packageReleaseRenderscript NO-SOURCE > Task :sqflite:packageReleaseRenderscript NO-SOURCE > Task :url_launcher_android:packageReleaseRenderscript NO-SOURCE > Task :wakelock:packageReleaseRenderscript NO-SOURCE > Task :app:compileReleaseRenderscript NO-SOURCE > Task :app:generateReleaseBuildConfig UP-TO-DATE > Task :app_installer:writeReleaseAarMetadata UP-TO-DATE > Task :connectivity_plus:writeReleaseAarMetadata UP-TO-DATE > Task :cr_file_saver:writeReleaseAarMetadata UP-TO-DATE > Task :device_apps:writeReleaseAarMetadata UP-TO-DATE > Task :device_info_plus:writeReleaseAarMetadata UP-TO-DATE > Task :dynamic_color:writeReleaseAarMetadata UP-TO-DATE > Task :file_picker:writeReleaseAarMetadata UP-TO-DATE > Task :flutter_background:writeReleaseAarMetadata UP-TO-DATE > Task :flutter_local_notifications:writeReleaseAarMetadata UP-TO-DATE > Task :flutter_plugin_android_lifecycle:writeReleaseAarMetadata UP-TO-DATE > Task :flutter_statusbarcolor_ns:writeReleaseAarMetadata UP-TO-DATE > Task :fluttertoast:writeReleaseAarMetadata UP-TO-DATE > Task :logcat:writeReleaseAarMetadata UP-TO-DATE > Task :package_info_plus:writeReleaseAarMetadata UP-TO-DATE > Task :path_provider_android:writeReleaseAarMetadata UP-TO-DATE > Task :permission_handler_android:writeReleaseAarMetadata UP-TO-DATE > Task :root:writeReleaseAarMetadata UP-TO-DATE > Task :share_extend:writeReleaseAarMetadata UP-TO-DATE > Task :shared_preferences_android:writeReleaseAarMetadata UP-TO-DATE > Task :sqflite:writeReleaseAarMetadata UP-TO-DATE > Task :url_launcher_android:writeReleaseAarMetadata UP-TO-DATE > Task :wakelock:writeReleaseAarMetadata UP-TO-DATE > Task :app:checkReleaseAarMetadata UP-TO-DATE > Task :app:cleanMergeReleaseAssets > Task :app:mergeReleaseShaders UP-TO-DATE > Task :app:compileReleaseShaders NO-SOURCE > Task :app:generateReleaseAssets UP-TO-DATE > Task :app_installer:mergeReleaseShaders UP-TO-DATE > Task :app_installer:compileReleaseShaders NO-SOURCE > Task :app_installer:generateReleaseAssets UP-TO-DATE > Task :app_installer:packageReleaseAssets UP-TO-DATE > Task :connectivity_plus:mergeReleaseShaders UP-TO-DATE > Task :connectivity_plus:compileReleaseShaders NO-SOURCE > Task :connectivity_plus:generateReleaseAssets UP-TO-DATE > Task :connectivity_plus:packageReleaseAssets UP-TO-DATE > Task :cr_file_saver:mergeReleaseShaders UP-TO-DATE > Task :cr_file_saver:compileReleaseShaders NO-SOURCE > Task :cr_file_saver:generateReleaseAssets UP-TO-DATE > Task :cr_file_saver:packageReleaseAssets UP-TO-DATE > Task :device_apps:mergeReleaseShaders UP-TO-DATE > Task :device_apps:compileReleaseShaders NO-SOURCE > Task :device_apps:generateReleaseAssets UP-TO-DATE > Task :device_apps:packageReleaseAssets UP-TO-DATE > Task :device_info_plus:mergeReleaseShaders UP-TO-DATE > Task :device_info_plus:compileReleaseShaders NO-SOURCE > Task :device_info_plus:generateReleaseAssets UP-TO-DATE > Task :device_info_plus:packageReleaseAssets UP-TO-DATE > Task :dynamic_color:mergeReleaseShaders UP-TO-DATE > Task :dynamic_color:compileReleaseShaders NO-SOURCE > Task :dynamic_color:generateReleaseAssets UP-TO-DATE > Task :dynamic_color:packageReleaseAssets UP-TO-DATE > Task :file_picker:mergeReleaseShaders UP-TO-DATE > Task :file_picker:compileReleaseShaders NO-SOURCE > Task :file_picker:generateReleaseAssets UP-TO-DATE > Task :file_picker:packageReleaseAssets UP-TO-DATE > Task :flutter_background:mergeReleaseShaders UP-TO-DATE > Task :flutter_background:compileReleaseShaders NO-SOURCE > Task :flutter_background:generateReleaseAssets UP-TO-DATE > Task :flutter_background:packageReleaseAssets UP-TO-DATE > Task :flutter_local_notifications:mergeReleaseShaders UP-TO-DATE > Task :flutter_local_notifications:compileReleaseShaders NO-SOURCE > Task :flutter_local_notifications:generateReleaseAssets UP-TO-DATE > Task :flutter_local_notifications:packageReleaseAssets UP-TO-DATE > Task :flutter_plugin_android_lifecycle:mergeReleaseShaders UP-TO-DATE > Task :flutter_plugin_android_lifecycle:compileReleaseShaders NO-SOURCE > Task :flutter_plugin_android_lifecycle:generateReleaseAssets UP-TO-DATE > Task :flutter_plugin_android_lifecycle:packageReleaseAssets UP-TO-DATE > Task :flutter_statusbarcolor_ns:mergeReleaseShaders UP-TO-DATE > Task :flutter_statusbarcolor_ns:compileReleaseShaders NO-SOURCE > Task :flutter_statusbarcolor_ns:generateReleaseAssets UP-TO-DATE > Task :flutter_statusbarcolor_ns:packageReleaseAssets UP-TO-DATE > Task :fluttertoast:mergeReleaseShaders UP-TO-DATE > Task :fluttertoast:compileReleaseShaders NO-SOURCE > Task :fluttertoast:generateReleaseAssets UP-TO-DATE > Task :fluttertoast:packageReleaseAssets UP-TO-DATE > Task :logcat:mergeReleaseShaders UP-TO-DATE > Task :logcat:compileReleaseShaders NO-SOURCE > Task :logcat:generateReleaseAssets UP-TO-DATE > Task :logcat:packageReleaseAssets UP-TO-DATE > Task :package_info_plus:mergeReleaseShaders UP-TO-DATE > Task :package_info_plus:compileReleaseShaders NO-SOURCE > Task :package_info_plus:generateReleaseAssets UP-TO-DATE > Task :package_info_plus:packageReleaseAssets UP-TO-DATE > Task :path_provider_android:mergeReleaseShaders UP-TO-DATE > Task :path_provider_android:compileReleaseShaders NO-SOURCE > Task :path_provider_android:generateReleaseAssets UP-TO-DATE > Task :path_provider_android:packageReleaseAssets UP-TO-DATE > Task :permission_handler_android:mergeReleaseShaders UP-TO-DATE > Task :permission_handler_android:compileReleaseShaders NO-SOURCE > Task :permission_handler_android:generateReleaseAssets UP-TO-DATE > Task :permission_handler_android:packageReleaseAssets UP-TO-DATE > Task :root:mergeReleaseShaders UP-TO-DATE > Task :root:compileReleaseShaders NO-SOURCE > Task :root:generateReleaseAssets UP-TO-DATE > Task :root:packageReleaseAssets UP-TO-DATE > Task :share_extend:mergeReleaseShaders UP-TO-DATE > Task :share_extend:compileReleaseShaders NO-SOURCE > Task :share_extend:generateReleaseAssets UP-TO-DATE > Task :share_extend:packageReleaseAssets UP-TO-DATE > Task :shared_preferences_android:mergeReleaseShaders UP-TO-DATE > Task :shared_preferences_android:compileReleaseShaders NO-SOURCE > Task :shared_preferences_android:generateReleaseAssets UP-TO-DATE > Task :shared_preferences_android:packageReleaseAssets UP-TO-DATE > Task :sqflite:mergeReleaseShaders UP-TO-DATE > Task :sqflite:compileReleaseShaders NO-SOURCE > Task :sqflite:generateReleaseAssets UP-TO-DATE > Task :sqflite:packageReleaseAssets UP-TO-DATE > Task :url_launcher_android:mergeReleaseShaders UP-TO-DATE > Task :url_launcher_android:compileReleaseShaders NO-SOURCE > Task :url_launcher_android:generateReleaseAssets UP-TO-DATE > Task :url_launcher_android:packageReleaseAssets UP-TO-DATE > Task :wakelock:mergeReleaseShaders UP-TO-DATE > Task :wakelock:compileReleaseShaders NO-SOURCE > Task :wakelock:generateReleaseAssets UP-TO-DATE > Task :wakelock:packageReleaseAssets UP-TO-DATE > Task :app:mergeReleaseAssets > Task :app:copyFlutterAssetsRelease > Task :app:generateReleaseResValues UP-TO-DATE > Task :app:generateReleaseResources UP-TO-DATE > Task :app_installer:compileReleaseRenderscript NO-SOURCE > Task :app_installer:generateReleaseResValues UP-TO-DATE > Task :app_installer:generateReleaseResources UP-TO-DATE > Task :app_installer:packageReleaseResources UP-TO-DATE > Task :connectivity_plus:compileReleaseRenderscript NO-SOURCE > Task :connectivity_plus:generateReleaseResValues UP-TO-DATE > Task :connectivity_plus:generateReleaseResources UP-TO-DATE > Task :connectivity_plus:packageReleaseResources UP-TO-DATE > Task :cr_file_saver:compileReleaseRenderscript NO-SOURCE > Task :cr_file_saver:generateReleaseResValues UP-TO-DATE > Task :cr_file_saver:generateReleaseResources UP-TO-DATE > Task :cr_file_saver:packageReleaseResources UP-TO-DATE > Task :device_apps:compileReleaseRenderscript NO-SOURCE > Task :device_apps:generateReleaseResValues UP-TO-DATE > Task :device_apps:generateReleaseResources UP-TO-DATE > Task :device_apps:packageReleaseResources UP-TO-DATE > Task :device_info_plus:compileReleaseRenderscript NO-SOURCE > Task :device_info_plus:generateReleaseResValues UP-TO-DATE > Task :device_info_plus:generateReleaseResources UP-TO-DATE > Task :device_info_plus:packageReleaseResources UP-TO-DATE > Task :dynamic_color:compileReleaseRenderscript NO-SOURCE > Task :dynamic_color:generateReleaseResValues UP-TO-DATE > Task :dynamic_color:generateReleaseResources UP-TO-DATE > Task :dynamic_color:packageReleaseResources UP-TO-DATE > Task :file_picker:compileReleaseRenderscript NO-SOURCE > Task :file_picker:generateReleaseResValues UP-TO-DATE > Task :file_picker:generateReleaseResources UP-TO-DATE > Task :file_picker:packageReleaseResources UP-TO-DATE > Task :flutter_background:compileReleaseRenderscript NO-SOURCE > Task :flutter_background:generateReleaseResValues UP-TO-DATE > Task :flutter_background:generateReleaseResources UP-TO-DATE > Task :flutter_background:packageReleaseResources UP-TO-DATE > Task :flutter_local_notifications:compileReleaseRenderscript NO-SOURCE > Task :flutter_local_notifications:generateReleaseResValues UP-TO-DATE > Task :flutter_local_notifications:generateReleaseResources UP-TO-DATE > Task :flutter_local_notifications:packageReleaseResources UP-TO-DATE > Task :flutter_plugin_android_lifecycle:compileReleaseRenderscript NO-SOURCE > Task :flutter_plugin_android_lifecycle:generateReleaseResValues UP-TO-DATE > Task :flutter_plugin_android_lifecycle:generateReleaseResources UP-TO-DATE > Task :flutter_plugin_android_lifecycle:packageReleaseResources UP-TO-DATE > Task :flutter_statusbarcolor_ns:compileReleaseRenderscript NO-SOURCE > Task :flutter_statusbarcolor_ns:generateReleaseResValues UP-TO-DATE > Task :flutter_statusbarcolor_ns:generateReleaseResources UP-TO-DATE > Task :flutter_statusbarcolor_ns:packageReleaseResources UP-TO-DATE > Task :fluttertoast:compileReleaseRenderscript NO-SOURCE > Task :fluttertoast:generateReleaseResValues UP-TO-DATE > Task :fluttertoast:generateReleaseResources UP-TO-DATE > Task :fluttertoast:packageReleaseResources UP-TO-DATE > Task :logcat:compileReleaseRenderscript NO-SOURCE > Task :logcat:generateReleaseResValues UP-TO-DATE > Task :logcat:generateReleaseResources UP-TO-DATE > Task :logcat:packageReleaseResources UP-TO-DATE > Task :package_info_plus:compileReleaseRenderscript NO-SOURCE > Task :package_info_plus:generateReleaseResValues UP-TO-DATE > Task :package_info_plus:generateReleaseResources UP-TO-DATE > Task :package_info_plus:packageReleaseResources UP-TO-DATE > Task :path_provider_android:compileReleaseRenderscript NO-SOURCE > Task :path_provider_android:generateReleaseResValues UP-TO-DATE > Task :path_provider_android:generateReleaseResources UP-TO-DATE > Task :path_provider_android:packageReleaseResources UP-TO-DATE > Task :permission_handler_android:compileReleaseRenderscript NO-SOURCE > Task :permission_handler_android:generateReleaseResValues UP-TO-DATE > Task :permission_handler_android:generateReleaseResources UP-TO-DATE > Task :permission_handler_android:packageReleaseResources UP-TO-DATE > Task :root:compileReleaseRenderscript NO-SOURCE > Task :root:generateReleaseResValues UP-TO-DATE > Task :root:generateReleaseResources UP-TO-DATE > Task :root:packageReleaseResources UP-TO-DATE > Task :share_extend:compileReleaseRenderscript NO-SOURCE > Task :share_extend:generateReleaseResValues UP-TO-DATE > Task :share_extend:generateReleaseResources UP-TO-DATE > Task :share_extend:packageReleaseResources UP-TO-DATE > Task :shared_preferences_android:compileReleaseRenderscript NO-SOURCE > Task :shared_preferences_android:generateReleaseResValues UP-TO-DATE > Task :shared_preferences_android:generateReleaseResources UP-TO-DATE > Task :shared_preferences_android:packageReleaseResources UP-TO-DATE > Task :sqflite:compileReleaseRenderscript NO-SOURCE > Task :sqflite:generateReleaseResValues UP-TO-DATE > Task :sqflite:generateReleaseResources UP-TO-DATE > Task :sqflite:packageReleaseResources UP-TO-DATE > Task :url_launcher_android:compileReleaseRenderscript NO-SOURCE > Task :url_launcher_android:generateReleaseResValues UP-TO-DATE > Task :url_launcher_android:generateReleaseResources UP-TO-DATE > Task :url_launcher_android:packageReleaseResources UP-TO-DATE > Task :wakelock:compileReleaseRenderscript NO-SOURCE > Task :wakelock:generateReleaseResValues UP-TO-DATE > Task :wakelock:generateReleaseResources UP-TO-DATE > Task :wakelock:packageReleaseResources UP-TO-DATE > Task :app:mergeReleaseResources UP-TO-DATE > Task :app:createReleaseCompatibleScreenManifests UP-TO-DATE > Task :app:extractDeepLinksRelease UP-TO-DATE > Task :app_installer:extractDeepLinksRelease UP-TO-DATE > Task :app_installer:processReleaseManifest UP-TO-DATE > Task :connectivity_plus:extractDeepLinksRelease UP-TO-DATE > Task :connectivity_plus:processReleaseManifest UP-TO-DATE > Task :cr_file_saver:extractDeepLinksRelease UP-TO-DATE > Task :cr_file_saver:processReleaseManifest UP-TO-DATE > Task :device_apps:extractDeepLinksRelease UP-TO-DATE > Task :device_apps:processReleaseManifest UP-TO-DATE > Task :device_info_plus:extractDeepLinksRelease UP-TO-DATE > Task :device_info_plus:processReleaseManifest UP-TO-DATE > Task :dynamic_color:extractDeepLinksRelease UP-TO-DATE > Task :dynamic_color:processReleaseManifest UP-TO-DATE > Task :file_picker:extractDeepLinksRelease UP-TO-DATE > Task :file_picker:processReleaseManifest UP-TO-DATE > Task :flutter_background:extractDeepLinksRelease UP-TO-DATE > Task :flutter_background:processReleaseManifest UP-TO-DATE > Task :flutter_local_notifications:extractDeepLinksRelease UP-TO-DATE > Task :flutter_local_notifications:processReleaseManifest UP-TO-DATE > Task :flutter_plugin_android_lifecycle:extractDeepLinksRelease UP-TO-DATE > Task :flutter_plugin_android_lifecycle:processReleaseManifest UP-TO-DATE > Task :flutter_statusbarcolor_ns:extractDeepLinksRelease UP-TO-DATE > Task :flutter_statusbarcolor_ns:processReleaseManifest UP-TO-DATE > Task :fluttertoast:extractDeepLinksRelease UP-TO-DATE > Task :fluttertoast:processReleaseManifest UP-TO-DATE > Task :logcat:extractDeepLinksRelease UP-TO-DATE > Task :logcat:processReleaseManifest UP-TO-DATE > Task :package_info_plus:extractDeepLinksRelease UP-TO-DATE > Task :package_info_plus:processReleaseManifest UP-TO-DATE > Task :path_provider_android:extractDeepLinksRelease UP-TO-DATE > Task :path_provider_android:processReleaseManifest UP-TO-DATE > Task :permission_handler_android:extractDeepLinksRelease UP-TO-DATE > Task :permission_handler_android:processReleaseManifest UP-TO-DATE > Task :root:extractDeepLinksRelease UP-TO-DATE > Task :root:processReleaseManifest UP-TO-DATE > Task :share_extend:extractDeepLinksRelease UP-TO-DATE > Task :share_extend:processReleaseManifest UP-TO-DATE > Task :shared_preferences_android:extractDeepLinksRelease UP-TO-DATE > Task :shared_preferences_android:processReleaseManifest UP-TO-DATE > Task :sqflite:extractDeepLinksRelease UP-TO-DATE > Task :sqflite:processReleaseManifest UP-TO-DATE > Task :url_launcher_android:extractDeepLinksRelease UP-TO-DATE > Task :url_launcher_android:processReleaseManifest UP-TO-DATE > Task :wakelock:extractDeepLinksRelease UP-TO-DATE > Task :wakelock:processReleaseManifest UP-TO-DATE > Task :app:processReleaseMainManifest UP-TO-DATE > Task :app:processReleaseManifest UP-TO-DATE > Task :app:processReleaseManifestForPackage UP-TO-DATE > Task :app_installer:compileReleaseLibraryResources UP-TO-DATE > Task :app_installer:parseReleaseLocalResources UP-TO-DATE > Task :app_installer:generateReleaseRFile UP-TO-DATE > Task :connectivity_plus:compileReleaseLibraryResources UP-TO-DATE > Task :connectivity_plus:parseReleaseLocalResources UP-TO-DATE > Task :connectivity_plus:generateReleaseRFile UP-TO-DATE > Task :cr_file_saver:compileReleaseLibraryResources UP-TO-DATE > Task :cr_file_saver:parseReleaseLocalResources UP-TO-DATE > Task :cr_file_saver:generateReleaseRFile UP-TO-DATE > Task :device_apps:compileReleaseLibraryResources UP-TO-DATE > Task :device_apps:parseReleaseLocalResources UP-TO-DATE > Task :device_apps:generateReleaseRFile UP-TO-DATE > Task :device_info_plus:compileReleaseLibraryResources UP-TO-DATE > Task :device_info_plus:parseReleaseLocalResources UP-TO-DATE > Task :device_info_plus:generateReleaseRFile UP-TO-DATE > Task :dynamic_color:compileReleaseLibraryResources UP-TO-DATE > Task :dynamic_color:parseReleaseLocalResources UP-TO-DATE > Task :dynamic_color:generateReleaseRFile UP-TO-DATE > Task :file_picker:compileReleaseLibraryResources UP-TO-DATE > Task :file_picker:parseReleaseLocalResources UP-TO-DATE > Task :flutter_plugin_android_lifecycle:parseReleaseLocalResources UP-TO-DATE > Task :flutter_plugin_android_lifecycle:generateReleaseRFile UP-TO-DATE > Task :file_picker:generateReleaseRFile UP-TO-DATE > Task :flutter_background:compileReleaseLibraryResources UP-TO-DATE > Task :flutter_background:parseReleaseLocalResources UP-TO-DATE > Task :flutter_background:generateReleaseRFile UP-TO-DATE > Task :flutter_local_notifications:compileReleaseLibraryResources UP-TO-DATE > Task :flutter_local_notifications:parseReleaseLocalResources UP-TO-DATE > Task :flutter_local_notifications:generateReleaseRFile UP-TO-DATE > Task :flutter_plugin_android_lifecycle:compileReleaseLibraryResources UP-TO-DATE > Task :flutter_statusbarcolor_ns:compileReleaseLibraryResources UP-TO-DATE > Task :flutter_statusbarcolor_ns:parseReleaseLocalResources UP-TO-DATE > Task :flutter_statusbarcolor_ns:generateReleaseRFile UP-TO-DATE > Task :fluttertoast:compileReleaseLibraryResources UP-TO-DATE > Task :fluttertoast:parseReleaseLocalResources UP-TO-DATE > Task :fluttertoast:generateReleaseRFile UP-TO-DATE > Task :logcat:compileReleaseLibraryResources UP-TO-DATE > Task :logcat:parseReleaseLocalResources UP-TO-DATE > Task :logcat:generateReleaseRFile UP-TO-DATE > Task :package_info_plus:compileReleaseLibraryResources UP-TO-DATE > Task :package_info_plus:parseReleaseLocalResources UP-TO-DATE > Task :package_info_plus:generateReleaseRFile UP-TO-DATE > Task :path_provider_android:compileReleaseLibraryResources UP-TO-DATE > Task :path_provider_android:parseReleaseLocalResources UP-TO-DATE > Task :path_provider_android:generateReleaseRFile UP-TO-DATE > Task :permission_handler_android:compileReleaseLibraryResources UP-TO-DATE > Task :permission_handler_android:parseReleaseLocalResources UP-TO-DATE > Task :permission_handler_android:generateReleaseRFile UP-TO-DATE > Task :root:compileReleaseLibraryResources UP-TO-DATE > Task :root:parseReleaseLocalResources UP-TO-DATE > Task :root:generateReleaseRFile UP-TO-DATE > Task :share_extend:compileReleaseLibraryResources UP-TO-DATE > Task :share_extend:parseReleaseLocalResources UP-TO-DATE > Task :share_extend:generateReleaseRFile UP-TO-DATE > Task :shared_preferences_android:compileReleaseLibraryResources UP-TO-DATE > Task :shared_preferences_android:parseReleaseLocalResources UP-TO-DATE > Task :shared_preferences_android:generateReleaseRFile UP-TO-DATE > Task :sqflite:compileReleaseLibraryResources UP-TO-DATE > Task :sqflite:parseReleaseLocalResources UP-TO-DATE > Task :sqflite:generateReleaseRFile UP-TO-DATE > Task :url_launcher_android:compileReleaseLibraryResources UP-TO-DATE > Task :url_launcher_android:parseReleaseLocalResources UP-TO-DATE > Task :url_launcher_android:generateReleaseRFile UP-TO-DATE > Task :wakelock:compileReleaseLibraryResources UP-TO-DATE > Task :wakelock:parseReleaseLocalResources UP-TO-DATE > Task :wakelock:generateReleaseRFile UP-TO-DATE > Task :app:processReleaseResources UP-TO-DATE > Task :app_installer:generateReleaseBuildConfig UP-TO-DATE > Task :app_installer:javaPreCompileRelease UP-TO-DATE > Task :app_installer:compileReleaseJavaWithJavac UP-TO-DATE > Task :app_installer:bundleLibCompileToJarRelease UP-TO-DATE > Task :connectivity_plus:generateReleaseBuildConfig UP-TO-DATE > Task :connectivity_plus:javaPreCompileRelease UP-TO-DATE > Task :connectivity_plus:compileReleaseJavaWithJavac UP-TO-DATE > Task :connectivity_plus:bundleLibCompileToJarRelease UP-TO-DATE > Task :cr_file_saver:generateReleaseBuildConfig UP-TO-DATE > Task :cr_file_saver:compileReleaseKotlin UP-TO-DATE > Task :cr_file_saver:javaPreCompileRelease UP-TO-DATE > Task :cr_file_saver:compileReleaseJavaWithJavac UP-TO-DATE > Task :cr_file_saver:bundleLibCompileToJarRelease UP-TO-DATE > Task :device_apps:generateReleaseBuildConfig UP-TO-DATE > Task :device_apps:javaPreCompileRelease UP-TO-DATE > Task :device_apps:compileReleaseJavaWithJavac UP-TO-DATE > Task :device_apps:bundleLibCompileToJarRelease UP-TO-DATE > Task :device_info_plus:generateReleaseBuildConfig UP-TO-DATE > Task :device_info_plus:compileReleaseKotlin UP-TO-DATE > Task :device_info_plus:javaPreCompileRelease UP-TO-DATE > Task :device_info_plus:compileReleaseJavaWithJavac UP-TO-DATE > Task :device_info_plus:bundleLibCompileToJarRelease UP-TO-DATE > Task :dynamic_color:generateReleaseBuildConfig UP-TO-DATE > Task :dynamic_color:compileReleaseKotlin UP-TO-DATE > Task :dynamic_color:javaPreCompileRelease UP-TO-DATE > Task :dynamic_color:compileReleaseJavaWithJavac UP-TO-DATE > Task :dynamic_color:bundleLibCompileToJarRelease UP-TO-DATE > Task :file_picker:generateReleaseBuildConfig UP-TO-DATE > Task :file_picker:javaPreCompileRelease UP-TO-DATE > Task :flutter_plugin_android_lifecycle:generateReleaseBuildConfig UP-TO-DATE > Task :flutter_plugin_android_lifecycle:javaPreCompileRelease UP-TO-DATE > Task :flutter_plugin_android_lifecycle:compileReleaseJavaWithJavac UP-TO-DATE > Task :flutter_plugin_android_lifecycle:bundleLibCompileToJarRelease UP-TO-DATE > Task :file_picker:compileReleaseJavaWithJavac UP-TO-DATE > Task :file_picker:bundleLibCompileToJarRelease UP-TO-DATE > Task :flutter_background:generateReleaseBuildConfig UP-TO-DATE > Task :flutter_background:compileReleaseKotlin UP-TO-DATE > Task :flutter_background:javaPreCompileRelease UP-TO-DATE > Task :flutter_background:compileReleaseJavaWithJavac UP-TO-DATE > Task :flutter_background:bundleLibCompileToJarRelease UP-TO-DATE > Task :flutter_local_notifications:generateReleaseBuildConfig UP-TO-DATE > Task :flutter_local_notifications:javaPreCompileRelease UP-TO-DATE > Task :flutter_local_notifications:compileReleaseJavaWithJavac UP-TO-DATE > Task :flutter_local_notifications:bundleLibCompileToJarRelease UP-TO-DATE > Task :flutter_statusbarcolor_ns:generateReleaseBuildConfig UP-TO-DATE > Task :flutter_statusbarcolor_ns:compileReleaseKotlin UP-TO-DATE > Task :flutter_statusbarcolor_ns:javaPreCompileRelease UP-TO-DATE > Task :flutter_statusbarcolor_ns:compileReleaseJavaWithJavac UP-TO-DATE > Task :flutter_statusbarcolor_ns:bundleLibCompileToJarRelease UP-TO-DATE > Task :fluttertoast:generateReleaseBuildConfig UP-TO-DATE > Task :fluttertoast:compileReleaseKotlin UP-TO-DATE > Task :fluttertoast:javaPreCompileRelease UP-TO-DATE > Task :fluttertoast:compileReleaseJavaWithJavac UP-TO-DATE > Task :fluttertoast:bundleLibCompileToJarRelease UP-TO-DATE > Task :logcat:generateReleaseBuildConfig UP-TO-DATE > Task :logcat:javaPreCompileRelease UP-TO-DATE > Task :logcat:compileReleaseJavaWithJavac UP-TO-DATE > Task :logcat:bundleLibCompileToJarRelease UP-TO-DATE > Task :package_info_plus:generateReleaseBuildConfig UP-TO-DATE > Task :package_info_plus:compileReleaseKotlin UP-TO-DATE > Task :package_info_plus:javaPreCompileRelease UP-TO-DATE > Task :package_info_plus:compileReleaseJavaWithJavac UP-TO-DATE > Task :package_info_plus:bundleLibCompileToJarRelease UP-TO-DATE > Task :path_provider_android:generateReleaseBuildConfig UP-TO-DATE > Task :path_provider_android:javaPreCompileRelease UP-TO-DATE > Task :path_provider_android:compileReleaseJavaWithJavac UP-TO-DATE > Task :path_provider_android:bundleLibCompileToJarRelease UP-TO-DATE > Task :permission_handler_android:generateReleaseBuildConfig UP-TO-DATE > Task :permission_handler_android:javaPreCompileRelease UP-TO-DATE > Task :permission_handler_android:compileReleaseJavaWithJavac UP-TO-DATE > Task :permission_handler_android:bundleLibCompileToJarRelease UP-TO-DATE > Task :root:generateReleaseBuildConfig UP-TO-DATE > Task :root:javaPreCompileRelease UP-TO-DATE > Task :root:compileReleaseJavaWithJavac UP-TO-DATE > Task :root:bundleLibCompileToJarRelease UP-TO-DATE > Task :share_extend:generateReleaseBuildConfig UP-TO-DATE > Task :share_extend:javaPreCompileRelease UP-TO-DATE > Task :share_extend:compileReleaseJavaWithJavac UP-TO-DATE > Task :share_extend:bundleLibCompileToJarRelease UP-TO-DATE > Task :shared_preferences_android:generateReleaseBuildConfig UP-TO-DATE > Task :shared_preferences_android:javaPreCompileRelease UP-TO-DATE > Task :shared_preferences_android:compileReleaseJavaWithJavac UP-TO-DATE > Task :shared_preferences_android:bundleLibCompileToJarRelease UP-TO-DATE > Task :sqflite:generateReleaseBuildConfig UP-TO-DATE > Task :sqflite:javaPreCompileRelease UP-TO-DATE > Task :sqflite:compileReleaseJavaWithJavac UP-TO-DATE > Task :sqflite:bundleLibCompileToJarRelease UP-TO-DATE > Task :url_launcher_android:generateReleaseBuildConfig UP-TO-DATE > Task :url_launcher_android:javaPreCompileRelease UP-TO-DATE > Task :url_launcher_android:compileReleaseJavaWithJavac UP-TO-DATE > Task :url_launcher_android:bundleLibCompileToJarRelease UP-TO-DATE > Task :wakelock:generateReleaseBuildConfig UP-TO-DATE > Task :wakelock:compileReleaseKotlin UP-TO-DATE > Task :wakelock:javaPreCompileRelease UP-TO-DATE > Task :wakelock:compileReleaseJavaWithJavac UP-TO-DATE > Task :wakelock:bundleLibCompileToJarRelease UP-TO-DATE > Task :app:compileReleaseKotlin UP-TO-DATE > Task :app:javaPreCompileRelease UP-TO-DATE > Task :app:compileReleaseJavaWithJavac UP-TO-DATE > Task :app:preReleaseUnitTestBuild UP-TO-DATE > Task :app:processReleaseJavaRes NO-SOURCE > Task :app:javaPreCompileReleaseUnitTest > Task :app:processReleaseUnitTestJavaRes NO-SOURCE > Task :app_installer:processReleaseJavaRes NO-SOURCE > Task :app_installer:bundleLibResRelease NO-SOURCE > Task :app_installer:bundleLibRuntimeToJarRelease UP-TO-DATE > Task :connectivity_plus:processReleaseJavaRes NO-SOURCE > Task :connectivity_plus:bundleLibResRelease NO-SOURCE > Task :connectivity_plus:bundleLibRuntimeToJarRelease UP-TO-DATE > Task :cr_file_saver:processReleaseJavaRes NO-SOURCE > Task :cr_file_saver:bundleLibResRelease UP-TO-DATE > Task :cr_file_saver:bundleLibRuntimeToJarRelease UP-TO-DATE > Task :app:bundleReleaseClasses > Task :device_apps:processReleaseJavaRes NO-SOURCE > Task :device_apps:bundleLibResRelease NO-SOURCE > Task :device_apps:bundleLibRuntimeToJarRelease UP-TO-DATE > Task :device_info_plus:processReleaseJavaRes NO-SOURCE > Task :device_info_plus:bundleLibResRelease UP-TO-DATE > Task :device_info_plus:bundleLibRuntimeToJarRelease UP-TO-DATE > Task :dynamic_color:processReleaseJavaRes NO-SOURCE > Task :dynamic_color:bundleLibResRelease UP-TO-DATE > Task :dynamic_color:bundleLibRuntimeToJarRelease UP-TO-DATE > Task :file_picker:processReleaseJavaRes NO-SOURCE > Task :file_picker:bundleLibResRelease NO-SOURCE > Task :file_picker:bundleLibRuntimeToJarRelease UP-TO-DATE > Task :flutter_background:processReleaseJavaRes NO-SOURCE > Task :app:compileReleaseUnitTestKotlin NO-SOURCE > Task :app:bundleReleaseClassesToRuntimeJar > Task :app:compileReleaseUnitTestJavaWithJavac NO-SOURCE > Task :flutter_background:bundleLibResRelease UP-TO-DATE > Task :flutter_background:bundleLibRuntimeToJarRelease UP-TO-DATE > Task :flutter_local_notifications:processReleaseJavaRes NO-SOURCE > Task :flutter_local_notifications:bundleLibResRelease NO-SOURCE > Task :flutter_local_notifications:bundleLibRuntimeToJarRelease UP-TO-DATE > Task :flutter_plugin_android_lifecycle:processReleaseJavaRes NO-SOURCE > Task :flutter_plugin_android_lifecycle:bundleLibResRelease NO-SOURCE > Task :flutter_plugin_android_lifecycle:bundleLibRuntimeToJarRelease UP-TO-DATE > Task :flutter_statusbarcolor_ns:processReleaseJavaRes NO-SOURCE > Task :flutter_statusbarcolor_ns:bundleLibResRelease UP-TO-DATE > Task :flutter_statusbarcolor_ns:bundleLibRuntimeToJarRelease UP-TO-DATE > Task :fluttertoast:processReleaseJavaRes NO-SOURCE > Task :fluttertoast:bundleLibResRelease UP-TO-DATE > Task :fluttertoast:bundleLibRuntimeToJarRelease UP-TO-DATE > Task :logcat:processReleaseJavaRes NO-SOURCE > Task :logcat:bundleLibResRelease NO-SOURCE > Task :logcat:bundleLibRuntimeToJarRelease UP-TO-DATE > Task :package_info_plus:processReleaseJavaRes NO-SOURCE > Task :package_info_plus:bundleLibResRelease UP-TO-DATE > Task :package_info_plus:bundleLibRuntimeToJarRelease UP-TO-DATE > Task :path_provider_android:processReleaseJavaRes NO-SOURCE > Task :path_provider_android:bundleLibResRelease NO-SOURCE > Task :path_provider_android:bundleLibRuntimeToJarRelease UP-TO-DATE > Task :permission_handler_android:processReleaseJavaRes NO-SOURCE > Task :permission_handler_android:bundleLibResRelease NO-SOURCE > Task :permission_handler_android:bundleLibRuntimeToJarRelease UP-TO-DATE > Task :root:processReleaseJavaRes NO-SOURCE > Task :root:bundleLibResRelease NO-SOURCE > Task :root:bundleLibRuntimeToJarRelease UP-TO-DATE > Task :share_extend:processReleaseJavaRes NO-SOURCE > Task :share_extend:bundleLibResRelease NO-SOURCE > Task :share_extend:bundleLibRuntimeToJarRelease UP-TO-DATE > Task :shared_preferences_android:processReleaseJavaRes NO-SOURCE > Task :shared_preferences_android:bundleLibResRelease NO-SOURCE > Task :shared_preferences_android:bundleLibRuntimeToJarRelease UP-TO-DATE > Task :sqflite:processReleaseJavaRes NO-SOURCE > Task :sqflite:bundleLibResRelease NO-SOURCE > Task :sqflite:bundleLibRuntimeToJarRelease UP-TO-DATE > Task :url_launcher_android:processReleaseJavaRes NO-SOURCE > Task :url_launcher_android:bundleLibResRelease NO-SOURCE > Task :url_launcher_android:bundleLibRuntimeToJarRelease UP-TO-DATE > Task :wakelock:processReleaseJavaRes NO-SOURCE > Task :wakelock:bundleLibResRelease UP-TO-DATE > Task :wakelock:bundleLibRuntimeToJarRelease UP-TO-DATE > Task :app:testReleaseUnitTest NO-SOURCE > Task :app_installer:preDebugUnitTestBuild UP-TO-DATE > Task :app_installer:processDebugUnitTestJavaRes NO-SOURCE > Task :app_installer:preProfileUnitTestBuild UP-TO-DATE > Task :app_installer:javaPreCompileDebugUnitTest > Task :app_installer:processProfileUnitTestJavaRes NO-SOURCE > Task :app_installer:preReleaseUnitTestBuild UP-TO-DATE > Task :app_installer:javaPreCompileProfileUnitTest > Task :app_installer:processReleaseUnitTestJavaRes NO-SOURCE > Task :connectivity_plus:preDebugUnitTestBuild UP-TO-DATE > Task :app_installer:javaPreCompileReleaseUnitTest > Task :app_installer:generateProfileUnitTestStubRFile > Task :app_installer:compileProfileUnitTestJavaWithJavac NO-SOURCE > Task :app_installer:testProfileUnitTest NO-SOURCE > Task :app_installer:generateReleaseUnitTestStubRFile > Task :app_installer:compileReleaseUnitTestJavaWithJavac NO-SOURCE > Task :app_installer:generateDebugUnitTestStubRFile > Task :connectivity_plus:javaPreCompileDebugUnitTest > Task :app_installer:testReleaseUnitTest NO-SOURCE > Task :app_installer:compileDebugUnitTestJavaWithJavac NO-SOURCE > Task :app_installer:testDebugUnitTest NO-SOURCE > Task :app_installer:test UP-TO-DATE > Task :connectivity_plus:processDebugUnitTestJavaRes NO-SOURCE > Task :connectivity_plus:preProfileUnitTestBuild UP-TO-DATE > Task :connectivity_plus:generateDebugUnitTestStubRFile > Task :connectivity_plus:compileDebugUnitTestJavaWithJavac NO-SOURCE > Task :connectivity_plus:testDebugUnitTest NO-SOURCE > Task :connectivity_plus:processProfileUnitTestJavaRes NO-SOURCE > Task :connectivity_plus:preReleaseUnitTestBuild UP-TO-DATE > Task :connectivity_plus:javaPreCompileProfileUnitTest > Task :connectivity_plus:generateProfileUnitTestStubRFile > Task :connectivity_plus:compileProfileUnitTestJavaWithJavac NO-SOURCE > Task :connectivity_plus:testProfileUnitTest NO-SOURCE > Task :connectivity_plus:processReleaseUnitTestJavaRes NO-SOURCE > Task :cr_file_saver:preDebugUnitTestBuild UP-TO-DATE > Task :connectivity_plus:javaPreCompileReleaseUnitTest > Task :connectivity_plus:generateReleaseUnitTestStubRFile > Task :connectivity_plus:compileReleaseUnitTestJavaWithJavac NO-SOURCE > Task :cr_file_saver:javaPreCompileDebugUnitTest > Task :connectivity_plus:testReleaseUnitTest NO-SOURCE > Task :connectivity_plus:test UP-TO-DATE > Task :cr_file_saver:processDebugUnitTestJavaRes NO-SOURCE > Task :cr_file_saver:preProfileUnitTestBuild UP-TO-DATE > Task :cr_file_saver:generateDebugUnitTestStubRFile > Task :cr_file_saver:compileDebugUnitTestKotlin NO-SOURCE > Task :cr_file_saver:compileDebugUnitTestJavaWithJavac NO-SOURCE > Task :cr_file_saver:testDebugUnitTest NO-SOURCE > Task :cr_file_saver:processProfileUnitTestJavaRes NO-SOURCE > Task :cr_file_saver:preReleaseUnitTestBuild UP-TO-DATE > Task :cr_file_saver:javaPreCompileProfileUnitTest > Task :cr_file_saver:generateProfileUnitTestStubRFile > Task :cr_file_saver:processReleaseUnitTestJavaRes NO-SOURCE > Task :cr_file_saver:compileProfileUnitTestKotlin NO-SOURCE > Task :cr_file_saver:compileProfileUnitTestJavaWithJavac NO-SOURCE > Task :cr_file_saver:testProfileUnitTest NO-SOURCE > Task :device_apps:preDebugUnitTestBuild UP-TO-DATE > Task :cr_file_saver:javaPreCompileReleaseUnitTest > Task :device_apps:processDebugUnitTestJavaRes NO-SOURCE > Task :device_apps:preProfileUnitTestBuild UP-TO-DATE > Task :device_apps:javaPreCompileDebugUnitTest > Task :cr_file_saver:generateReleaseUnitTestStubRFile > Task :device_apps:generateDebugUnitTestStubRFile > Task :cr_file_saver:compileReleaseUnitTestKotlin NO-SOURCE > Task :cr_file_saver:compileReleaseUnitTestJavaWithJavac NO-SOURCE > Task :cr_file_saver:testReleaseUnitTest NO-SOURCE > Task :cr_file_saver:test UP-TO-DATE > Task :device_apps:compileDebugUnitTestJavaWithJavac NO-SOURCE > Task :device_apps:testDebugUnitTest NO-SOURCE > Task :device_apps:processProfileUnitTestJavaRes NO-SOURCE > Task :device_apps:preReleaseUnitTestBuild UP-TO-DATE > Task :device_apps:javaPreCompileProfileUnitTest > Task :device_apps:generateProfileUnitTestStubRFile > Task :device_apps:compileProfileUnitTestJavaWithJavac NO-SOURCE > Task :device_apps:testProfileUnitTest NO-SOURCE > Task :device_apps:processReleaseUnitTestJavaRes NO-SOURCE > Task :device_info_plus:preDebugUnitTestBuild UP-TO-DATE > Task :device_apps:javaPreCompileReleaseUnitTest > Task :device_info_plus:processDebugUnitTestJavaRes NO-SOURCE > Task :device_info_plus:preProfileUnitTestBuild UP-TO-DATE > Task :device_info_plus:javaPreCompileDebugUnitTest > Task :device_info_plus:generateDebugUnitTestStubRFile > Task :device_info_plus:generateProfileUnitTestStubRFile > Task :device_info_plus:compileDebugUnitTestKotlin NO-SOURCE > Task :device_info_plus:compileDebugUnitTestJavaWithJavac NO-SOURCE > Task :device_info_plus:javaPreCompileProfileUnitTest > Task :device_info_plus:testDebugUnitTest NO-SOURCE > Task :device_info_plus:compileProfileUnitTestKotlin NO-SOURCE > Task :device_info_plus:compileProfileUnitTestJavaWithJavac NO-SOURCE > Task :device_apps:generateReleaseUnitTestStubRFile > Task :device_info_plus:processProfileUnitTestJavaRes NO-SOURCE > Task :device_apps:compileReleaseUnitTestJavaWithJavac NO-SOURCE > Task :device_apps:testReleaseUnitTest NO-SOURCE > Task :device_apps:test UP-TO-DATE > Task :device_info_plus:testProfileUnitTest NO-SOURCE > Task :device_info_plus:preReleaseUnitTestBuild UP-TO-DATE > Task :device_info_plus:processReleaseUnitTestJavaRes NO-SOURCE > Task :dynamic_color:preDebugUnitTestBuild UP-TO-DATE > Task :device_info_plus:javaPreCompileReleaseUnitTest > Task :dynamic_color:processDebugUnitTestJavaRes NO-SOURCE > Task :dynamic_color:preProfileUnitTestBuild UP-TO-DATE > Task :dynamic_color:generateDebugUnitTestStubRFile > Task :dynamic_color:javaPreCompileDebugUnitTest > Task :dynamic_color:compileDebugUnitTestKotlin NO-SOURCE > Task :dynamic_color:compileDebugUnitTestJavaWithJavac NO-SOURCE > Task :dynamic_color:testDebugUnitTest NO-SOURCE > Task :dynamic_color:processProfileUnitTestJavaRes NO-SOURCE > Task :dynamic_color:preReleaseUnitTestBuild UP-TO-DATE > Task :dynamic_color:javaPreCompileProfileUnitTest > Task :device_info_plus:generateReleaseUnitTestStubRFile > Task :dynamic_color:generateProfileUnitTestStubRFile > Task :device_info_plus:compileReleaseUnitTestKotlin NO-SOURCE > Task :device_info_plus:compileReleaseUnitTestJavaWithJavac NO-SOURCE > Task :device_info_plus:testReleaseUnitTest NO-SOURCE > Task :device_info_plus:test UP-TO-DATE > Task :dynamic_color:compileProfileUnitTestKotlin NO-SOURCE > Task :dynamic_color:compileProfileUnitTestJavaWithJavac NO-SOURCE > Task :dynamic_color:testProfileUnitTest NO-SOURCE > Task :dynamic_color:generateReleaseUnitTestStubRFile > Task :dynamic_color:compileReleaseUnitTestKotlin NO-SOURCE > Task :dynamic_color:javaPreCompileReleaseUnitTest > Task :dynamic_color:processReleaseUnitTestJavaRes NO-SOURCE > Task :dynamic_color:compileReleaseUnitTestJavaWithJavac NO-SOURCE > Task :dynamic_color:testReleaseUnitTest NO-SOURCE > Task :dynamic_color:test UP-TO-DATE > Task :file_picker:preDebugUnitTestBuild UP-TO-DATE > Task :file_picker:processDebugUnitTestJavaRes NO-SOURCE > Task :file_picker:preProfileUnitTestBuild UP-TO-DATE > Task :file_picker:javaPreCompileDebugUnitTest > Task :file_picker:processProfileUnitTestJavaRes NO-SOURCE > Task :file_picker:preReleaseUnitTestBuild UP-TO-DATE > Task :file_picker:javaPreCompileProfileUnitTest > Task :file_picker:processReleaseUnitTestJavaRes NO-SOURCE > Task :flutter_background:preDebugUnitTestBuild UP-TO-DATE > Task :file_picker:generateDebugUnitTestStubRFile > Task :file_picker:javaPreCompileReleaseUnitTest > Task :file_picker:generateProfileUnitTestStubRFile > Task :file_picker:compileDebugUnitTestJavaWithJavac NO-SOURCE > Task :file_picker:testDebugUnitTest NO-SOURCE > Task :file_picker:generateReleaseUnitTestStubRFile > Task :file_picker:compileProfileUnitTestJavaWithJavac NO-SOURCE > Task :file_picker:testProfileUnitTest NO-SOURCE > Task :file_picker:compileReleaseUnitTestJavaWithJavac NO-SOURCE > Task :file_picker:testReleaseUnitTest NO-SOURCE > Task :file_picker:test UP-TO-DATE > Task :flutter_background:generateDebugUnitTestStubRFile > Task :flutter_background:compileDebugUnitTestKotlin NO-SOURCE > Task :flutter_background:processDebugUnitTestJavaRes NO-SOURCE > Task :flutter_background:preProfileUnitTestBuild UP-TO-DATE > Task :flutter_background:javaPreCompileDebugUnitTest > Task :flutter_background:compileDebugUnitTestJavaWithJavac NO-SOURCE > Task :flutter_background:testDebugUnitTest NO-SOURCE > Task :flutter_background:processProfileUnitTestJavaRes NO-SOURCE > Task :flutter_background:preReleaseUnitTestBuild UP-TO-DATE > Task :flutter_background:generateProfileUnitTestStubRFile > Task :flutter_background:javaPreCompileProfileUnitTest > Task :flutter_background:compileProfileUnitTestKotlin NO-SOURCE > Task :flutter_background:compileProfileUnitTestJavaWithJavac NO-SOURCE > Task :flutter_background:testProfileUnitTest NO-SOURCE > Task :flutter_background:processReleaseUnitTestJavaRes NO-SOURCE > Task :flutter_local_notifications:preDebugUnitTestBuild UP-TO-DATE > Task :flutter_background:javaPreCompileReleaseUnitTest > Task :flutter_background:generateReleaseUnitTestStubRFile > Task :app:compileProfileKotlin w: /home/aun/Programming/Projects/FlutterProjects/revanced-manager/android/app/src/main/kotlin/app/revanced/manager/flutter/MainActivity.kt: (313, 28): The corresponding parameter in the supertype 'Logger' is named 'msg'. This may cause problems when calling this function with named arguments. > Task :flutter_local_notifications:generateDebugUnitTestStubRFile > Task :app:compileProfileJavaWithJavac > Task :flutter_background:compileReleaseUnitTestKotlin NO-SOURCE > Task :flutter_background:compileReleaseUnitTestJavaWithJavac NO-SOURCE > Task :flutter_background:testReleaseUnitTest NO-SOURCE > Task :flutter_background:test UP-TO-DATE > Task :flutter_local_notifications:processDebugUnitTestJavaRes NO-SOURCE > Task :flutter_local_notifications:preProfileUnitTestBuild UP-TO-DATE > Task :flutter_local_notifications:javaPreCompileDebugUnitTest > Task :app:bundleProfileClasses > Task :flutter_local_notifications:generateProfileUnitTestStubRFile > Task :app:bundleProfileClassesToRuntimeJar > Task :flutter_local_notifications:compileDebugUnitTestJavaWithJavac Note: /home/aun/.pub-cache/hosted/pub.dev/flutter_local_notifications-13.0.0/android/src/test/java/com/dexterous/flutterlocalnotifications/models/NotificationActionTest.java uses or overrides a deprecated API. Note: Recompile with -Xlint:deprecation for details. > Task :app:compileProfileUnitTestKotlin NO-SOURCE > Task :app:compileProfileUnitTestJavaWithJavac NO-SOURCE > Task :app:testProfileUnitTest NO-SOURCE > Task :app:test UP-TO-DATE > Task :flutter_local_notifications:testDebugUnitTest > Task :flutter_local_notifications:processProfileUnitTestJavaRes NO-SOURCE > Task :flutter_local_notifications:preReleaseUnitTestBuild UP-TO-DATE > Task :flutter_local_notifications:javaPreCompileProfileUnitTest > Task :flutter_local_notifications:generateReleaseUnitTestStubRFile > Task :flutter_local_notifications:compileProfileUnitTestJavaWithJavac Note: /home/aun/.pub-cache/hosted/pub.dev/flutter_local_notifications-13.0.0/android/src/test/java/com/dexterous/flutterlocalnotifications/models/NotificationActionTest.java uses or overrides a deprecated API. Note: Recompile with -Xlint:deprecation for details. > Task :flutter_local_notifications:testProfileUnitTest > Task :flutter_local_notifications:processReleaseUnitTestJavaRes NO-SOURCE > Task :flutter_plugin_android_lifecycle:preDebugUnitTestBuild UP-TO-DATE > Task :flutter_local_notifications:javaPreCompileReleaseUnitTest > Task :flutter_plugin_android_lifecycle:javaPreCompileDebugUnitTest > Task :flutter_local_notifications:compileReleaseUnitTestJavaWithJavac Note: /home/aun/.pub-cache/hosted/pub.dev/flutter_local_notifications-13.0.0/android/src/test/java/com/dexterous/flutterlocalnotifications/models/NotificationActionTest.java uses or overrides a deprecated API. Note: Recompile with -Xlint:deprecation for details. > Task :flutter_local_notifications:testReleaseUnitTest > Task :flutter_local_notifications:test > Task :flutter_plugin_android_lifecycle:generateDebugUnitTestResValues > Task :flutter_plugin_android_lifecycle:generateDebugUnitTestResources > Task :flutter_plugin_android_lifecycle:checkDebugUnitTestAarMetadata > Task :flutter_plugin_android_lifecycle:processDebugUnitTestManifest > Task :flutter_plugin_android_lifecycle:mergeDebugAssets > Task :flutter_plugin_android_lifecycle:mergeDebugAssetsForUnitTest NO-SOURCE > Task :flutter_plugin_android_lifecycle:generateDebugUnitTestAssets UP-TO-DATE > Task :flutter_plugin_android_lifecycle:mergeDebugUnitTestAssets > Task :flutter_plugin_android_lifecycle:mergeDebugUnitTestAssetsForUnitTest NO-SOURCE > Task :flutter_plugin_android_lifecycle:processDebugUnitTestJavaRes NO-SOURCE > Task :flutter_plugin_android_lifecycle:preProfileUnitTestBuild UP-TO-DATE > Task :flutter_plugin_android_lifecycle:javaPreCompileProfileUnitTest > Task :flutter_plugin_android_lifecycle:checkProfileUnitTestAarMetadata > Task :flutter_plugin_android_lifecycle:generateProfileUnitTestResValues > Task :flutter_plugin_android_lifecycle:generateProfileUnitTestResources > Task :flutter_plugin_android_lifecycle:mergeDebugUnitTestResources > Task :flutter_plugin_android_lifecycle:processProfileUnitTestManifest > Task :flutter_plugin_android_lifecycle:mergeProfileAssets > Task :flutter_plugin_android_lifecycle:mergeProfileAssetsForUnitTest NO-SOURCE > Task :flutter_plugin_android_lifecycle:generateProfileUnitTestAssets UP-TO-DATE > Task :flutter_plugin_android_lifecycle:mergeProfileUnitTestAssets > Task :flutter_plugin_android_lifecycle:mergeProfileUnitTestAssetsForUnitTest NO-SOURCE > Task :flutter_plugin_android_lifecycle:processProfileUnitTestJavaRes NO-SOURCE > Task :flutter_plugin_android_lifecycle:preReleaseUnitTestBuild UP-TO-DATE > Task :flutter_plugin_android_lifecycle:javaPreCompileReleaseUnitTest > Task :flutter_plugin_android_lifecycle:generateReleaseUnitTestResValues > Task :flutter_plugin_android_lifecycle:generateReleaseUnitTestResources > Task :flutter_plugin_android_lifecycle:checkReleaseUnitTestAarMetadata > Task :flutter_plugin_android_lifecycle:mergeProfileUnitTestResources > Task :flutter_plugin_android_lifecycle:processReleaseUnitTestManifest > Task :flutter_plugin_android_lifecycle:mergeReleaseAssets > Task :flutter_plugin_android_lifecycle:mergeReleaseAssetsForUnitTest NO-SOURCE > Task :flutter_plugin_android_lifecycle:generateReleaseUnitTestAssets UP-TO-DATE > Task :flutter_plugin_android_lifecycle:mergeReleaseUnitTestAssets > Task :flutter_plugin_android_lifecycle:mergeReleaseUnitTestAssetsForUnitTest NO-SOURCE > Task :flutter_plugin_android_lifecycle:processReleaseUnitTestJavaRes NO-SOURCE > Task :flutter_statusbarcolor_ns:preDebugUnitTestBuild UP-TO-DATE > Task :flutter_statusbarcolor_ns:processDebugUnitTestJavaRes NO-SOURCE > Task :flutter_statusbarcolor_ns:preProfileUnitTestBuild UP-TO-DATE > Task :flutter_statusbarcolor_ns:javaPreCompileDebugUnitTest > Task :flutter_plugin_android_lifecycle:mergeReleaseUnitTestResources > Task :flutter_statusbarcolor_ns:generateDebugUnitTestStubRFile > Task :flutter_statusbarcolor_ns:javaPreCompileProfileUnitTest > Task :flutter_plugin_android_lifecycle:processDebugUnitTestResources > Task :flutter_statusbarcolor_ns:generateProfileUnitTestStubRFile > Task :flutter_plugin_android_lifecycle:processProfileUnitTestResources > Task :flutter_plugin_android_lifecycle:processReleaseUnitTestResources > Task :flutter_plugin_android_lifecycle:compileDebugUnitTestJavaWithJavac > Task :flutter_plugin_android_lifecycle:packageDebugUnitTestForUnitTest > Task :flutter_plugin_android_lifecycle:generateDebugUnitTestConfig > Task :flutter_plugin_android_lifecycle:compileProfileUnitTestJavaWithJavac > Task :flutter_plugin_android_lifecycle:testDebugUnitTest OpenJDK 64-Bit Server VM warning: Sharing is only supported for boot loader classes because bootstrap classpath has been appended io.flutter.embedding.engine.plugins.lifecycle.FlutterLifecycleAdapterTest > getActivityLifecycle PASSED > Task :flutter_plugin_android_lifecycle:packageProfileUnitTestForUnitTest > Task :flutter_plugin_android_lifecycle:generateProfileUnitTestConfig > Task :flutter_plugin_android_lifecycle:compileReleaseUnitTestJavaWithJavac > Task :flutter_plugin_android_lifecycle:testProfileUnitTest OpenJDK 64-Bit Server VM warning: Sharing is only supported for boot loader classes because bootstrap classpath has been appended io.flutter.embedding.engine.plugins.lifecycle.FlutterLifecycleAdapterTest > getActivityLifecycle PASSED > Task :flutter_plugin_android_lifecycle:packageReleaseUnitTestForUnitTest > Task :flutter_statusbarcolor_ns:compileDebugUnitTestKotlin NO-SOURCE > Task :flutter_plugin_android_lifecycle:generateReleaseUnitTestConfig > Task :flutter_plugin_android_lifecycle:testReleaseUnitTest OpenJDK 64-Bit Server VM warning: Sharing is only supported for boot loader classes because bootstrap classpath has been appended io.flutter.embedding.engine.plugins.lifecycle.FlutterLifecycleAdapterTest > getActivityLifecycle PASSED > Task :flutter_plugin_android_lifecycle:test > Task :flutter_statusbarcolor_ns:compileDebugUnitTestJavaWithJavac NO-SOURCE > Task :flutter_statusbarcolor_ns:testDebugUnitTest NO-SOURCE > Task :flutter_statusbarcolor_ns:compileProfileUnitTestKotlin NO-SOURCE > Task :flutter_statusbarcolor_ns:compileProfileUnitTestJavaWithJavac NO-SOURCE > Task :flutter_statusbarcolor_ns:processProfileUnitTestJavaRes NO-SOURCE > Task :flutter_statusbarcolor_ns:testProfileUnitTest NO-SOURCE > Task :flutter_statusbarcolor_ns:preReleaseUnitTestBuild UP-TO-DATE > Task :flutter_statusbarcolor_ns:processReleaseUnitTestJavaRes NO-SOURCE > Task :fluttertoast:preDebugUnitTestBuild UP-TO-DATE > Task :flutter_statusbarcolor_ns:javaPreCompileReleaseUnitTest > Task :flutter_statusbarcolor_ns:generateReleaseUnitTestStubRFile > Task :fluttertoast:javaPreCompileDebugUnitTest > Task :flutter_statusbarcolor_ns:compileReleaseUnitTestKotlin NO-SOURCE > Task :flutter_statusbarcolor_ns:compileReleaseUnitTestJavaWithJavac NO-SOURCE > Task :flutter_statusbarcolor_ns:testReleaseUnitTest NO-SOURCE > Task :flutter_statusbarcolor_ns:test UP-TO-DATE > Task :fluttertoast:processDebugUnitTestJavaRes NO-SOURCE > Task :fluttertoast:preProfileUnitTestBuild UP-TO-DATE > Task :fluttertoast:generateDebugUnitTestStubRFile > Task :fluttertoast:compileDebugUnitTestKotlin NO-SOURCE > Task :fluttertoast:compileDebugUnitTestJavaWithJavac NO-SOURCE > Task :fluttertoast:testDebugUnitTest NO-SOURCE > Task :fluttertoast:processProfileUnitTestJavaRes NO-SOURCE > Task :fluttertoast:preReleaseUnitTestBuild UP-TO-DATE > Task :fluttertoast:javaPreCompileProfileUnitTest > Task :fluttertoast:generateProfileUnitTestStubRFile > Task :fluttertoast:compileProfileUnitTestKotlin NO-SOURCE > Task :fluttertoast:compileProfileUnitTestJavaWithJavac NO-SOURCE > Task :fluttertoast:testProfileUnitTest NO-SOURCE > Task :fluttertoast:processReleaseUnitTestJavaRes NO-SOURCE > Task :logcat:preDebugUnitTestBuild UP-TO-DATE > Task :fluttertoast:javaPreCompileReleaseUnitTest > Task :fluttertoast:generateReleaseUnitTestStubRFile > Task :fluttertoast:compileReleaseUnitTestKotlin NO-SOURCE > Task :fluttertoast:compileReleaseUnitTestJavaWithJavac NO-SOURCE > Task :fluttertoast:testReleaseUnitTest NO-SOURCE > Task :fluttertoast:test UP-TO-DATE > Task :logcat:processDebugUnitTestJavaRes NO-SOURCE > Task :logcat:preProfileUnitTestBuild UP-TO-DATE > Task :logcat:javaPreCompileDebugUnitTest > Task :logcat:generateDebugUnitTestStubRFile > Task :logcat:compileDebugUnitTestJavaWithJavac NO-SOURCE > Task :logcat:testDebugUnitTest NO-SOURCE > Task :logcat:processProfileUnitTestJavaRes NO-SOURCE > Task :logcat:preReleaseUnitTestBuild UP-TO-DATE > Task :logcat:javaPreCompileProfileUnitTest > Task :logcat:generateProfileUnitTestStubRFile > Task :logcat:compileProfileUnitTestJavaWithJavac NO-SOURCE > Task :logcat:testProfileUnitTest NO-SOURCE > Task :logcat:processReleaseUnitTestJavaRes NO-SOURCE > Task :package_info_plus:preDebugUnitTestBuild UP-TO-DATE > Task :logcat:javaPreCompileReleaseUnitTest > Task :logcat:generateReleaseUnitTestStubRFile > Task :package_info_plus:javaPreCompileDebugUnitTest > Task :logcat:compileReleaseUnitTestJavaWithJavac NO-SOURCE > Task :logcat:testReleaseUnitTest NO-SOURCE > Task :logcat:test UP-TO-DATE > Task :package_info_plus:processDebugUnitTestJavaRes NO-SOURCE > Task :package_info_plus:preProfileUnitTestBuild UP-TO-DATE > Task :package_info_plus:processProfileUnitTestJavaRes NO-SOURCE > Task :package_info_plus:preReleaseUnitTestBuild UP-TO-DATE > Task :package_info_plus:javaPreCompileProfileUnitTest > Task :package_info_plus:generateDebugUnitTestStubRFile > Task :package_info_plus:generateProfileUnitTestStubRFile > Task :package_info_plus:compileDebugUnitTestKotlin NO-SOURCE > Task :package_info_plus:compileDebugUnitTestJavaWithJavac NO-SOURCE > Task :package_info_plus:testDebugUnitTest NO-SOURCE > Task :package_info_plus:compileProfileUnitTestKotlin NO-SOURCE > Task :package_info_plus:compileProfileUnitTestJavaWithJavac NO-SOURCE > Task :package_info_plus:testProfileUnitTest NO-SOURCE > Task :package_info_plus:generateReleaseUnitTestStubRFile > Task :package_info_plus:processReleaseUnitTestJavaRes NO-SOURCE > Task :package_info_plus:javaPreCompileReleaseUnitTest > Task :package_info_plus:compileReleaseUnitTestKotlin NO-SOURCE > Task :package_info_plus:compileReleaseUnitTestJavaWithJavac NO-SOURCE > Task :package_info_plus:testReleaseUnitTest NO-SOURCE > Task :package_info_plus:test UP-TO-DATE > Task :path_provider_android:preDebugUnitTestBuild UP-TO-DATE > Task :path_provider_android:javaPreCompileDebugUnitTest > Task :path_provider_android:generateDebugUnitTestResValues > Task :path_provider_android:checkDebugUnitTestAarMetadata > Task :path_provider_android:generateDebugUnitTestResources > Task :path_provider_android:processDebugUnitTestManifest > Task :path_provider_android:mergeDebugAssets > Task :path_provider_android:mergeDebugAssetsForUnitTest NO-SOURCE > Task :path_provider_android:generateDebugUnitTestAssets UP-TO-DATE > Task :path_provider_android:mergeDebugUnitTestAssets > Task :path_provider_android:mergeDebugUnitTestAssetsForUnitTest NO-SOURCE > Task :path_provider_android:processDebugUnitTestJavaRes NO-SOURCE > Task :path_provider_android:preProfileUnitTestBuild UP-TO-DATE > Task :path_provider_android:javaPreCompileProfileUnitTest > Task :path_provider_android:checkProfileUnitTestAarMetadata > Task :path_provider_android:generateProfileUnitTestResValues > Task :path_provider_android:generateProfileUnitTestResources > Task :path_provider_android:mergeDebugUnitTestResources > Task :path_provider_android:processProfileUnitTestManifest > Task :path_provider_android:mergeProfileAssets > Task :path_provider_android:mergeProfileAssetsForUnitTest NO-SOURCE > Task :path_provider_android:generateProfileUnitTestAssets UP-TO-DATE > Task :path_provider_android:mergeProfileUnitTestAssets > Task :path_provider_android:mergeProfileUnitTestAssetsForUnitTest NO-SOURCE > Task :path_provider_android:processProfileUnitTestJavaRes NO-SOURCE > Task :path_provider_android:preReleaseUnitTestBuild UP-TO-DATE > Task :path_provider_android:javaPreCompileReleaseUnitTest > Task :path_provider_android:generateReleaseUnitTestResValues > Task :path_provider_android:generateReleaseUnitTestResources > Task :path_provider_android:checkReleaseUnitTestAarMetadata > Task :path_provider_android:mergeProfileUnitTestResources > Task :path_provider_android:processReleaseUnitTestManifest > Task :path_provider_android:mergeReleaseAssets > Task :path_provider_android:mergeReleaseAssetsForUnitTest NO-SOURCE > Task :path_provider_android:generateReleaseUnitTestAssets UP-TO-DATE > Task :path_provider_android:mergeReleaseUnitTestAssets > Task :path_provider_android:mergeReleaseUnitTestAssetsForUnitTest NO-SOURCE > Task :path_provider_android:processReleaseUnitTestJavaRes NO-SOURCE > Task :permission_handler_android:preDebugUnitTestBuild UP-TO-DATE > Task :permission_handler_android:processDebugUnitTestJavaRes NO-SOURCE > Task :permission_handler_android:preProfileUnitTestBuild UP-TO-DATE > Task :permission_handler_android:javaPreCompileDebugUnitTest > Task :permission_handler_android:generateDebugUnitTestStubRFile > Task :permission_handler_android:compileDebugUnitTestJavaWithJavac NO-SOURCE > Task :permission_handler_android:testDebugUnitTest NO-SOURCE > Task :path_provider_android:mergeReleaseUnitTestResources > Task :permission_handler_android:processProfileUnitTestJavaRes NO-SOURCE > Task :permission_handler_android:javaPreCompileProfileUnitTest > Task :permission_handler_android:generateProfileUnitTestStubRFile > Task :permission_handler_android:compileProfileUnitTestJavaWithJavac NO-SOURCE > Task :permission_handler_android:testProfileUnitTest NO-SOURCE > Task :permission_handler_android:preReleaseUnitTestBuild UP-TO-DATE > Task :permission_handler_android:processReleaseUnitTestJavaRes NO-SOURCE > Task :root:preDebugUnitTestBuild UP-TO-DATE > Task :permission_handler_android:javaPreCompileReleaseUnitTest > Task :permission_handler_android:generateReleaseUnitTestStubRFile > Task :root:javaPreCompileDebugUnitTest > Task :root:processDebugUnitTestJavaRes NO-SOURCE > Task :permission_handler_android:compileReleaseUnitTestJavaWithJavac NO-SOURCE > Task :permission_handler_android:testReleaseUnitTest NO-SOURCE > Task :permission_handler_android:test UP-TO-DATE > Task :root:preProfileUnitTestBuild UP-TO-DATE > Task :root:generateDebugUnitTestStubRFile > Task :root:compileDebugUnitTestJavaWithJavac NO-SOURCE > Task :root:testDebugUnitTest NO-SOURCE > Task :root:processProfileUnitTestJavaRes NO-SOURCE > Task :root:javaPreCompileProfileUnitTest > Task :root:preReleaseUnitTestBuild UP-TO-DATE > Task :root:generateProfileUnitTestStubRFile > Task :root:compileProfileUnitTestJavaWithJavac NO-SOURCE > Task :root:testProfileUnitTest NO-SOURCE > Task :root:processReleaseUnitTestJavaRes NO-SOURCE > Task :share_extend:preDebugUnitTestBuild UP-TO-DATE > Task :root:javaPreCompileReleaseUnitTest > Task :root:generateReleaseUnitTestStubRFile > Task :root:compileReleaseUnitTestJavaWithJavac NO-SOURCE > Task :root:testReleaseUnitTest NO-SOURCE > Task :root:test UP-TO-DATE > Task :share_extend:generateDebugUnitTestStubRFile > Task :share_extend:processDebugUnitTestJavaRes NO-SOURCE > Task :share_extend:preProfileUnitTestBuild UP-TO-DATE > Task :share_extend:javaPreCompileDebugUnitTest > Task :share_extend:compileDebugUnitTestJavaWithJavac NO-SOURCE > Task :share_extend:testDebugUnitTest NO-SOURCE > Task :share_extend:processProfileUnitTestJavaRes NO-SOURCE > Task :share_extend:preReleaseUnitTestBuild UP-TO-DATE > Task :share_extend:javaPreCompileProfileUnitTest > Task :share_extend:processReleaseUnitTestJavaRes NO-SOURCE > Task :shared_preferences_android:preDebugUnitTestBuild UP-TO-DATE > Task :share_extend:javaPreCompileReleaseUnitTest > Task :shared_preferences_android:javaPreCompileDebugUnitTest > Task :share_extend:generateProfileUnitTestStubRFile > Task :path_provider_android:processDebugUnitTestResources > Task :share_extend:generateReleaseUnitTestStubRFile > Task :path_provider_android:processProfileUnitTestResources > Task :path_provider_android:processReleaseUnitTestResources > Task :shared_preferences_android:checkDebugUnitTestAarMetadata > Task :path_provider_android:compileDebugUnitTestJavaWithJavac > Task :path_provider_android:packageDebugUnitTestForUnitTest > Task :path_provider_android:generateDebugUnitTestConfig > Task :path_provider_android:compileProfileUnitTestJavaWithJavac > Task :path_provider_android:testDebugUnitTest io.flutter.plugins.pathprovider.StorageDirectoryMapperTest > testAndroidType_valid PASSED io.flutter.plugins.pathprovider.StorageDirectoryMapperTest > testAndroidType_null PASSED io.flutter.plugins.pathprovider.StorageDirectoryMapperTest > testAndroidType_invalid PASSED > Task :path_provider_android:packageProfileUnitTestForUnitTest > Task :path_provider_android:generateProfileUnitTestConfig > Task :path_provider_android:compileReleaseUnitTestJavaWithJavac > Task :path_provider_android:testProfileUnitTest io.flutter.plugins.pathprovider.StorageDirectoryMapperTest > testAndroidType_valid PASSED io.flutter.plugins.pathprovider.StorageDirectoryMapperTest > testAndroidType_null PASSED io.flutter.plugins.pathprovider.StorageDirectoryMapperTest > testAndroidType_invalid PASSED > Task :path_provider_android:packageReleaseUnitTestForUnitTest > Task :share_extend:compileProfileUnitTestJavaWithJavac NO-SOURCE > Task :path_provider_android:generateReleaseUnitTestConfig > Task :share_extend:testProfileUnitTest NO-SOURCE > Task :path_provider_android:testReleaseUnitTest io.flutter.plugins.pathprovider.StorageDirectoryMapperTest > testAndroidType_valid PASSED io.flutter.plugins.pathprovider.StorageDirectoryMapperTest > testAndroidType_null PASSED io.flutter.plugins.pathprovider.StorageDirectoryMapperTest > testAndroidType_invalid PASSED > Task :path_provider_android:test > Task :share_extend:compileReleaseUnitTestJavaWithJavac NO-SOURCE > Task :share_extend:testReleaseUnitTest NO-SOURCE > Task :share_extend:test UP-TO-DATE > Task :shared_preferences_android:generateDebugUnitTestResValues > Task :shared_preferences_android:generateDebugUnitTestResources > Task :shared_preferences_android:processDebugUnitTestManifest > Task :shared_preferences_android:mergeDebugAssets > Task :shared_preferences_android:mergeDebugAssetsForUnitTest NO-SOURCE > Task :shared_preferences_android:generateDebugUnitTestAssets UP-TO-DATE > Task :shared_preferences_android:mergeDebugUnitTestAssets > Task :shared_preferences_android:mergeDebugUnitTestAssetsForUnitTest NO-SOURCE > Task :shared_preferences_android:processDebugUnitTestJavaRes NO-SOURCE > Task :shared_preferences_android:preProfileUnitTestBuild UP-TO-DATE > Task :shared_preferences_android:javaPreCompileProfileUnitTest > Task :shared_preferences_android:checkProfileUnitTestAarMetadata > Task :shared_preferences_android:generateProfileUnitTestResValues > Task :shared_preferences_android:generateProfileUnitTestResources > Task :shared_preferences_android:mergeDebugUnitTestResources > Task :shared_preferences_android:processProfileUnitTestManifest > Task :shared_preferences_android:mergeProfileAssets > Task :shared_preferences_android:mergeProfileAssetsForUnitTest NO-SOURCE > Task :shared_preferences_android:generateProfileUnitTestAssets UP-TO-DATE > Task :shared_preferences_android:mergeProfileUnitTestAssets > Task :shared_preferences_android:mergeProfileUnitTestAssetsForUnitTest NO-SOURCE > Task :shared_preferences_android:processProfileUnitTestJavaRes NO-SOURCE > Task :shared_preferences_android:preReleaseUnitTestBuild UP-TO-DATE > Task :shared_preferences_android:javaPreCompileReleaseUnitTest > Task :shared_preferences_android:generateReleaseUnitTestResValues > Task :shared_preferences_android:generateReleaseUnitTestResources > Task :shared_preferences_android:checkReleaseUnitTestAarMetadata > Task :shared_preferences_android:mergeProfileUnitTestResources > Task :shared_preferences_android:processReleaseUnitTestManifest > Task :shared_preferences_android:mergeReleaseAssets > Task :shared_preferences_android:mergeReleaseAssetsForUnitTest NO-SOURCE > Task :shared_preferences_android:generateReleaseUnitTestAssets UP-TO-DATE > Task :shared_preferences_android:mergeReleaseUnitTestAssets > Task :shared_preferences_android:mergeReleaseUnitTestAssetsForUnitTest NO-SOURCE > Task :shared_preferences_android:processReleaseUnitTestJavaRes NO-SOURCE > Task :sqflite:preDebugUnitTestBuild UP-TO-DATE > Task :shared_preferences_android:mergeReleaseUnitTestResources > Task :sqflite:javaPreCompileDebugUnitTest > Task :sqflite:processDebugUnitTestJavaRes NO-SOURCE > Task :sqflite:preProfileUnitTestBuild UP-TO-DATE > Task :sqflite:generateDebugUnitTestStubRFile > Task :sqflite:generateProfileUnitTestStubRFile > Task :shared_preferences_android:processDebugUnitTestResources > Task :sqflite:compileDebugUnitTestJavaWithJavac > Task :shared_preferences_android:processProfileUnitTestResources > Task :shared_preferences_android:processReleaseUnitTestResources > Task :shared_preferences_android:compileDebugUnitTestJavaWithJavac > Task :shared_preferences_android:packageDebugUnitTestForUnitTest > Task :shared_preferences_android:generateDebugUnitTestConfig > Task :shared_preferences_android:compileProfileUnitTestJavaWithJavac > Task :shared_preferences_android:testDebugUnitTest OpenJDK 64-Bit Server VM warning: Sharing is only supported for boot loader classes because bootstrap classpath has been appended io.flutter.plugins.sharedpreferences.SharedPreferencesTest > setStringList PASSED io.flutter.plugins.sharedpreferences.SharedPreferencesTest > setInt PASSED io.flutter.plugins.sharedpreferences.SharedPreferencesTest > setDouble PASSED io.flutter.plugins.sharedpreferences.SharedPreferencesTest > getAllWithPrefix PASSED io.flutter.plugins.sharedpreferences.SharedPreferencesTest > setString PASSED io.flutter.plugins.sharedpreferences.SharedPreferencesTest > clearAll PASSED io.flutter.plugins.sharedpreferences.SharedPreferencesTest > clearWithPrefix PASSED io.flutter.plugins.sharedpreferences.SharedPreferencesTest > testRemove PASSED io.flutter.plugins.sharedpreferences.SharedPreferencesTest > setBool PASSED > Task :shared_preferences_android:packageProfileUnitTestForUnitTest > Task :shared_preferences_android:generateProfileUnitTestConfig > Task :shared_preferences_android:compileReleaseUnitTestJavaWithJavac > Task :shared_preferences_android:testProfileUnitTest OpenJDK 64-Bit Server VM warning: Sharing is only supported for boot loader classes because bootstrap classpath has been appended io.flutter.plugins.sharedpreferences.SharedPreferencesTest > setStringList PASSED io.flutter.plugins.sharedpreferences.SharedPreferencesTest > setInt PASSED io.flutter.plugins.sharedpreferences.SharedPreferencesTest > setDouble PASSED io.flutter.plugins.sharedpreferences.SharedPreferencesTest > getAllWithPrefix PASSED io.flutter.plugins.sharedpreferences.SharedPreferencesTest > setString PASSED io.flutter.plugins.sharedpreferences.SharedPreferencesTest > clearAll PASSED io.flutter.plugins.sharedpreferences.SharedPreferencesTest > clearWithPrefix PASSED io.flutter.plugins.sharedpreferences.SharedPreferencesTest > testRemove PASSED io.flutter.plugins.sharedpreferences.SharedPreferencesTest > setBool PASSED > Task :shared_preferences_android:packageReleaseUnitTestForUnitTest > Task :shared_preferences_android:generateReleaseUnitTestConfig > Task :sqflite:testDebugUnitTest > Task :shared_preferences_android:testReleaseUnitTest OpenJDK 64-Bit Server VM warning: Sharing is only supported for boot loader classes because bootstrap classpath has been appended io.flutter.plugins.sharedpreferences.SharedPreferencesTest > setStringList PASSED io.flutter.plugins.sharedpreferences.SharedPreferencesTest > setInt PASSED io.flutter.plugins.sharedpreferences.SharedPreferencesTest > setDouble PASSED io.flutter.plugins.sharedpreferences.SharedPreferencesTest > getAllWithPrefix PASSED io.flutter.plugins.sharedpreferences.SharedPreferencesTest > setString PASSED io.flutter.plugins.sharedpreferences.SharedPreferencesTest > clearAll PASSED io.flutter.plugins.sharedpreferences.SharedPreferencesTest > clearWithPrefix PASSED io.flutter.plugins.sharedpreferences.SharedPreferencesTest > testRemove PASSED io.flutter.plugins.sharedpreferences.SharedPreferencesTest > setBool PASSED > Task :shared_preferences_android:test > Task :sqflite:processProfileUnitTestJavaRes NO-SOURCE > Task :sqflite:preReleaseUnitTestBuild UP-TO-DATE > Task :sqflite:javaPreCompileProfileUnitTest > Task :sqflite:generateReleaseUnitTestStubRFile > Task :sqflite:compileProfileUnitTestJavaWithJavac > Task :sqflite:testProfileUnitTest > Task :sqflite:processReleaseUnitTestJavaRes NO-SOURCE > Task :url_launcher_android:preDebugUnitTestBuild UP-TO-DATE > Task :sqflite:javaPreCompileReleaseUnitTest > Task :url_launcher_android:javaPreCompileDebugUnitTest > Task :sqflite:compileReleaseUnitTestJavaWithJavac > Task :sqflite:testReleaseUnitTest > Task :sqflite:test > Task :url_launcher_android:checkDebugUnitTestAarMetadata > Task :url_launcher_android:generateDebugUnitTestResValues > Task :url_launcher_android:generateDebugUnitTestResources > Task :url_launcher_android:processDebugUnitTestManifest > Task :url_launcher_android:mergeDebugAssets > Task :url_launcher_android:mergeDebugAssetsForUnitTest NO-SOURCE > Task :url_launcher_android:generateDebugUnitTestAssets UP-TO-DATE > Task :url_launcher_android:mergeDebugUnitTestAssets > Task :url_launcher_android:mergeDebugUnitTestAssetsForUnitTest NO-SOURCE > Task :url_launcher_android:processDebugUnitTestJavaRes NO-SOURCE > Task :url_launcher_android:preProfileUnitTestBuild UP-TO-DATE > Task :url_launcher_android:javaPreCompileProfileUnitTest > Task :url_launcher_android:checkProfileUnitTestAarMetadata > Task :url_launcher_android:generateProfileUnitTestResValues > Task :url_launcher_android:generateProfileUnitTestResources > Task :url_launcher_android:mergeDebugUnitTestResources > Task :url_launcher_android:mergeProfileUnitTestResources > Task :url_launcher_android:processProfileUnitTestManifest > Task :url_launcher_android:mergeProfileAssets > Task :url_launcher_android:mergeProfileAssetsForUnitTest NO-SOURCE > Task :url_launcher_android:generateProfileUnitTestAssets UP-TO-DATE > Task :url_launcher_android:mergeProfileUnitTestAssets > Task :url_launcher_android:mergeProfileUnitTestAssetsForUnitTest NO-SOURCE > Task :url_launcher_android:processProfileUnitTestJavaRes NO-SOURCE > Task :url_launcher_android:preReleaseUnitTestBuild UP-TO-DATE > Task :url_launcher_android:javaPreCompileReleaseUnitTest > Task :url_launcher_android:generateReleaseUnitTestResValues > Task :url_launcher_android:generateReleaseUnitTestResources > Task :url_launcher_android:checkReleaseUnitTestAarMetadata > Task :url_launcher_android:processReleaseUnitTestManifest > Task :url_launcher_android:mergeReleaseAssets > Task :url_launcher_android:mergeReleaseAssetsForUnitTest NO-SOURCE > Task :url_launcher_android:generateReleaseUnitTestAssets UP-TO-DATE > Task :url_launcher_android:mergeReleaseUnitTestAssets > Task :url_launcher_android:mergeReleaseUnitTestAssetsForUnitTest NO-SOURCE > Task :url_launcher_android:processReleaseUnitTestJavaRes NO-SOURCE > Task :wakelock:preDebugUnitTestBuild UP-TO-DATE > Task :wakelock:generateDebugUnitTestStubRFile > Task :wakelock:processDebugUnitTestJavaRes NO-SOURCE > Task :wakelock:compileDebugUnitTestKotlin NO-SOURCE > Task :wakelock:preProfileUnitTestBuild UP-TO-DATE > Task :wakelock:javaPreCompileDebugUnitTest > Task :wakelock:compileDebugUnitTestJavaWithJavac NO-SOURCE > Task :wakelock:testDebugUnitTest NO-SOURCE > Task :url_launcher_android:mergeReleaseUnitTestResources > Task :wakelock:generateProfileUnitTestStubRFile > Task :wakelock:javaPreCompileProfileUnitTest > Task :wakelock:compileProfileUnitTestKotlin NO-SOURCE > Task :wakelock:compileProfileUnitTestJavaWithJavac NO-SOURCE > Task :wakelock:processProfileUnitTestJavaRes NO-SOURCE > Task :wakelock:testProfileUnitTest NO-SOURCE > Task :wakelock:preReleaseUnitTestBuild UP-TO-DATE > Task :wakelock:processReleaseUnitTestJavaRes NO-SOURCE > Task :wakelock:javaPreCompileReleaseUnitTest > Task :url_launcher_android:processDebugUnitTestResources > Task :wakelock:generateReleaseUnitTestStubRFile > Task :url_launcher_android:processProfileUnitTestResources > Task :url_launcher_android:processReleaseUnitTestResources > Task :url_launcher_android:compileDebugUnitTestJavaWithJavac > Task :url_launcher_android:packageDebugUnitTestForUnitTest > Task :url_launcher_android:generateDebugUnitTestConfig > Task :url_launcher_android:compileProfileUnitTestJavaWithJavac > Task :url_launcher_android:testDebugUnitTest io.flutter.plugins.urllauncher.MethodCallHandlerImplTest > stopListening_doesNothingWhenUnset FAILED java.lang.NoClassDefFoundError at Class.java:-2 Caused by: java.lang.ClassNotFoundException at SandboxClassLoader.java:147 Caused by: java.lang.IllegalArgumentException at ClassReader.java:195 io.flutter.plugins.urllauncher.MethodCallHandlerImplTest > stopListening_unregistersExistingChannel FAILED java.lang.NoClassDefFoundError at Class.java:-2 Caused by: java.lang.ClassNotFoundException at SandboxClassLoader.java:147 Caused by: java.lang.IllegalArgumentException at ClassReader.java:195 io.flutter.plugins.urllauncher.MethodCallHandlerImplTest > startListening_unregistersExistingChannel FAILED java.lang.NoClassDefFoundError at Class.java:-2 Caused by: java.lang.ClassNotFoundException at SandboxClassLoader.java:147 Caused by: java.lang.IllegalArgumentException at ClassReader.java:195 io.flutter.plugins.urllauncher.MethodCallHandlerImplTest > onMethodCall_closeWebView FAILED java.lang.NoClassDefFoundError at Class.java:-2 Caused by: java.lang.ClassNotFoundException at SandboxClassLoader.java:147 Caused by: java.lang.IllegalArgumentException at ClassReader.java:195 io.flutter.plugins.urllauncher.MethodCallHandlerImplTest > onMethodCall_canLaunchReturnsFalse FAILED java.lang.NoClassDefFoundError at Class.java:-2 Caused by: java.lang.ClassNotFoundException at SandboxClassLoader.java:147 Caused by: java.lang.IllegalArgumentException at ClassReader.java:195 io.flutter.plugins.urllauncher.MethodCallHandlerImplTest > onMethodCall_launchReturnsNoActivityError FAILED java.lang.NoClassDefFoundError at Class.java:-2 Caused by: java.lang.ClassNotFoundException at SandboxClassLoader.java:147 Caused by: java.lang.IllegalArgumentException at ClassReader.java:195 io.flutter.plugins.urllauncher.MethodCallHandlerImplTest > startListening_registersChannel FAILED java.lang.NoClassDefFoundError at Class.java:-2 Caused by: java.lang.ClassNotFoundException at SandboxClassLoader.java:147 Caused by: java.lang.IllegalArgumentException at ClassReader.java:195 io.flutter.plugins.urllauncher.MethodCallHandlerImplTest > onMethodCall_launchReturnsTrue FAILED java.lang.NoClassDefFoundError at Class.java:-2 Caused by: java.lang.ClassNotFoundException at SandboxClassLoader.java:147 Caused by: java.lang.IllegalArgumentException at ClassReader.java:195 io.flutter.plugins.urllauncher.MethodCallHandlerImplTest > onMethodCall_launchReturnsActivityNotFoundError FAILED java.lang.NoClassDefFoundError at Class.java:-2 Caused by: java.lang.ClassNotFoundException at SandboxClassLoader.java:147 Caused by: java.lang.IllegalArgumentException at ClassReader.java:195 io.flutter.plugins.urllauncher.MethodCallHandlerImplTest > onMethodCall_canLaunchReturnsTrue FAILED java.lang.NoClassDefFoundError at Class.java:-2 Caused by: java.lang.ClassNotFoundException at SandboxClassLoader.java:147 Caused by: java.lang.IllegalArgumentException at ClassReader.java:195 io.flutter.plugins.urllauncher.WebViewActivityTest > extractHeaders_returnsEmptyMapWhenHeadersBundleNull PASSED 11 tests completed, 10 failed > Task :url_launcher_android:testDebugUnitTest FAILED FAILURE: Build failed with an exception. * What went wrong: Execution failed for task ':url_launcher_android:testDebugUnitTest'. > There were failing tests. See the report at: file:///home/aun/Programming/Projects/FlutterProjects/revanced-manager/build/url_launcher_android/reports/tests/testDebugUnitTest/index.html * Try: > Run with --stacktrace option to get the stack trace. > Run with --info or --debug option to get more log output. > Run with --scan to get full insights. * Get more help at https://help.gradle.org Deprecated Gradle features were used in this build, making it incompatible with Gradle 8.0. You can use '--warning-mode all' to show the individual deprecation warnings and determine if they come from your own scripts or plugins. See https://docs.gradle.org/7.6.1/userguide/command_line_interface.html#sec:command_line_warnings BUILD FAILED in 4m 55s 1362 actionable tasks: 998 executed, 364 up-to-date " /> - - </testcase> - - </testsuite> -</testsuites> diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 0000000000..36d069afc2 --- /dev/null +++ b/gradle.properties @@ -0,0 +1,26 @@ +# Project-wide Gradle settings. +# 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. +org.gradle.jvmargs=-Xmx2048m -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 +# AndroidX package structure to make it clearer which packages are bundled with the +# Android operating system, and which are packaged with your app's APK +# https://developer.android.com/topic/libraries/support-library/androidx-rn +android.useAndroidX=true +# Kotlin code style for this project: "official" or "obsolete": +kotlin.code.style=official +# Enables namespacing of each library's R class so that its R class includes only the +# resources declared in the library itself and none from the library's dependencies, +# thereby reducing the size of the R class for that library +android.nonTransitiveRClass=true +android.nonFinalResIds=false +org.gradle.configuration-cache=true +org.gradle.caching=true diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml new file mode 100644 index 0000000000..70749da4b2 --- /dev/null +++ b/gradle/libs.versions.toml @@ -0,0 +1,147 @@ +[versions] +ktx = "1.15.0" +material3 = "1.3.1" +ui-tooling = "1.7.7" +viewmodel-lifecycle = "2.8.7" +splash-screen = "1.0.1" +activity = "1.10.0" +appcompat = "1.7.0" +preferences-datastore = "1.1.2" +work-runtime = "2.10.0" +compose-bom = "2025.01.01" +navigation = "2.8.6" +accompanist = "0.37.0" +placeholder = "1.1.2" +reorderable = "2.4.3" +serialization = "1.8.0" +collection = "0.3.8" +datetime = "0.6.1" +room-version = "2.6.1" +revanced-patcher = "21.0.0" +revanced-library = "3.0.2" +koin = "3.5.3" +ktor = "2.3.9" +markdown-renderer = "0.30.0" +fading-edges = "1.0.4" +kotlin = "2.1.10" +android-gradle-plugin = "8.8.0" +dev-tools-gradle-plugin = "2.1.10-1.0.29" +about-libraries-gradle-plugin = "11.5.0" +binary-compatibility-validator = "0.17.0" +coil = "2.7.0" +app-icon-loader-coil = "1.5.0" +skrapeit = "1.2.2" +libsu = "6.0.0" +scrollbars = "1.0.4" +enumutil = "1.1.1" +compose-icons = "1.2.4" +kotlin-process = "1.5.1" +hidden-api-stub = "4.3.3" + +[libraries] +# AndroidX Core +androidx-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "ktx" } +runtime-ktx = { group = "androidx.lifecycle", name = "lifecycle-runtime-ktx", version.ref = "viewmodel-lifecycle" } +runtime-compose = { group = "androidx.lifecycle", name = "lifecycle-runtime-compose", version.ref = "viewmodel-lifecycle" } +splash-screen = { group = "androidx.core", name = "core-splashscreen", version.ref = "splash-screen" } +activity-compose = { group = "androidx.activity", name = "activity-compose", version.ref = "activity" } +activity-ktx = { group = "androidx.activity", name = "activity-ktx", version.ref = "activity" } +work-runtime-ktx = { group = "androidx.work", name = "work-runtime-ktx", version.ref = "work-runtime" } +preferences-datastore = { group = "androidx.datastore", name = "datastore-preferences", version.ref = "preferences-datastore" } +appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "appcompat" } + +# Compose +compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "compose-bom" } +compose-ui = { group = "androidx.compose.ui", name = "ui" } +compose-ui-preview = { group = "androidx.compose.ui", name = "ui-tooling-preview" } +compose-ui-tooling = { group = "androidx.compose.ui", name = "ui-tooling", version.ref = "ui-tooling" } +compose-livedata = { group = "androidx.compose.runtime", name = "runtime-livedata" } +compose-material3 = { group = "androidx.compose.material3", name = "material3", version.ref = "material3" } +compose-material-icons-extended = { group = "androidx.compose.material", name = "material-icons-extended" } +navigation-compose = { group = "androidx.navigation", name = "navigation-compose", version.ref = "navigation" } + +# Coil +coil-compose = { group = "io.coil-kt", name = "coil-compose", version.ref = "coil" } +coil-appiconloader = { group = "me.zhanghai.android.appiconloader", name = "appiconloader-coil", version.ref = "app-icon-loader-coil" } + +# Accompanist +accompanist-drawablepainter = { group = "com.google.accompanist", name = "accompanist-drawablepainter", version.ref = "accompanist" } + +# Placeholder +placeholder-material3 = { group = "io.github.fornewid", name = "placeholder-material3", version.ref = "placeholder"} + +# Kotlinx +kotlinx-serialization-json = { group = "org.jetbrains.kotlinx", name = "kotlinx-serialization-json", version.ref = "serialization" } +kotlinx-collection-immutable = { group = "org.jetbrains.kotlinx", name = "kotlinx-collections-immutable", version.ref = "collection" } +kotlinx-datetime = { group = "org.jetbrains.kotlinx", name = "kotlinx-datetime", version.ref = "datetime" } + +# Room +room-runtime = { group = "androidx.room", name = "room-runtime", version.ref = "room-version" } +room-ktx = { group = "androidx.room", name = "room-ktx", version.ref = "room-version" } +room-compiler = { group = "androidx.room", name = "room-compiler", version.ref = "room-version" } + +# Patcher +revanced-patcher = { group = "app.revanced", name = "revanced-patcher", version.ref = "revanced-patcher" } +revanced-library = { group = "app.revanced", name = "revanced-library", version.ref = "revanced-library" } + +# Koin +koin-android = { group = "io.insert-koin", name = "koin-android", version.ref = "koin" } +koin-compose = { group = "io.insert-koin", name = "koin-androidx-compose", version.ref = "koin" } +koin-compose-navigation = { group = "io.insert-koin", name = "koin-androidx-compose-navigation", version.ref = "koin" } +koin-workmanager = { group = "io.insert-koin", name = "koin-androidx-workmanager", version.ref = "koin" } + +# About Libraries +about-libraries = { group = "com.mikepenz", name = "aboutlibraries-compose", version.ref = "about-libraries-gradle-plugin" } + +# Ktor +ktor-core = { group = "io.ktor", name = "ktor-client-core", version.ref = "ktor" } +ktor-logging = { group = "io.ktor", name = "ktor-client-logging", version.ref = "ktor" } +ktor-okhttp = { group = "io.ktor", name = "ktor-client-okhttp", version.ref = "ktor" } +ktor-content-negotiation = { group = "io.ktor", name = "ktor-client-content-negotiation", version.ref = "ktor" } +ktor-serialization = { group = "io.ktor", name = "ktor-serialization-kotlinx-json", version.ref = "ktor" } + +# HTML Scraper +skrapeit-dsl = { group = "it.skrape", name = "skrapeit-dsl", version.ref = "skrapeit" } +skrapeit-parser = { group = "it.skrape", name = "skrapeit-html-parser", version.ref = "skrapeit" } + +# Markdown +markdown-renderer = { group = "com.mikepenz", name = "multiplatform-markdown-renderer-m3", version.ref = "markdown-renderer" } + +# Fading Edges +fading-edges = { group = "com.github.GIGAMOLE", name = "ComposeFadingEdges", version.ref = "fading-edges"} + +# Native processes +kotlin-process = { group = "com.github.pgreze", name = "kotlin-process", version.ref = "kotlin-process" } + +# HiddenAPI +hidden-api-stub = { group = "dev.rikka.hidden", name = "stub", version.ref = "hidden-api-stub" } + +# LibSU +libsu-core = { group = "com.github.topjohnwu.libsu", name = "core", version.ref = "libsu" } +libsu-service = { group = "com.github.topjohnwu.libsu", name = "service", version.ref = "libsu" } +libsu-nio = { group = "com.github.topjohnwu.libsu", name = "nio", version.ref = "libsu" } + +# Scrollbars +scrollbars = { group = "com.github.GIGAMOLE", name = "ComposeScrollbars", version.ref = "scrollbars" } + +# EnumUtil +enumutil = { group = "io.github.materiiapps", name = "enumutil", version.ref = "enumutil" } +enumutil-ksp = { group = "io.github.materiiapps", name = "enumutil-ksp", version.ref = "enumutil" } + +# Reorderable lists +reorderable = { module = "sh.calvin.reorderable:reorderable", version.ref = "reorderable" } + +# Compose Icons +# switch to br.com.devsrsouza.compose.icons after DevSrSouza/compose-icons#30 is merged +compose-icons-fontawesome = { group = "com.github.BenjaminHalko.compose-icons", name = "font-awesome", version.ref = "compose-icons" } + +[plugins] +android-application = { id = "com.android.application", version.ref = "android-gradle-plugin" } +android-library = { id = "com.android.library", version.ref = "android-gradle-plugin" } +kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } +kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" } +kotlin-parcelize = { id = "org.jetbrains.kotlin.plugin.parcelize", version.ref = "kotlin" } +compose-compiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" } +devtools = { id = "com.google.devtools.ksp", version.ref = "dev-tools-gradle-plugin" } +about-libraries = { id = "com.mikepenz.aboutlibraries.plugin", version.ref = "about-libraries-gradle-plugin" } +binary-compatibility-validator = { id = "org.jetbrains.kotlinx.binary-compatibility-validator", version.ref = "binary-compatibility-validator" } \ No newline at end of file diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000..a4b76b9530 Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/android/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties similarity index 60% rename from android/gradle/wrapper/gradle-wrapper.properties rename to gradle/wrapper/gradle-wrapper.properties index 76777308a3..d71047787f 100644 --- a/android/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,7 +1,8 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionSha256Sum=a01b6587e15fe7ed120a0ee299c25982a1eee045abd6a9dd5e216b2f628ef9ac -distributionUrl=https\://services.gradle.org/distributions/gradle-7.6.2-bin.zip +distributionSha256Sum=8d97a97984f6cbd2b85fe4c60a743440a347544bf18818048e611f5288d46c94 +distributionUrl=https\://services.gradle.org/distributions/gradle-8.12.1-bin.zip networkTimeout=10000 +validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew new file mode 100755 index 0000000000..f3b75f3b0d --- /dev/null +++ b/gradlew @@ -0,0 +1,251 @@ +#!/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. +# +# SPDX-License-Identifier: Apache-2.0 +# + +############################################################################## +# +# 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/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# 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 + +# 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 -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +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 + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + 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" && ! "$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 + +# 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" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + 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 + # 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 +fi + + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, 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 new file mode 100644 index 0000000000..9b42019c79 --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,94 @@ +@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 +@rem SPDX-License-Identifier: Apache-2.0 +@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/lib/app/app.dart b/lib/app/app.dart deleted file mode 100644 index 76c32b193c..0000000000 --- a/lib/app/app.dart +++ /dev/null @@ -1,43 +0,0 @@ -import 'package:revanced_manager/services/github_api.dart'; -import 'package:revanced_manager/services/manager_api.dart'; -import 'package:revanced_manager/services/patcher_api.dart'; -import 'package:revanced_manager/services/revanced_api.dart'; -import 'package:revanced_manager/services/toast.dart'; -import 'package:revanced_manager/ui/views/app_selector/app_selector_view.dart'; -import 'package:revanced_manager/ui/views/contributors/contributors_view.dart'; -import 'package:revanced_manager/ui/views/home/home_viewmodel.dart'; -import 'package:revanced_manager/ui/views/installer/installer_view.dart'; -import 'package:revanced_manager/ui/views/navigation/navigation_view.dart'; -import 'package:revanced_manager/ui/views/navigation/navigation_viewmodel.dart'; -import 'package:revanced_manager/ui/views/patcher/patcher_view.dart'; -import 'package:revanced_manager/ui/views/patcher/patcher_viewmodel.dart'; -import 'package:revanced_manager/ui/views/patches_selector/patches_selector_view.dart'; -import 'package:revanced_manager/ui/views/settings/settings_view.dart'; -import 'package:revanced_manager/ui/widgets/appInfoView/app_info_view.dart'; -import 'package:stacked/stacked_annotations.dart'; -import 'package:stacked_services/stacked_services.dart'; - -@StackedApp( - routes: [ - MaterialRoute(page: NavigationView), - MaterialRoute(page: PatcherView), - MaterialRoute(page: AppSelectorView), - MaterialRoute(page: PatchesSelectorView), - MaterialRoute(page: InstallerView), - MaterialRoute(page: SettingsView), - MaterialRoute(page: ContributorsView), - MaterialRoute(page: AppInfoView), - ], - dependencies: [ - LazySingleton(classType: NavigationViewModel), - LazySingleton(classType: HomeViewModel), - LazySingleton(classType: PatcherViewModel), - LazySingleton(classType: NavigationService), - LazySingleton(classType: ManagerAPI), - LazySingleton(classType: PatcherAPI), - LazySingleton(classType: RevancedAPI), - LazySingleton(classType: GithubAPI), - LazySingleton(classType: Toast), - ], -) -class AppSetup {} diff --git a/lib/main.dart b/lib/main.dart deleted file mode 100644 index 29b715b9a9..0000000000 --- a/lib/main.dart +++ /dev/null @@ -1,65 +0,0 @@ -import 'dart:developer'; - -import 'package:flutter/material.dart'; -import 'package:flutter_i18n/flutter_i18n.dart'; -import 'package:flutter_localizations/flutter_localizations.dart'; -import 'package:revanced_manager/app/app.locator.dart'; -import 'package:revanced_manager/services/github_api.dart'; -import 'package:revanced_manager/services/manager_api.dart'; -import 'package:revanced_manager/services/revanced_api.dart'; -import 'package:revanced_manager/ui/theme/dynamic_theme_builder.dart'; -import 'package:revanced_manager/ui/views/navigation/navigation_view.dart'; -import 'package:shared_preferences/shared_preferences.dart'; -import 'package:stacked_themes/stacked_themes.dart'; -import 'package:timezone/data/latest.dart' as tz; - -late SharedPreferences prefs; -Future main() async { - await ThemeManager.initialise(); - await setupLocator(); - WidgetsFlutterBinding.ensureInitialized(); - await locator<ManagerAPI>().initialize(); - final String apiUrl = locator<ManagerAPI>().getApiUrl(); - await locator<RevancedAPI>().initialize(apiUrl); - final String repoUrl = locator<ManagerAPI>().getRepoUrl(); - locator<GithubAPI>().initialize(repoUrl); - tz.initializeTimeZones(); - prefs = await SharedPreferences.getInstance(); - - runApp(const MyApp()); -} - -class MyApp extends StatelessWidget { - const MyApp({Key? key}) : super(key: key); - - @override - Widget build(BuildContext context) { - // String rawLocale = prefs.getString('language') ?? 'en_US'; - // String replaceLocale = rawLocale.replaceAll('_', '-'); - // List<String> localeList = replaceLocale.split('-'); - // Locale locale = Locale(localeList[0], localeList[1]); - const Locale locale = Locale('en', 'US'); - - return DynamicThemeBuilder( - title: 'ReVanced Manager', - home: const NavigationView(), - localizationsDelegates: [ - FlutterI18nDelegate( - translationLoader: FileTranslationLoader( - fallbackFile: 'en_US', - forcedLocale: locale, - basePath: 'assets/i18n', - useCountryCode: true, - ), - missingTranslationHandler: (key, locale) { - log( - '--> Missing translation: key: $key, languageCode: ${locale?.languageCode}', - ); - }, - ), - GlobalMaterialLocalizations.delegate, - GlobalWidgetsLocalizations.delegate, - ], - ); - } -} diff --git a/lib/models/patch.dart b/lib/models/patch.dart deleted file mode 100644 index 7acf05ba7e..0000000000 --- a/lib/models/patch.dart +++ /dev/null @@ -1,48 +0,0 @@ -import 'package:json_annotation/json_annotation.dart'; -import 'package:revanced_manager/utils/string.dart'; - -part 'patch.g.dart'; - -@JsonSerializable() -class Patch { - Patch({ - required this.name, - required this.description, - required this.excluded, - required this.dependencies, - required this.compatiblePackages, - }); - - factory Patch.fromJson(Map<String, dynamic> json) => _$PatchFromJson(json); - final String name; - final String description; - final bool excluded; - final List<String> dependencies; - final List<Package> compatiblePackages; - - Map<String, dynamic> toJson() => _$PatchToJson(this); - - String getSimpleName() { - return name - .replaceAll('-', ' ') - .split('-') - .join(' ') - .toTitleCase() - .replaceFirst('Microg', 'MicroG'); - } -} - -@JsonSerializable() -class Package { - Package({ - required this.name, - required this.versions, - }); - - factory Package.fromJson(Map<String, dynamic> json) => - _$PackageFromJson(json); - final String name; - final List<String> versions; - - Map toJson() => _$PackageToJson(this); -} diff --git a/lib/models/patched_application.dart b/lib/models/patched_application.dart deleted file mode 100644 index 90bfb9a31e..0000000000 --- a/lib/models/patched_application.dart +++ /dev/null @@ -1,48 +0,0 @@ -import 'dart:convert'; -import 'dart:typed_data'; -import 'package:json_annotation/json_annotation.dart'; - -part 'patched_application.g.dart'; - -@JsonSerializable() -class PatchedApplication { - PatchedApplication({ - required this.name, - required this.packageName, - required this.originalPackageName, - required this.version, - required this.apkFilePath, - required this.icon, - required this.patchDate, - this.isRooted = false, - this.isFromStorage = false, - this.hasUpdates = false, - this.appliedPatches = const [], - this.changelog = const [], - }); - - factory PatchedApplication.fromJson(Map<String, dynamic> json) => - _$PatchedApplicationFromJson(json); - String name; - String packageName; - String originalPackageName; - String version; - final String apkFilePath; - @JsonKey( - fromJson: decodeBase64, - toJson: encodeBase64, - ) - Uint8List icon; - DateTime patchDate; - bool isRooted; - bool isFromStorage; - bool hasUpdates; - List<String> appliedPatches; - List<String> changelog; - - Map<String, dynamic> toJson() => _$PatchedApplicationToJson(this); - - static Uint8List decodeBase64(String icon) => base64.decode(icon); - - static String encodeBase64(Uint8List bytes) => base64.encode(bytes); -} diff --git a/lib/services/github_api.dart b/lib/services/github_api.dart deleted file mode 100644 index de8c160b2b..0000000000 --- a/lib/services/github_api.dart +++ /dev/null @@ -1,268 +0,0 @@ -import 'dart:convert'; -import 'dart:io'; -import 'package:collection/collection.dart'; -import 'package:dio/dio.dart'; -import 'package:dio_cache_interceptor/dio_cache_interceptor.dart'; -import 'package:flutter/foundation.dart'; -import 'package:flutter_cache_manager/flutter_cache_manager.dart'; -import 'package:injectable/injectable.dart'; -import 'package:revanced_manager/app/app.locator.dart'; -import 'package:revanced_manager/models/patch.dart'; -import 'package:revanced_manager/services/manager_api.dart'; - -@lazySingleton -class GithubAPI { - late Dio _dio = Dio(); - late final ManagerAPI _managerAPI = locator<ManagerAPI>(); - - final _cacheOptions = CacheOptions( - store: MemCacheStore(), - maxStale: const Duration(days: 1), - priority: CachePriority.high, - ); - - final Map<String, String> repoAppPath = { - 'com.google.android.youtube': 'youtube', - 'com.google.android.apps.youtube.music': 'music', - 'com.twitter.android': 'twitter', - 'com.reddit.frontpage': 'reddit', - 'com.zhiliaoapp.musically': 'tiktok', - 'de.dwd.warnapp': 'warnwetter', - 'com.garzotto.pflotsh.ecmwf_a': 'ecmwf', - 'com.spotify.music': 'spotify', - }; - - Future<void> initialize(String repoUrl) async { - try { - _dio = Dio( - BaseOptions( - baseUrl: repoUrl, - ), - ); - - _dio.interceptors.add(DioCacheInterceptor(options: _cacheOptions)); - } on Exception catch (e) { - if (kDebugMode) { - print(e); - } - } - } - - Future<void> clearAllCache() async { - try { - await _cacheOptions.store!.clean(); - } on Exception catch (e) { - if (kDebugMode) { - print(e); - } - } - } - - Future<Map<String, dynamic>?> getLatestRelease( - String repoName, - ) async { - try { - final response = await _dio.get( - '/repos/$repoName/releases', - ); - return response.data[0]; - } on Exception catch (e) { - if (kDebugMode) { - print(e); - } - return null; - } - } - - Future<Map<String, dynamic>?> getPatchesRelease( - String repoName, - String version, - ) async { - try { - final response = await _dio.get( - '/repos/$repoName/releases/tags/$version', - ); - return response.data; - } on Exception catch (e) { - if (kDebugMode) { - print(e); - } - return null; - } - } - - Future<Map<String, dynamic>?> getLatestPatchesRelease( - String repoName, - ) async { - try { - final response = await _dio.get( - '/repos/$repoName/releases/latest', - ); - return response.data; - } on Exception catch (e) { - if (kDebugMode) { - print(e); - } - return null; - } - } - - Future<Map<String, dynamic>?> getLatestManagerRelease( - String repoName, - ) async { - try { - final response = await _dio.get( - '/repos/$repoName/releases', - ); - final Map<String, dynamic> releases = response.data[0]; - int updates = 0; - final String currentVersion = - await ManagerAPI().getCurrentManagerVersion(); - while (response.data[updates]['tag_name'] != 'v$currentVersion') { - updates++; - } - for (int i = 1; i < updates; i++) { - releases.update( - 'body', - (value) => - value + - '\n' + - '# ' + - response.data[i]['tag_name'] + - '\n' + - response.data[i]['body'], - ); - } - return releases; - } on Exception catch (e) { - if (kDebugMode) { - print(e); - } - return null; - } - } - - Future<List<String>> getCommits( - String packageName, - String repoName, - DateTime since, - ) async { - final String path = - 'src/main/kotlin/app/revanced/patches/${repoAppPath[packageName]}'; - try { - final response = await _dio.get( - '/repos/$repoName/commits', - queryParameters: { - 'path': path, - 'since': since.toIso8601String(), - }, - ); - final List<dynamic> commits = response.data; - return commits - .map( - (commit) => commit['commit']['message'].split('\n')[0] + - ' - ' + - commit['commit']['author']['name'] + - '\n' as String, - ) - .toList(); - } on Exception catch (e) { - if (kDebugMode) { - print(e); - } - } - return []; - } - - Future<File?> getLatestReleaseFile( - String extension, - String repoName, - ) async { - try { - final Map<String, dynamic>? release = await getLatestRelease(repoName); - if (release != null) { - final Map<String, dynamic>? asset = - (release['assets'] as List<dynamic>).firstWhereOrNull( - (asset) => (asset['name'] as String).endsWith(extension), - ); - if (asset != null) { - return await DefaultCacheManager().getSingleFile( - asset['browser_download_url'], - ); - } - } - } on Exception catch (e) { - if (kDebugMode) { - print(e); - } - } - return null; - } - - Future<File?> getPatchesReleaseFile( - String extension, - String repoName, - String version, - String url, - ) async { - try { - if (url.isNotEmpty) { - return await DefaultCacheManager().getSingleFile( - url, - ); - } - final Map<String, dynamic>? release = - await getPatchesRelease(repoName, version); - if (release != null) { - final Map<String, dynamic>? asset = - (release['assets'] as List<dynamic>).firstWhereOrNull( - (asset) => (asset['name'] as String).endsWith(extension), - ); - if (asset != null) { - final String downloadUrl = asset['browser_download_url']; - if (extension == '.apk') { - _managerAPI.setIntegrationsDownloadURL(downloadUrl); - } else if (extension == '.json') { - _managerAPI.setPatchesDownloadURL(downloadUrl, false); - } else { - _managerAPI.setPatchesDownloadURL(downloadUrl, true); - } - return await DefaultCacheManager().getSingleFile( - downloadUrl, - ); - } - } - } on Exception catch (e) { - if (kDebugMode) { - print(e); - } - } - return null; - } - - Future<List<Patch>> getPatches( - String repoName, - String version, - String url, - ) async { - List<Patch> patches = []; - try { - final File? f = await getPatchesReleaseFile( - '.json', - repoName, - version, - url, - ); - if (f != null) { - final List<dynamic> list = jsonDecode(f.readAsStringSync()); - patches = list.map((patch) => Patch.fromJson(patch)).toList(); - } - } on Exception catch (e) { - if (kDebugMode) { - print(e); - } - } - - return patches; - } -} diff --git a/lib/services/manager_api.dart b/lib/services/manager_api.dart deleted file mode 100644 index 1391f707db..0000000000 --- a/lib/services/manager_api.dart +++ /dev/null @@ -1,759 +0,0 @@ -import 'dart:convert'; -import 'dart:io'; -import 'package:device_apps/device_apps.dart'; -import 'package:flutter/foundation.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_i18n/widgets/I18nText.dart'; -import 'package:injectable/injectable.dart'; -import 'package:package_info_plus/package_info_plus.dart'; -import 'package:path_provider/path_provider.dart'; -import 'package:revanced_manager/app/app.locator.dart'; -import 'package:revanced_manager/models/patch.dart'; -import 'package:revanced_manager/models/patched_application.dart'; -import 'package:revanced_manager/services/github_api.dart'; -import 'package:revanced_manager/services/revanced_api.dart'; -import 'package:revanced_manager/services/root_api.dart'; -import 'package:revanced_manager/ui/widgets/shared/custom_material_button.dart'; -import 'package:revanced_manager/utils/check_for_supported_patch.dart'; -import 'package:shared_preferences/shared_preferences.dart'; -import 'package:timeago/timeago.dart'; - -@lazySingleton -class ManagerAPI { - final RevancedAPI _revancedAPI = locator<RevancedAPI>(); - final GithubAPI _githubAPI = locator<GithubAPI>(); - final RootAPI _rootAPI = RootAPI(); - final String patcherRepo = 'revanced-patcher'; - final String cliRepo = 'revanced-cli'; - late SharedPreferences _prefs; - bool isRooted = false; - String storedPatchesFile = '/selected-patches.json'; - String keystoreFile = - '/sdcard/Android/data/app.revanced.manager.flutter/files/revanced-manager.keystore'; - String defaultKeystorePassword = 's3cur3p@ssw0rd'; - String defaultApiUrl = 'https://api.revanced.app/'; - String defaultRepoUrl = 'https://api.github.com'; - String defaultPatcherRepo = 'revanced/revanced-patcher'; - String defaultPatchesRepo = 'revanced/revanced-patches'; - String defaultIntegrationsRepo = 'revanced/revanced-integrations'; - String defaultCliRepo = 'revanced/revanced-cli'; - String defaultManagerRepo = 'revanced/revanced-manager'; - String? patchesVersion = ''; - String? integrationsVersion = ''; - bool isDefaultPatchesRepo() { - return getPatchesRepo() == 'revanced/revanced-patches'; - } - - bool isDefaultIntegrationsRepo() { - return getIntegrationsRepo() == 'revanced/revanced-integrations'; - } - - Future<void> initialize() async { - _prefs = await SharedPreferences.getInstance(); - isRooted = await _rootAPI.isRooted(); - storedPatchesFile = - (await getApplicationDocumentsDirectory()).path + storedPatchesFile; - } - - String getApiUrl() { - return _prefs.getString('apiUrl') ?? defaultApiUrl; - } - - Future<void> setApiUrl(String url) async { - if (url.isEmpty || url == ' ') { - url = defaultApiUrl; - } - await _revancedAPI.initialize(url); - await _revancedAPI.clearAllCache(); - await _prefs.setString('apiUrl', url); - } - - String getRepoUrl() { - return _prefs.getString('repoUrl') ?? defaultRepoUrl; - } - - Future<void> setRepoUrl(String url) async { - if (url.isEmpty || url == ' ') { - url = defaultRepoUrl; - } - await _prefs.setString('repoUrl', url); - } - - String getPatchesDownloadURL(bool bundle) { - return _prefs.getString('patchesDownloadURL-$bundle') ?? ''; - } - - Future<void> setPatchesDownloadURL(String value, bool bundle) async { - await _prefs.setString('patchesDownloadURL-$bundle', value); - } - - String getPatchesRepo() { - return _prefs.getString('patchesRepo') ?? defaultPatchesRepo; - } - - Future<void> setPatchesRepo(String value) async { - if (value.isEmpty || value.startsWith('/') || value.endsWith('/')) { - value = defaultPatchesRepo; - } - await _prefs.setString('patchesRepo', value); - } - - bool getPatchesConsent() { - return _prefs.getBool('patchesConsent') ?? false; - } - - Future<void> setPatchesConsent(bool consent) async { - await _prefs.setBool('patchesConsent', consent); - } - - bool isPatchesAutoUpdate() { - return _prefs.getBool('patchesAutoUpdate') ?? false; - } - - bool isPatchesChangeEnabled() { - if (getPatchedApps().isNotEmpty && !isChangingToggleModified()) { - for (final apps in getPatchedApps()) { - if (getSavedPatches(apps.originalPackageName) - .indexWhere((patch) => patch.excluded) != - -1) { - setPatchesChangeWarning(false); - setPatchesChangeEnabled(true); - break; - } - } - } - return _prefs.getBool('patchesChangeEnabled') ?? false; - } - - void setPatchesChangeEnabled(bool value) { - _prefs.setBool('patchesChangeEnabled', value); - } - - bool showPatchesChangeWarning() { - return _prefs.getBool('showPatchesChangeWarning') ?? true; - } - - void setPatchesChangeWarning(bool value) { - _prefs.setBool('showPatchesChangeWarning', !value); - } - - bool isChangingToggleModified() { - return _prefs.getBool('isChangingToggleModified') ?? false; - } - - void setChangingToggleModified(bool value) { - _prefs.setBool('isChangingToggleModified', value); - } - - Future<void> setPatchesAutoUpdate(bool value) async { - await _prefs.setBool('patchesAutoUpdate', value); - } - - List<Patch> getSavedPatches(String packageName) { - final List<String> patchesJson = - _prefs.getStringList('savedPatches-$packageName') ?? []; - final List<Patch> patches = patchesJson.map((String patchJson) { - return Patch.fromJson(jsonDecode(patchJson)); - }).toList(); - return patches; - } - - Future<void> savePatches(List<Patch> patches, String packageName) async { - final List<String> patchesJson = patches.map((Patch patch) { - return jsonEncode(patch.toJson()); - }).toList(); - await _prefs.setStringList('savedPatches-$packageName', patchesJson); - } - - String getIntegrationsDownloadURL() { - return _prefs.getString('integrationsDownloadURL') ?? ''; - } - - Future<void> setIntegrationsDownloadURL(String value) async { - await _prefs.setString('integrationsDownloadURL', value); - } - - List<Patch> getUsedPatches(String packageName) { - final List<String> patchesJson = - _prefs.getStringList('usedPatches-$packageName') ?? []; - final List<Patch> patches = patchesJson.map((String patchJson) { - return Patch.fromJson(jsonDecode(patchJson)); - }).toList(); - return patches; - } - - Future<void> setUsedPatches(List<Patch> patches, String packageName) async { - final List<String> patchesJson = patches.map((Patch patch) { - return jsonEncode(patch.toJson()); - }).toList(); - await _prefs.setStringList('usedPatches-$packageName', patchesJson); - } - - String getIntegrationsRepo() { - return _prefs.getString('integrationsRepo') ?? defaultIntegrationsRepo; - } - - Future<void> setIntegrationsRepo(String value) async { - if (value.isEmpty || value.startsWith('/') || value.endsWith('/')) { - value = defaultIntegrationsRepo; - } - await _prefs.setString('integrationsRepo', value); - } - - bool getUseDynamicTheme() { - return _prefs.getBool('useDynamicTheme') ?? false; - } - - Future<void> setUseDynamicTheme(bool value) async { - await _prefs.setBool('useDynamicTheme', value); - } - - bool getUseDarkTheme() { - return _prefs.getBool('useDarkTheme') ?? false; - } - - Future<void> setUseDarkTheme(bool value) async { - await _prefs.setBool('useDarkTheme', value); - } - - bool areUniversalPatchesEnabled() { - return _prefs.getBool('universalPatchesEnabled') ?? false; - } - - Future<void> enableUniversalPatchesStatus(bool value) async { - await _prefs.setBool('universalPatchesEnabled', value); - } - - bool areExperimentalPatchesEnabled() { - return _prefs.getBool('experimentalPatchesEnabled') ?? false; - } - - Future<void> enableExperimentalPatchesStatus(bool value) async { - await _prefs.setBool('experimentalPatchesEnabled', value); - } - - Future<void> setKeystorePassword(String password) async { - await _prefs.setString('keystorePassword', password); - } - - String getKeystorePassword() { - return _prefs.getString('keystorePassword') ?? defaultKeystorePassword; - } - - Future<void> deleteTempFolder() async { - final Directory dir = Directory('/data/local/tmp/revanced-manager'); - if (await dir.exists()) { - await dir.delete(recursive: true); - } - } - - Future<void> deleteKeystore() async { - final File keystore = File( - keystoreFile, - ); - if (await keystore.exists()) { - await keystore.delete(); - } - } - - List<PatchedApplication> getPatchedApps() { - final List<String> apps = _prefs.getStringList('patchedApps') ?? []; - return apps.map((a) => PatchedApplication.fromJson(jsonDecode(a))).toList(); - } - - Future<void> setPatchedApps( - List<PatchedApplication> patchedApps, - ) async { - if (patchedApps.length > 1) { - patchedApps.sort((a, b) => a.name.compareTo(b.name)); - } - await _prefs.setStringList( - 'patchedApps', - patchedApps.map((a) => json.encode(a.toJson())).toList(), - ); - } - - Future<void> savePatchedApp(PatchedApplication app) async { - final List<PatchedApplication> patchedApps = getPatchedApps(); - patchedApps.removeWhere((a) => a.packageName == app.packageName); - final ApplicationWithIcon? installed = await DeviceApps.getApp( - app.packageName, - true, - ) as ApplicationWithIcon?; - if (installed != null) { - app.name = installed.appName; - app.version = installed.versionName!; - app.icon = installed.icon; - } - patchedApps.add(app); - await setPatchedApps(patchedApps); - } - - Future<void> deletePatchedApp(PatchedApplication app) async { - final List<PatchedApplication> patchedApps = getPatchedApps(); - patchedApps.removeWhere((a) => a.packageName == app.packageName); - await setPatchedApps(patchedApps); - } - - Future<void> clearAllData() async { - try { - _revancedAPI.clearAllCache(); - _githubAPI.clearAllCache(); - } on Exception catch (e) { - if (kDebugMode) { - print(e); - } - } - } - - Future<Map<String, List<dynamic>>> getContributors() async { - return await _revancedAPI.getContributors(); - } - - Future<List<Patch>> getPatches() async { - try { - final String repoName = getPatchesRepo(); - final String currentVersion = await getCurrentPatchesVersion(); - final String url = getPatchesDownloadURL(false); - return await _githubAPI.getPatches( - repoName, - currentVersion, - url, - ); - } on Exception catch (e) { - if (kDebugMode) { - print(e); - } - return []; - } - } - - Future<File?> downloadPatches() async { - try { - final String repoName = getPatchesRepo(); - final String currentVersion = await getCurrentPatchesVersion(); - final String url = getPatchesDownloadURL(true); - return await _githubAPI.getPatchesReleaseFile( - '.jar', - repoName, - currentVersion, - url, - ); - } on Exception catch (e) { - if (kDebugMode) { - print(e); - } - return null; - } - } - - Future<File?> downloadIntegrations() async { - try { - final String repoName = getIntegrationsRepo(); - final String currentVersion = await getCurrentIntegrationsVersion(); - final String url = getIntegrationsDownloadURL(); - return await _githubAPI.getPatchesReleaseFile( - '.apk', - repoName, - currentVersion, - url, - ); - } on Exception catch (e) { - if (kDebugMode) { - print(e); - } - return null; - } - } - - Future<File?> downloadManager() async { - return await _revancedAPI.getLatestReleaseFile( - '.apk', - defaultManagerRepo, - ); - } - - Future<String?> getLatestPatchesReleaseTime() async { - if (isDefaultPatchesRepo()) { - return await _revancedAPI.getLatestReleaseTime( - '.json', - defaultPatchesRepo, - ); - } else { - final release = - await _githubAPI.getLatestPatchesRelease(getPatchesRepo()); - if (release != null) { - final DateTime timestamp = - DateTime.parse(release['created_at'] as String); - return format(timestamp, locale: 'en_short'); - } else { - return null; - } - } - } - - Future<String?> getLatestManagerReleaseTime() async { - return await _revancedAPI.getLatestReleaseTime( - '.apk', - defaultManagerRepo, - ); - } - - Future<String?> getLatestManagerVersion() async { - return await _revancedAPI.getLatestReleaseVersion( - '.apk', - defaultManagerRepo, - ); - } - - Future<String?> getLatestIntegrationsVersion() async { - if (isDefaultIntegrationsRepo()) { - return await _revancedAPI.getLatestReleaseVersion( - '.apk', - defaultIntegrationsRepo, - ); - } else { - final release = await _githubAPI.getLatestRelease(getIntegrationsRepo()); - if (release != null) { - return release['tag_name']; - } else { - return null; - } - } - } - - Future<String?> getLatestPatchesVersion() async { - if (isDefaultPatchesRepo()) { - return await _revancedAPI.getLatestReleaseVersion( - '.json', - defaultPatchesRepo, - ); - } else { - final release = - await _githubAPI.getLatestPatchesRelease(getPatchesRepo()); - if (release != null) { - return release['tag_name']; - } else { - return null; - } - } - } - - Future<String> getCurrentManagerVersion() async { - final PackageInfo packageInfo = await PackageInfo.fromPlatform(); - return packageInfo.version; - } - - Future<String> getCurrentPatchesVersion() async { - patchesVersion = _prefs.getString('patchesVersion') ?? '0.0.0'; - if (patchesVersion == '0.0.0' || isPatchesAutoUpdate()) { - final String newPatchesVersion = - await getLatestPatchesVersion() ?? '0.0.0'; - if (patchesVersion != newPatchesVersion && newPatchesVersion != '0.0.0') { - await setCurrentPatchesVersion(newPatchesVersion); - } - } - return patchesVersion!; - } - - Future<void> setCurrentPatchesVersion(String version) async { - await _prefs.setString('patchesVersion', version); - await setPatchesDownloadURL('', false); - await setPatchesDownloadURL('', true); - await downloadPatches(); - } - - Future<String> getCurrentIntegrationsVersion() async { - integrationsVersion = _prefs.getString('integrationsVersion') ?? '0.0.0'; - if (integrationsVersion == '0.0.0' || isPatchesAutoUpdate()) { - final String newIntegrationsVersion = - await getLatestIntegrationsVersion() ?? '0.0.0'; - if (integrationsVersion != newIntegrationsVersion && - newIntegrationsVersion != '0.0.0') { - await setCurrentIntegrationsVersion(newIntegrationsVersion); - } - } - return integrationsVersion!; - } - - Future<void> setCurrentIntegrationsVersion(String version) async { - await _prefs.setString('integrationsVersion', version); - await setIntegrationsDownloadURL(''); - await downloadIntegrations(); - } - - Future<List<PatchedApplication>> getAppsToRemove( - List<PatchedApplication> patchedApps, - ) async { - final List<PatchedApplication> toRemove = []; - for (final PatchedApplication app in patchedApps) { - final bool isRemove = await isAppUninstalled(app); - if (isRemove) { - toRemove.add(app); - } - } - return toRemove; - } - - Future<List<PatchedApplication>> getUnsavedApps( - List<PatchedApplication> patchedApps, - ) async { - final List<PatchedApplication> unsavedApps = []; - final bool hasRootPermissions = await _rootAPI.hasRootPermissions(); - if (hasRootPermissions) { - final List<String> installedApps = await _rootAPI.getInstalledApps(); - for (final String packageName in installedApps) { - if (!patchedApps.any((app) => app.packageName == packageName)) { - final ApplicationWithIcon? application = await DeviceApps.getApp( - packageName, - true, - ) as ApplicationWithIcon?; - if (application != null) { - unsavedApps.add( - PatchedApplication( - name: application.appName, - packageName: application.packageName, - originalPackageName: application.packageName, - version: application.versionName!, - apkFilePath: application.apkFilePath, - icon: application.icon, - patchDate: DateTime.now(), - isRooted: true, - ), - ); - } - } - } - } - final List<Application> userApps = - await DeviceApps.getInstalledApplications(); - for (final Application app in userApps) { - if (app.packageName.startsWith('app.revanced') && - !app.packageName.startsWith('app.revanced.manager.') && - !patchedApps.any((uapp) => uapp.packageName == app.packageName)) { - final ApplicationWithIcon? application = await DeviceApps.getApp( - app.packageName, - true, - ) as ApplicationWithIcon?; - if (application != null) { - unsavedApps.add( - PatchedApplication( - name: application.appName, - packageName: application.packageName, - originalPackageName: application.packageName, - version: application.versionName!, - apkFilePath: application.apkFilePath, - icon: application.icon, - patchDate: DateTime.now(), - ), - ); - } - } - } - return unsavedApps; - } - - Future<void> showPatchesChangeWarningDialog(BuildContext context) { - final ValueNotifier<bool> noShow = - ValueNotifier(!showPatchesChangeWarning()); - return showDialog( - barrierDismissible: false, - context: context, - builder: (context) => WillPopScope( - onWillPop: () async => false, - child: AlertDialog( - backgroundColor: Theme.of(context).colorScheme.secondaryContainer, - title: I18nText('warning'), - content: ValueListenableBuilder( - valueListenable: noShow, - builder: (context, value, child) { - return Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - I18nText( - 'patchItem.patchesChangeWarningDialogText', - child: const Text( - '', - style: TextStyle( - fontSize: 16, - fontWeight: FontWeight.w500, - ), - ), - ), - const SizedBox(height: 8), - CheckboxListTile( - value: value, - contentPadding: EdgeInsets.zero, - title: I18nText( - 'noShowAgain', - ), - onChanged: (selected) { - noShow.value = selected!; - }, - ), - ], - ); - }, - ), - actions: [ - CustomMaterialButton( - label: I18nText('okButton'), - onPressed: () { - setPatchesChangeWarning(noShow.value); - Navigator.of(context).pop(); - }, - ), - ], - ), - ), - ); - } - - Future<void> reAssessSavedApps() async { - final List<PatchedApplication> patchedApps = getPatchedApps(); - final List<PatchedApplication> unsavedApps = - await getUnsavedApps(patchedApps); - patchedApps.addAll(unsavedApps); - final List<PatchedApplication> toRemove = - await getAppsToRemove(patchedApps); - patchedApps.removeWhere((a) => toRemove.contains(a)); - for (final PatchedApplication app in patchedApps) { - app.hasUpdates = - await hasAppUpdates(app.originalPackageName, app.patchDate); - app.changelog = - await getAppChangelog(app.originalPackageName, app.patchDate); - if (!app.hasUpdates) { - final String? currentInstalledVersion = - (await DeviceApps.getApp(app.packageName))?.versionName; - if (currentInstalledVersion != null) { - final String currentSavedVersion = app.version; - final int currentInstalledVersionInt = int.parse( - currentInstalledVersion.replaceAll(RegExp('[^0-9]'), ''), - ); - final int currentSavedVersionInt = int.parse( - currentSavedVersion.replaceAll(RegExp('[^0-9]'), ''), - ); - if (currentInstalledVersionInt > currentSavedVersionInt) { - app.hasUpdates = true; - } - } - } - } - await setPatchedApps(patchedApps); - } - - Future<bool> isAppUninstalled(PatchedApplication app) async { - bool existsRoot = false; - final bool existsNonRoot = await DeviceApps.isAppInstalled(app.packageName); - if (app.isRooted) { - final bool hasRootPermissions = await _rootAPI.hasRootPermissions(); - if (hasRootPermissions) { - existsRoot = await _rootAPI.isAppInstalled(app.packageName); - } - return !existsRoot || !existsNonRoot; - } - return !existsNonRoot; - } - - Future<bool> hasAppUpdates( - String packageName, - DateTime patchDate, - ) async { - final List<String> commits = await _githubAPI.getCommits( - packageName, - getPatchesRepo(), - patchDate, - ); - return commits.isNotEmpty; - } - - Future<List<String>> getAppChangelog( - String packageName, - DateTime patchDate, - ) async { - List<String> newCommits = await _githubAPI.getCommits( - packageName, - getPatchesRepo(), - patchDate, - ); - if (newCommits.isEmpty) { - newCommits = await _githubAPI.getCommits( - packageName, - getPatchesRepo(), - patchDate, - ); - } - return newCommits; - } - - Future<bool> isSplitApk(PatchedApplication patchedApp) async { - Application? app; - if (patchedApp.isFromStorage) { - app = await DeviceApps.getAppFromStorage(patchedApp.apkFilePath); - } else { - app = await DeviceApps.getApp(patchedApp.packageName); - } - return app != null && app.isSplit; - } - - Future<void> setSelectedPatches( - String app, - List<String> patches, - ) async { - final File selectedPatchesFile = File(storedPatchesFile); - final Map<String, dynamic> patchesMap = await readSelectedPatchesFile(); - if (patches.isEmpty) { - patchesMap.remove(app); - } else { - patchesMap[app] = patches; - } - selectedPatchesFile.writeAsString(jsonEncode(patchesMap)); - } - - // get default patches for app - Future<List<String>> getDefaultPatches() async { - final List<Patch> patches = await getPatches(); - final List<String> defaultPatches = []; - if (areExperimentalPatchesEnabled() == false) { - defaultPatches.addAll( - patches - .where( - (element) => - element.excluded == false && isPatchSupported(element), - ) - .map((p) => p.name), - ); - } else { - defaultPatches.addAll( - patches - .where((element) => isPatchSupported(element)) - .map((p) => p.name), - ); - } - return defaultPatches; - } - - Future<List<String>> getSelectedPatches(String app) async { - final Map<String, dynamic> patchesMap = await readSelectedPatchesFile(); - final List<String> defaultPatches = await getDefaultPatches(); - return List.from(patchesMap.putIfAbsent(app, () => defaultPatches)); - } - - Future<Map<String, dynamic>> readSelectedPatchesFile() async { - final File selectedPatchesFile = File(storedPatchesFile); - if (!selectedPatchesFile.existsSync()) { - return {}; - } - final String string = selectedPatchesFile.readAsStringSync(); - if (string.trim().isEmpty) { - return {}; - } - return jsonDecode(string); - } - - Future<void> resetLastSelectedPatches() async { - final File selectedPatchesFile = File(storedPatchesFile); - selectedPatchesFile.deleteSync(); - } -} diff --git a/lib/services/patcher_api.dart b/lib/services/patcher_api.dart deleted file mode 100644 index dfaaf3ec95..0000000000 --- a/lib/services/patcher_api.dart +++ /dev/null @@ -1,336 +0,0 @@ -import 'dart:io'; - -import 'package:collection/collection.dart'; -import 'package:cr_file_saver/file_saver.dart'; -import 'package:device_apps/device_apps.dart'; -import 'package:flutter/foundation.dart'; -import 'package:flutter/services.dart'; -import 'package:injectable/injectable.dart'; -import 'package:install_plugin/install_plugin.dart'; -import 'package:path_provider/path_provider.dart'; -import 'package:revanced_manager/app/app.locator.dart'; -import 'package:revanced_manager/models/patch.dart'; -import 'package:revanced_manager/models/patched_application.dart'; -import 'package:revanced_manager/services/manager_api.dart'; -import 'package:revanced_manager/services/root_api.dart'; -import 'package:share_extend/share_extend.dart'; - -@lazySingleton -class PatcherAPI { - static const patcherChannel = - MethodChannel('app.revanced.manager.flutter/patcher'); - final ManagerAPI _managerAPI = locator<ManagerAPI>(); - final RootAPI _rootAPI = RootAPI(); - late Directory _dataDir; - late Directory _tmpDir; - late File _keyStoreFile; - List<Patch> _patches = []; - Map filteredPatches = <String, List<Patch>>{}; - File? _outFile; - - Future<void> initialize() async { - await _loadPatches(); - await _managerAPI.downloadPatches(); - await _managerAPI.downloadIntegrations(); - final Directory appCache = await getTemporaryDirectory(); - _dataDir = await getExternalStorageDirectory() ?? appCache; - _tmpDir = Directory('${appCache.path}/patcher'); - _keyStoreFile = File('${_dataDir.path}/revanced-manager.keystore'); - cleanPatcher(); - } - - void cleanPatcher() { - if (_tmpDir.existsSync()) { - _tmpDir.deleteSync(recursive: true); - } - } - - Future<void> _loadPatches() async { - try { - if (_patches.isEmpty) { - _patches = await _managerAPI.getPatches(); - } - } on Exception catch (e) { - if (kDebugMode) { - print(e); - } - _patches = List.empty(); - } - } - - Future<List<ApplicationWithIcon>> getFilteredInstalledApps( - bool showUniversalPatches, - ) async { - final List<ApplicationWithIcon> filteredApps = []; - final bool allAppsIncluded = - _patches.any((patch) => patch.compatiblePackages.isEmpty) && - showUniversalPatches; - if (allAppsIncluded) { - final allPackages = await DeviceApps.getInstalledApplications( - includeAppIcons: true, - onlyAppsWithLaunchIntent: true, - ); - for (final pkg in allPackages) { - if (!filteredApps.any((app) => app.packageName == pkg.packageName)) { - final appInfo = await DeviceApps.getApp( - pkg.packageName, - true, - ) as ApplicationWithIcon?; - if (appInfo != null) { - filteredApps.add(appInfo); - } - } - } - } - for (final Patch patch in _patches) { - for (final Package package in patch.compatiblePackages) { - try { - if (!filteredApps.any((app) => app.packageName == package.name)) { - final ApplicationWithIcon? app = await DeviceApps.getApp( - package.name, - true, - ) as ApplicationWithIcon?; - if (app != null) { - filteredApps.add(app); - } - } - } on Exception catch (e) { - if (kDebugMode) { - print(e); - } - } - } - } - return filteredApps; - } - - List<Patch> getFilteredPatches(String packageName) { - final List<Patch> patches = _patches - .where( - (patch) => - patch.compatiblePackages.isEmpty || - !patch.name.contains('settings') && - patch.compatiblePackages - .any((pack) => pack.name == packageName), - ) - .toList(); - if (!_managerAPI.areUniversalPatchesEnabled()) { - filteredPatches[packageName] = patches - .where((patch) => patch.compatiblePackages.isNotEmpty) - .toList(); - } else { - filteredPatches[packageName] = patches; - } - return filteredPatches[packageName]; - } - - Future<List<Patch>> getAppliedPatches( - List<String> appliedPatches, - ) async { - return _patches - .where((patch) => appliedPatches.contains(patch.name)) - .toList(); - } - - Future<bool> needsResourcePatching( - List<Patch> selectedPatches, - ) async { - return selectedPatches.any( - (patch) => patch.dependencies.any( - (dep) => dep.contains('resource-'), - ), - ); - } - - Future<bool> needsSettingsPatch(List<Patch> selectedPatches) async { - return selectedPatches.any( - (patch) => patch.dependencies.any( - (dep) => dep.contains('settings'), - ), - ); - } - - Future<void> runPatcher( - String packageName, - String apkFilePath, - List<Patch> selectedPatches, - ) async { - final bool includeSettings = await needsSettingsPatch(selectedPatches); - if (includeSettings) { - try { - final Patch? settingsPatch = _patches.firstWhereOrNull( - (patch) => - patch.name.contains('settings') && - patch.compatiblePackages.any((pack) => pack.name == packageName), - ); - if (settingsPatch != null) { - selectedPatches.add(settingsPatch); - } - } on Exception catch (e) { - if (kDebugMode) { - print(e); - } - } - } - final File? patchBundleFile = await _managerAPI.downloadPatches(); - final File? integrationsFile = await _managerAPI.downloadIntegrations(); - if (patchBundleFile != null) { - _dataDir.createSync(); - _tmpDir.createSync(); - final Directory workDir = _tmpDir.createTempSync('tmp-'); - final File inputFile = File('${workDir.path}/base.apk'); - final File patchedFile = File('${workDir.path}/patched.apk'); - _outFile = File('${workDir.path}/out.apk'); - final Directory cacheDir = Directory('${workDir.path}/cache'); - cacheDir.createSync(); - final String originalFilePath = apkFilePath; - try { - await patcherChannel.invokeMethod( - 'runPatcher', - { - 'patchBundleFilePath': patchBundleFile.path, - 'originalFilePath': originalFilePath, - 'inputFilePath': inputFile.path, - 'patchedFilePath': patchedFile.path, - 'outFilePath': _outFile!.path, - 'integrationsPath': integrationsFile!.path, - 'selectedPatches': selectedPatches.map((p) => p.name).toList(), - 'cacheDirPath': cacheDir.path, - 'keyStoreFilePath': _keyStoreFile.path, - 'keystorePassword': _managerAPI.getKeystorePassword(), - }, - ); - } on Exception catch (e) { - if (kDebugMode) { - print(e); - } - } - } - } - - Future<void> stopPatcher() async { - try { - await patcherChannel.invokeMethod('stopPatcher'); - } on Exception catch (e) { - if (kDebugMode) { - print(e); - } - } - } - - Future<bool> installPatchedFile(PatchedApplication patchedApp) async { - if (_outFile != null) { - try { - if (patchedApp.isRooted) { - final bool hasRootPermissions = await _rootAPI.hasRootPermissions(); - if (hasRootPermissions) { - return _rootAPI.installApp( - patchedApp.packageName, - patchedApp.apkFilePath, - _outFile!.path, - ); - } - } else { - final install = await InstallPlugin.installApk(_outFile!.path); - return install['isSuccess']; - } - } on Exception catch (e) { - if (kDebugMode) { - print(e); - } - return false; - } - } - return false; - } - - void exportPatchedFile(String appName, String version) { - try { - if (_outFile != null) { - final String newName = _getFileName(appName, version); - CRFileSaver.saveFileWithDialog( - SaveFileDialogParams( - sourceFilePath: _outFile!.path, - destinationFileName: newName, - ), - ); - } - } on Exception catch (e) { - if (kDebugMode) { - print(e); - } - } - } - - void sharePatchedFile(String appName, String version) { - try { - if (_outFile != null) { - final String newName = _getFileName(appName, version); - final int lastSeparator = _outFile!.path.lastIndexOf('/'); - final String newPath = - _outFile!.path.substring(0, lastSeparator + 1) + newName; - final File shareFile = _outFile!.copySync(newPath); - ShareExtend.share(shareFile.path, 'file'); - } - } on Exception catch (e) { - if (kDebugMode) { - print(e); - } - } - } - - String _getFileName(String appName, String version) { - final String prefix = appName.toLowerCase().replaceAll(' ', '-'); - final String newName = '$prefix-revanced_v$version.apk'; - return newName; - } - - Future<void> exportPatcherLog(String logs) async { - final Directory appCache = await getTemporaryDirectory(); - final Directory logDir = Directory('${appCache.path}/logs'); - logDir.createSync(); - final String dateTime = DateTime.now() - .toIso8601String() - .replaceAll('-', '') - .replaceAll(':', '') - .replaceAll('T', '') - .replaceAll('.', ''); - final String fileName = 'revanced-manager_patcher_$dateTime.log'; - final File log = File('${logDir.path}/$fileName'); - log.writeAsStringSync(logs); - CRFileSaver.saveFileWithDialog( - SaveFileDialogParams( - sourceFilePath: log.path, - destinationFileName: fileName, - ), - ); - } - - String getSuggestedVersion(String packageName) { - final Map<String, int> versions = {}; - for (final Patch patch in _patches) { - final Package? package = patch.compatiblePackages.firstWhereOrNull( - (pack) => pack.name == packageName, - ); - if (package != null) { - for (final String version in package.versions) { - versions.update( - version, - (value) => versions[version]! + 1, - ifAbsent: () => 1, - ); - } - } - } - if (versions.isNotEmpty) { - final entries = versions.entries.toList() - ..sort((a, b) => a.value.compareTo(b.value)); - versions - ..clear() - ..addEntries(entries); - versions.removeWhere((key, value) => value != versions.values.last); - return (versions.keys.toList()..sort()).last; - } - return ''; - } -} diff --git a/lib/services/revanced_api.dart b/lib/services/revanced_api.dart deleted file mode 100644 index dde23cee5e..0000000000 --- a/lib/services/revanced_api.dart +++ /dev/null @@ -1,190 +0,0 @@ -import 'dart:async'; -import 'dart:io'; - -import 'package:collection/collection.dart'; -import 'package:dio/dio.dart'; -import 'package:dio_cache_interceptor/dio_cache_interceptor.dart'; -import 'package:flutter/foundation.dart'; -import 'package:flutter_cache_manager/flutter_cache_manager.dart'; -import 'package:injectable/injectable.dart'; -import 'package:timeago/timeago.dart'; - -@lazySingleton -class RevancedAPI { - late Dio _dio = Dio(); - - final _cacheOptions = CacheOptions( - store: MemCacheStore(), - maxStale: const Duration(days: 1), - priority: CachePriority.high, - ); - - Future<void> initialize(String apiUrl) async { - try { - _dio = Dio( - BaseOptions( - baseUrl: apiUrl, - ), - ); - - _dio.interceptors.add(DioCacheInterceptor(options: _cacheOptions)); - } on Exception catch (e) { - if (kDebugMode) { - print(e); - } - } - } - - Future<void> clearAllCache() async { - try { - await _cacheOptions.store!.clean(); - } on Exception catch (e) { - if (kDebugMode) { - print(e); - } - } - } - - Future<Map<String, List<dynamic>>> getContributors() async { - final Map<String, List<dynamic>> contributors = {}; - try { - final response = await _dio.get('/contributors'); - final List<dynamic> repositories = response.data['repositories']; - for (final Map<String, dynamic> repo in repositories) { - final String name = repo['name']; - contributors[name] = repo['contributors']; - } - } on Exception catch (e) { - if (kDebugMode) { - print(e); - } - return {}; - } - return contributors; - } - - Future<Map<String, dynamic>?> _getLatestRelease( - String extension, - String repoName, - ) async { - try { - final response = await _dio.get('/tools'); - final List<dynamic> tools = response.data['tools']; - return tools.firstWhereOrNull( - (t) => - t['repository'] == repoName && - (t['name'] as String).endsWith(extension), - ); - } on Exception catch (e) { - if (kDebugMode) { - print(e); - } - return null; - } - } - - Future<String?> getLatestReleaseVersion( - String extension, - String repoName, - ) async { - try { - final Map<String, dynamic>? release = await _getLatestRelease( - extension, - repoName, - ); - if (release != null) { - return release['version']; - } - } on Exception catch (e) { - if (kDebugMode) { - print(e); - } - return null; - } - return null; - } - - Future<File?> getLatestReleaseFile( - String extension, - String repoName, - ) async { - try { - final Map<String, dynamic>? release = await _getLatestRelease( - extension, - repoName, - ); - if (release != null) { - final String url = release['browser_download_url']; - return await DefaultCacheManager().getSingleFile(url); - } - } on Exception catch (e) { - if (kDebugMode) { - print(e); - } - return null; - } - return null; - } - - StreamController<double> managerUpdateProgress = - StreamController<double>.broadcast(); - - void updateManagerDownloadProgress(int progress) { - managerUpdateProgress.add(progress.toDouble()); - } - - Stream<double> getManagerUpdateProgress() { - return managerUpdateProgress.stream; - } - - void disposeManagerUpdateProgress() { - managerUpdateProgress.close(); - } - - Future<File?> downloadManager() async { - final Map<String, dynamic>? release = await _getLatestRelease( - '.apk', - 'revanced/revanced-manager', - ); - File? outputFile; - await for (final result in DefaultCacheManager().getFileStream( - release!['browser_download_url'] as String, - withProgress: true, - )) { - if (result is DownloadProgress) { - final totalSize = result.totalSize ?? 10000000; - final progress = (result.downloaded / totalSize * 100).round(); - - updateManagerDownloadProgress(progress); - } else if (result is FileInfo) { - disposeManagerUpdateProgress(); - // The download is complete; convert the FileInfo object to a File object - outputFile = File(result.file.path); - } - } - return outputFile; - } - - Future<String?> getLatestReleaseTime( - String extension, - String repoName, - ) async { - try { - final Map<String, dynamic>? release = await _getLatestRelease( - extension, - repoName, - ); - if (release != null) { - final DateTime timestamp = - DateTime.parse(release['timestamp'] as String); - return format(timestamp, locale: 'en_short'); - } - } on Exception catch (e) { - if (kDebugMode) { - print(e); - } - return null; - } - return null; - } -} diff --git a/lib/services/root_api.dart b/lib/services/root_api.dart deleted file mode 100644 index f0c7d9173f..0000000000 --- a/lib/services/root_api.dart +++ /dev/null @@ -1,257 +0,0 @@ -import 'package:flutter/foundation.dart'; -import 'package:root/root.dart'; - -class RootAPI { - // TODO(ponces): remove in the future, keep it for now during migration. - final String _revancedOldDirPath = '/data/local/tmp/revanced-manager'; - final String _revancedDirPath = '/data/adb/revanced'; - final String _postFsDataDirPath = '/data/adb/post-fs-data.d'; - final String _serviceDDirPath = '/data/adb/service.d'; - - Future<bool> isRooted() async { - try { - final bool? isRooted = await Root.isRootAvailable(); - return isRooted != null && isRooted; - } on Exception catch (e) { - if (kDebugMode) { - print(e); - } - return false; - } - } - - Future<bool> hasRootPermissions() async { - try { - bool? isRooted = await Root.isRootAvailable(); - if (isRooted != null && isRooted) { - isRooted = await Root.isRooted(); - return isRooted != null && isRooted; - } - return false; - } on Exception catch (e) { - if (kDebugMode) { - print(e); - } - return false; - } - } - - Future<void> setPermissions( - String permissions, - ownerGroup, - seLinux, - String filePath, - ) async { - try { - if (permissions.isNotEmpty) { - await Root.exec( - cmd: 'chmod $permissions "$filePath"', - ); - } - if (ownerGroup.isNotEmpty) { - await Root.exec( - cmd: 'chown $ownerGroup "$filePath"', - ); - } - if (seLinux.isNotEmpty) { - await Root.exec( - cmd: 'chcon $seLinux "$filePath"', - ); - } - } on Exception catch (e) { - if (kDebugMode) { - print(e); - } - } - } - - Future<bool> isAppInstalled(String packageName) async { - if (packageName.isNotEmpty) { - return fileExists('$_serviceDDirPath/$packageName.sh'); - } - return false; - } - - Future<List<String>> getInstalledApps() async { - final List<String> apps = List.empty(growable: true); - try { - String? res = await Root.exec( - cmd: 'ls "$_revancedDirPath"', - ); - if (res != null) { - final List<String> list = res.split('\n'); - list.removeWhere((pack) => pack.isEmpty); - apps.addAll(list.map((pack) => pack.trim()).toList()); - } - // TODO(ponces): remove in the future, keep it for now during migration. - res = await Root.exec( - cmd: 'ls "$_revancedOldDirPath"', - ); - if (res != null) { - final List<String> list = res.split('\n'); - list.removeWhere((pack) => pack.isEmpty); - apps.addAll(list.map((pack) => pack.trim()).toList()); - } - } on Exception catch (e) { - if (kDebugMode) { - print(e); - } - } - return apps; - } - - Future<void> deleteApp(String packageName, String originalFilePath) async { - await Root.exec( - cmd: 'am force-stop "$packageName"', - ); - await Root.exec( - cmd: 'su -mm -c "umount -l $originalFilePath"', - ); - // TODO(ponces): remove in the future, keep it for now during migration. - await Root.exec( - cmd: 'rm -rf "$_revancedOldDirPath/$packageName"', - ); - await Root.exec( - cmd: 'rm -rf "$_revancedDirPath/$packageName"', - ); - await Root.exec( - cmd: 'rm -rf "$_serviceDDirPath/$packageName.sh"', - ); - await Root.exec( - cmd: 'rm -rf "$_postFsDataDirPath/$packageName.sh"', - ); - } - - Future<bool> installApp( - String packageName, - String originalFilePath, - String patchedFilePath, - ) async { - try { - await deleteApp(packageName, originalFilePath); - await Root.exec( - cmd: 'mkdir -p "$_revancedDirPath/$packageName"', - ); - await setPermissions( - '0755', - 'shell:shell', - '', - '$_revancedDirPath/$packageName', - ); - await saveOriginalFilePath(packageName, originalFilePath); - await installServiceDScript(packageName); - await installPostFsDataScript(packageName); - await installApk(packageName, patchedFilePath); - await mountApk(packageName, originalFilePath); - return true; - } on Exception catch (e) { - if (kDebugMode) { - print(e); - } - return false; - } - } - - Future<void> installServiceDScript(String packageName) async { - await Root.exec( - cmd: 'mkdir -p "$_serviceDDirPath"', - ); - final String content = '#!/system/bin/sh\n' - 'while [ "\$(getprop sys.boot_completed | tr -d \'"\'"\'\\\\r\'"\'"\')" != "1" ]; do sleep 3; done\n' - 'base_path=$_revancedDirPath/$packageName/base.apk\n' - 'stock_path=\$(pm path $packageName | grep base | sed \'"\'"\'s/package://g\'"\'"\')\n' - r'[ ! -z $stock_path ] && mount -o bind $base_path $stock_path'; - final String scriptFilePath = '$_serviceDDirPath/$packageName.sh'; - await Root.exec( - cmd: 'echo \'$content\' > "$scriptFilePath"', - ); - await setPermissions('0744', '', '', scriptFilePath); - } - - Future<void> installPostFsDataScript(String packageName) async { - await Root.exec( - cmd: 'mkdir -p "$_postFsDataDirPath"', - ); - final String content = '#!/system/bin/sh\n' - 'stock_path=\$(pm path $packageName | grep base | sed \'"\'"\'s/package://g\'"\'"\')\n' - r'[ ! -z $stock_path ] && umount -l $stock_path'; - final String scriptFilePath = '$_postFsDataDirPath/$packageName.sh'; - await Root.exec( - cmd: 'echo \'$content\' > "$scriptFilePath"', - ); - await setPermissions('0744', '', '', scriptFilePath); - } - - Future<void> installApk(String packageName, String patchedFilePath) async { - final String newPatchedFilePath = '$_revancedDirPath/$packageName/base.apk'; - await Root.exec( - cmd: 'cp "$patchedFilePath" "$newPatchedFilePath"', - ); - await setPermissions( - '0644', - 'system:system', - 'u:object_r:apk_data_file:s0', - newPatchedFilePath, - ); - } - - Future<void> mountApk(String packageName, String originalFilePath) async { - final String newPatchedFilePath = '$_revancedDirPath/$packageName/base.apk'; - await Root.exec( - cmd: 'am force-stop "$packageName"', - ); - await Root.exec( - cmd: 'su -mm -c "umount -l $originalFilePath"', - ); - await Root.exec( - cmd: 'su -mm -c "mount -o bind $newPatchedFilePath $originalFilePath"', - ); - } - - Future<bool> isMounted(String packageName) async { - final String? res = await Root.exec( - cmd: 'cat /proc/mounts | grep $packageName', - ); - return res != null && res.isNotEmpty; - } - - Future<void> saveOriginalFilePath( - String packageName, - String originalFilePath, - ) async { - final String originalRootPath = - '$_revancedDirPath/$packageName/original.apk'; - await Root.exec( - cmd: 'mkdir -p "$_revancedDirPath/$packageName"', - ); - await setPermissions( - '0755', - 'shell:shell', - '', - '$_revancedDirPath/$packageName', - ); - await Root.exec( - cmd: 'cp "$originalFilePath" "$originalRootPath"', - ); - await setPermissions( - '0644', - 'shell:shell', - 'u:object_r:apk_data_file:s0', - originalFilePath, - ); - } - - Future<bool> fileExists(String path) async { - try { - final String? res = await Root.exec( - cmd: 'ls $path', - ); - return res != null && res.isNotEmpty; - } on Exception catch (e) { - if (kDebugMode) { - print(e); - } - return false; - } - } -} diff --git a/lib/services/third_party_services_modules.dart b/lib/services/third_party_services_modules.dart deleted file mode 100644 index 6f54f6fb6a..0000000000 --- a/lib/services/third_party_services_modules.dart +++ /dev/null @@ -1,12 +0,0 @@ -import 'package:injectable/injectable.dart'; -import 'package:stacked_services/stacked_services.dart'; - -@module -abstract class ThirdPartyServicesModule { - @lazySingleton - NavigationService get navigationService; - @lazySingleton - DialogService get dialogService; - @lazySingleton - SnackbarService get snackbarService; -} diff --git a/lib/services/toast.dart b/lib/services/toast.dart deleted file mode 100644 index cb9a62b705..0000000000 --- a/lib/services/toast.dart +++ /dev/null @@ -1,34 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_i18n/flutter_i18n.dart'; -import 'package:fluttertoast/fluttertoast.dart' as t; - -class Toast { - final t.FToast _fToast = t.FToast(); - late BuildContext buildContext; - - void initialize(BuildContext context) { - _fToast.init(context); - } - - void show(String text) { - t.Fluttertoast.showToast( - msg: FlutterI18n.translate( - _fToast.context!, - text, - ), - toastLength: t.Toast.LENGTH_LONG, - gravity: t.ToastGravity.CENTER, - ); - } - - void showBottom(String text) { - t.Fluttertoast.showToast( - msg: FlutterI18n.translate( - _fToast.context!, - text, - ), - toastLength: t.Toast.LENGTH_LONG, - gravity: t.ToastGravity.BOTTOM, - ); - } -} diff --git a/lib/theme.dart b/lib/theme.dart deleted file mode 100644 index 89c93d520b..0000000000 --- a/lib/theme.dart +++ /dev/null @@ -1,44 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:google_fonts/google_fonts.dart'; - -var lightCustomColorScheme = ColorScheme.fromSeed( - seedColor: Colors.blue, - primary: const Color(0xff1B73E8), -); - -var lightCustomTheme = ThemeData( - useMaterial3: true, - colorScheme: lightCustomColorScheme, - navigationBarTheme: NavigationBarThemeData( - labelTextStyle: MaterialStateProperty.all( - TextStyle( - color: lightCustomColorScheme.onSurface, - fontWeight: FontWeight.w500, - ), - ), - ), - textTheme: GoogleFonts.robotoTextTheme(ThemeData.light().textTheme), -); - -var darkCustomColorScheme = ColorScheme.fromSeed( - seedColor: Colors.blue, - brightness: Brightness.dark, - primary: const Color(0xffA5CAFF), - surface: const Color(0xff1B1A1D), -); - -var darkCustomTheme = ThemeData( - useMaterial3: true, - colorScheme: darkCustomColorScheme, - navigationBarTheme: NavigationBarThemeData( - labelTextStyle: MaterialStateProperty.all( - TextStyle( - color: darkCustomColorScheme.onSurface, - fontWeight: FontWeight.w500, - ), - ), - ), - canvasColor: const Color(0xff1B1A1D), - scaffoldBackgroundColor: const Color(0xff1B1A1D), - textTheme: GoogleFonts.robotoTextTheme(ThemeData.dark().textTheme), -); diff --git a/lib/ui/theme/dynamic_theme_builder.dart b/lib/ui/theme/dynamic_theme_builder.dart deleted file mode 100644 index 65d74c2ca4..0000000000 --- a/lib/ui/theme/dynamic_theme_builder.dart +++ /dev/null @@ -1,73 +0,0 @@ -import 'package:dynamic_color/dynamic_color.dart'; -import 'package:dynamic_themes/dynamic_themes.dart'; -import 'package:flutter/material.dart'; -import 'package:google_fonts/google_fonts.dart'; -import 'package:revanced_manager/app/app.router.dart'; -import 'package:revanced_manager/theme.dart'; -import 'package:stacked_services/stacked_services.dart'; - -class DynamicThemeBuilder extends StatelessWidget { - const DynamicThemeBuilder({ - Key? key, - required this.title, - required this.home, - required this.localizationsDelegates, - }) : super(key: key); - final String title; - final Widget home; - final Iterable<LocalizationsDelegate> localizationsDelegates; - - @override - Widget build(BuildContext context) { - return DynamicColorBuilder( - builder: (lightColorScheme, darkColorScheme) { - final ThemeData lightDynamicTheme = ThemeData( - useMaterial3: true, - navigationBarTheme: NavigationBarThemeData( - labelTextStyle: MaterialStateProperty.all( - GoogleFonts.roboto( - color: lightColorScheme?.onSurface, - fontWeight: FontWeight.w500, - ), - ), - ), - colorScheme: lightColorScheme?.harmonized(), - textTheme: GoogleFonts.robotoTextTheme(ThemeData.light().textTheme), - ); - final ThemeData darkDynamicTheme = ThemeData( - useMaterial3: true, - navigationBarTheme: NavigationBarThemeData( - labelTextStyle: MaterialStateProperty.all( - GoogleFonts.roboto( - color: darkColorScheme?.onSurface, - fontWeight: FontWeight.w500, - ), - ), - ), - colorScheme: darkColorScheme?.harmonized(), - textTheme: GoogleFonts.robotoTextTheme(ThemeData.dark().textTheme), - ); - return DynamicTheme( - themeCollection: ThemeCollection( - themes: { - 0: lightCustomTheme, - 1: darkCustomTheme, - 2: lightDynamicTheme, - 3: darkDynamicTheme, - }, - fallbackTheme: lightCustomTheme, - ), - builder: (context, theme) => MaterialApp( - debugShowCheckedModeBanner: false, - title: title, - navigatorKey: StackedService.navigatorKey, - onGenerateRoute: StackedRouter().onGenerateRoute, - theme: theme, - home: home, - localizationsDelegates: localizationsDelegates, - ), - ); - }, - ); - } -} diff --git a/lib/ui/views/app_selector/app_selector_view.dart b/lib/ui/views/app_selector/app_selector_view.dart deleted file mode 100644 index 7877a226fa..0000000000 --- a/lib/ui/views/app_selector/app_selector_view.dart +++ /dev/null @@ -1,146 +0,0 @@ -import 'package:flutter/material.dart' hide SearchBar; -import 'package:flutter_i18n/flutter_i18n.dart'; -import 'package:revanced_manager/ui/views/app_selector/app_selector_viewmodel.dart'; -import 'package:revanced_manager/ui/widgets/appSelectorView/app_skeleton_loader.dart'; -import 'package:revanced_manager/ui/widgets/appSelectorView/installed_app_item.dart'; -import 'package:revanced_manager/ui/widgets/appSelectorView/not_installed_app_item.dart'; -import 'package:revanced_manager/ui/widgets/shared/search_bar.dart'; -import 'package:stacked/stacked.dart' hide SkeletonLoader; - -class AppSelectorView extends StatefulWidget { - const AppSelectorView({Key? key}) : super(key: key); - - @override - State<AppSelectorView> createState() => _AppSelectorViewState(); -} - -class _AppSelectorViewState extends State<AppSelectorView> { - String _query = ''; - - @override - Widget build(BuildContext context) { - return ViewModelBuilder<AppSelectorViewModel>.reactive( - onViewModelReady: (model) => model.initialize(), - viewModelBuilder: () => AppSelectorViewModel(), - builder: (context, model, child) => Scaffold( - resizeToAvoidBottomInset: false, - floatingActionButton: FloatingActionButton.extended( - label: I18nText('appSelectorView.storageButton'), - icon: const Icon(Icons.sd_storage), - onPressed: () { - model.selectAppFromStorage(context); - Navigator.of(context).pop(); - }, - ), - body: CustomScrollView( - slivers: [ - SliverAppBar( - pinned: true, - floating: true, - title: I18nText( - 'appSelectorView.viewTitle', - child: Text( - '', - style: TextStyle( - color: Theme.of(context).textTheme.titleLarge!.color, - ), - ), - ), - leading: IconButton( - icon: Icon( - Icons.arrow_back, - color: Theme.of(context).textTheme.titleLarge!.color, - ), - onPressed: () => Navigator.of(context).pop(), - ), - bottom: PreferredSize( - preferredSize: const Size.fromHeight(64.0), - child: Padding( - padding: const EdgeInsets.symmetric( - vertical: 8.0, - horizontal: 12.0, - ), - child: SearchBar( - hintText: FlutterI18n.translate( - context, - 'appSelectorView.searchBarHint', - ), - onQueryChanged: (searchQuery) { - setState(() { - _query = searchQuery; - }); - }, - ), - ), - ), - ), - SliverToBoxAdapter( - child: model.noApps - ? Center( - child: I18nText( - 'appSelectorCard.noAppsLabel', - child: Text( - '', - style: TextStyle( - color: - Theme.of(context).textTheme.titleLarge!.color, - ), - ), - ), - ) - : model.allApps.isEmpty - ? const AppSkeletonLoader() - : Padding( - padding: const EdgeInsets.symmetric(horizontal: 12.0) - .copyWith( - bottom: - MediaQuery.viewPaddingOf(context).bottom + 8.0, - ), - child: Column( - children: [ - ...model - .getFilteredApps(_query) - .map( - (app) => InstalledAppItem( - name: app.appName, - pkgName: app.packageName, - icon: app.icon, - patchesCount: - model.patchesCount(app.packageName), - suggestedVersion: - model.getSuggestedVersion( - app.packageName, - ), - installedVersion: app.versionName!, - onTap: () => model.canSelectInstalled( - context, - app.packageName, - ), - ), - ) - .toList(), - ...model - .getFilteredAppsNames(_query) - .map( - (app) => NotInstalledAppItem( - name: app, - patchesCount: model.patchesCount(app), - suggestedVersion: - model.getSuggestedVersion(app), - onTap: () { - model.showDownloadToast(); - }, - ), - ) - .toList(), - const SizedBox(height: 70.0), - ], - ), - ), - ), - ], - ), - ), - ); - } -} diff --git a/lib/ui/views/app_selector/app_selector_viewmodel.dart b/lib/ui/views/app_selector/app_selector_viewmodel.dart deleted file mode 100644 index 9075b2a31e..0000000000 --- a/lib/ui/views/app_selector/app_selector_viewmodel.dart +++ /dev/null @@ -1,243 +0,0 @@ -import 'dart:io'; - -import 'package:device_apps/device_apps.dart'; -import 'package:file_picker/file_picker.dart'; -import 'package:flutter/foundation.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_i18n/flutter_i18n.dart'; -import 'package:revanced_manager/app/app.locator.dart'; -import 'package:revanced_manager/models/patch.dart'; -import 'package:revanced_manager/models/patched_application.dart'; -import 'package:revanced_manager/services/manager_api.dart'; -import 'package:revanced_manager/services/patcher_api.dart'; -import 'package:revanced_manager/services/toast.dart'; -import 'package:revanced_manager/ui/views/patcher/patcher_viewmodel.dart'; -import 'package:revanced_manager/ui/widgets/shared/custom_material_button.dart'; -import 'package:stacked/stacked.dart'; - -class AppSelectorViewModel extends BaseViewModel { - final PatcherAPI _patcherAPI = locator<PatcherAPI>(); - final ManagerAPI _managerAPI = locator<ManagerAPI>(); - final Toast _toast = locator<Toast>(); - final List<ApplicationWithIcon> apps = []; - List<String> allApps = []; - bool noApps = false; - bool isRooted = false; - int patchesCount(String packageName) { - return _patcherAPI.getFilteredPatches(packageName).length; - } - - List<Patch> patches = []; - - Future<void> initialize() async { - patches = await _managerAPI.getPatches(); - isRooted = _managerAPI.isRooted; - - apps.addAll( - await _patcherAPI - .getFilteredInstalledApps(_managerAPI.areUniversalPatchesEnabled()), - ); - apps.sort( - (a, b) => _patcherAPI - .getFilteredPatches(b.packageName) - .length - .compareTo(_patcherAPI.getFilteredPatches(a.packageName).length), - ); - getAllApps(); - notifyListeners(); - } - - List<String> getAllApps() { - allApps = patches - .expand((e) => e.compatiblePackages.map((p) => p.name)) - .toSet() - .where((name) => !apps.any((app) => app.packageName == name)) - .toList(); - noApps = allApps.isEmpty; - return allApps; - } - - String getSuggestedVersion(String packageName) { - return _patcherAPI.getSuggestedVersion(packageName); - } - - Future<bool> checkSplitApk(String packageName) async { - final app = await DeviceApps.getApp(packageName); - if (app != null) { - return app.isSplit; - } - return true; - } - - Future<void> selectApp(ApplicationWithIcon application) async { - locator<PatcherViewModel>().selectedApp = PatchedApplication( - name: application.appName, - packageName: application.packageName, - originalPackageName: application.packageName, - version: application.versionName!, - apkFilePath: application.apkFilePath, - icon: application.icon, - patchDate: DateTime.now(), - ); - locator<PatcherViewModel>().loadLastSelectedPatches(); - } - - Future<void> canSelectInstalled( - BuildContext context, - String packageName, - ) async { - final app = - await DeviceApps.getApp(packageName, true) as ApplicationWithIcon?; - if (app != null) { - if (await checkSplitApk(packageName) && !isRooted) { - return showSelectFromStorageDialog(context); - } else if (!await checkSplitApk(packageName) || isRooted) { - selectApp(app); - Navigator.pop(context); - } - } - } - - Future showSelectFromStorageDialog(BuildContext context) async { - return showDialog( - context: context, - builder: (context) => SimpleDialog( - alignment: Alignment.center, - contentPadding: - const EdgeInsets.symmetric(horizontal: 20, vertical: 20), - children: [ - const SizedBox(height: 10), - Icon( - Icons.block, - size: 28, - color: Theme.of(context).colorScheme.primary, - ), - const SizedBox(height: 20), - I18nText( - 'appSelectorView.featureNotAvailable', - child: const Text( - '', - textAlign: TextAlign.center, - style: TextStyle( - fontSize: 20, - fontWeight: FontWeight.w600, - wordSpacing: 1.5, - ), - ), - ), - const SizedBox(height: 20), - I18nText( - 'appSelectorView.featureNotAvailableText', - child: const Text( - '', - style: TextStyle( - fontSize: 14, - ), - ), - ), - const SizedBox(height: 30), - CustomMaterialButton( - onPressed: () => selectAppFromStorage(context).then( - (_) { - Navigator.pop(context); - Navigator.pop(context); - }, - ), - label: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - const Icon(Icons.sd_card), - const SizedBox(width: 10), - I18nText('appSelectorView.selectFromStorageButton'), - ], - ), - ), - const SizedBox(height: 10), - CustomMaterialButton( - isFilled: false, - onPressed: () { - Navigator.pop(context); - }, - label: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - const SizedBox(width: 10), - I18nText('cancelButton'), - ], - ), - ), - ], - ), - ); - } - - Future<void> selectAppFromStorage(BuildContext context) async { - try { - final FilePickerResult? result = await FilePicker.platform.pickFiles( - type: FileType.custom, - allowedExtensions: ['apk'], - ); - if (result != null && result.files.single.path != null) { - final File apkFile = File(result.files.single.path!); - final List<String> pathSplit = result.files.single.path!.split('/'); - pathSplit.removeLast(); - final Directory filePickerCacheDir = Directory(pathSplit.join('/')); - final Iterable<File> deletableFiles = - (await filePickerCacheDir.list().toList()).whereType<File>(); - for (final file in deletableFiles) { - if (file.path != apkFile.path && file.path.endsWith('.apk')) { - file.delete(); - } - } - final ApplicationWithIcon? application = - await DeviceApps.getAppFromStorage( - apkFile.path, - true, - ) as ApplicationWithIcon?; - if (application != null) { - locator<PatcherViewModel>().selectedApp = PatchedApplication( - name: application.appName, - packageName: application.packageName, - originalPackageName: application.packageName, - version: application.versionName!, - apkFilePath: result.files.single.path!, - icon: application.icon, - patchDate: DateTime.now(), - isFromStorage: true, - ); - locator<PatcherViewModel>().loadLastSelectedPatches(); - } - } - } on Exception catch (e) { - if (kDebugMode) { - print(e); - } - _toast.showBottom('appSelectorView.errorMessage'); - } - } - - List<ApplicationWithIcon> getFilteredApps(String query) { - return apps - .where( - (app) => - query.isEmpty || - query.length < 2 || - app.appName.toLowerCase().contains(query.toLowerCase()), - ) - .toList(); - } - - List<String> getFilteredAppsNames(String query) { - return allApps - .where( - (app) => - query.isEmpty || - query.length < 2 || - app.toLowerCase().contains(query.toLowerCase()), - ) - .toList(); - } - - void showDownloadToast() => - _toast.showBottom('appSelectorView.downloadToast'); -} diff --git a/lib/ui/views/contributors/contributors_view.dart b/lib/ui/views/contributors/contributors_view.dart deleted file mode 100644 index a409c17ecd..0000000000 --- a/lib/ui/views/contributors/contributors_view.dart +++ /dev/null @@ -1,70 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_i18n/flutter_i18n.dart'; -import 'package:google_fonts/google_fonts.dart'; -import 'package:revanced_manager/ui/views/contributors/contributors_viewmodel.dart'; -import 'package:revanced_manager/ui/widgets/contributorsView/contributors_card.dart'; -import 'package:revanced_manager/ui/widgets/shared/custom_sliver_app_bar.dart'; -import 'package:stacked/stacked.dart'; - -class ContributorsView extends StatelessWidget { - const ContributorsView({Key? key}) : super(key: key); - - @override - Widget build(BuildContext context) { - return ViewModelBuilder<ContributorsViewModel>.reactive( - viewModelBuilder: () => ContributorsViewModel(), - onViewModelReady: (model) => model.getContributors(), - builder: (context, model, child) => Scaffold( - body: CustomScrollView( - slivers: <Widget>[ - CustomSliverAppBar( - title: I18nText( - 'contributorsView.widgetTitle', - child: Text( - '', - style: GoogleFonts.inter( - color: Theme.of(context).textTheme.titleLarge!.color, - ), - ), - ), - ), - SliverPadding( - padding: const EdgeInsets.all(20.0), - sliver: SliverList( - delegate: SliverChildListDelegate.fixed( - <Widget>[ - ContributorsCard( - title: 'contributorsView.patcherContributors', - contributors: model.patcherContributors, - ), - const SizedBox(height: 20), - ContributorsCard( - title: 'contributorsView.patchesContributors', - contributors: model.patchesContributors, - ), - const SizedBox(height: 20), - ContributorsCard( - title: 'contributorsView.integrationsContributors', - contributors: model.integrationsContributors, - ), - const SizedBox(height: 20), - ContributorsCard( - title: 'contributorsView.cliContributors', - contributors: model.cliContributors, - ), - const SizedBox(height: 20), - ContributorsCard( - title: 'contributorsView.managerContributors', - contributors: model.managerContributors, - ), - SizedBox(height: MediaQuery.viewPaddingOf(context).bottom), - ], - ), - ), - ), - ], - ), - ), - ); - } -} diff --git a/lib/ui/views/contributors/contributors_viewmodel.dart b/lib/ui/views/contributors/contributors_viewmodel.dart deleted file mode 100644 index c8c7975fd8..0000000000 --- a/lib/ui/views/contributors/contributors_viewmodel.dart +++ /dev/null @@ -1,24 +0,0 @@ -import 'package:revanced_manager/app/app.locator.dart'; -import 'package:revanced_manager/services/manager_api.dart'; -import 'package:stacked/stacked.dart'; - -class ContributorsViewModel extends BaseViewModel { - final ManagerAPI _managerAPI = locator<ManagerAPI>(); - List<dynamic> patcherContributors = []; - List<dynamic> patchesContributors = []; - List<dynamic> integrationsContributors = []; - List<dynamic> cliContributors = []; - List<dynamic> managerContributors = []; - - Future<void> getContributors() async { - final Map<String, List<dynamic>> contributors = - await _managerAPI.getContributors(); - patcherContributors = contributors[_managerAPI.defaultPatcherRepo] ?? []; - patchesContributors = contributors[_managerAPI.getPatchesRepo()] ?? []; - integrationsContributors = - contributors[_managerAPI.getIntegrationsRepo()] ?? []; - cliContributors = contributors[_managerAPI.defaultCliRepo] ?? []; - managerContributors = contributors[_managerAPI.defaultManagerRepo] ?? []; - notifyListeners(); - } -} diff --git a/lib/ui/views/home/home_view.dart b/lib/ui/views/home/home_view.dart deleted file mode 100644 index 06ed6e9f28..0000000000 --- a/lib/ui/views/home/home_view.dart +++ /dev/null @@ -1,72 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_i18n/flutter_i18n.dart'; -import 'package:google_fonts/google_fonts.dart'; -import 'package:revanced_manager/app/app.locator.dart'; -import 'package:revanced_manager/ui/views/home/home_viewmodel.dart'; -import 'package:revanced_manager/ui/widgets/homeView/installed_apps_card.dart'; -import 'package:revanced_manager/ui/widgets/homeView/latest_commit_card.dart'; -import 'package:revanced_manager/ui/widgets/shared/custom_sliver_app_bar.dart'; -import 'package:stacked/stacked.dart'; - -class HomeView extends StatelessWidget { - const HomeView({Key? key}) : super(key: key); - - @override - Widget build(BuildContext context) { - return ViewModelBuilder<HomeViewModel>.reactive( - disposeViewModel: false, - fireOnViewModelReadyOnce: true, - onViewModelReady: (model) => model.initialize(context), - viewModelBuilder: () => locator<HomeViewModel>(), - builder: (context, model, child) => Scaffold( - body: RefreshIndicator( - onRefresh: () => model.forceRefresh(context), - child: CustomScrollView( - slivers: <Widget>[ - CustomSliverAppBar( - isMainView: true, - title: I18nText( - 'homeView.widgetTitle', - child: Text( - '', - style: GoogleFonts.inter( - color: Theme.of(context).textTheme.titleLarge!.color, - ), - ), - ), - ), - SliverPadding( - padding: const EdgeInsets.all(20.0), - sliver: SliverList( - delegate: SliverChildListDelegate.fixed( - <Widget>[ - I18nText( - 'homeView.updatesSubtitle', - child: Text( - '', - style: Theme.of(context).textTheme.titleLarge, - ), - ), - const SizedBox(height: 10), - LatestCommitCard(model: model, parentContext: context), - const SizedBox(height: 23), - I18nText( - 'homeView.patchedSubtitle', - child: Text( - '', - style: Theme.of(context).textTheme.titleLarge, - ), - ), - const SizedBox(height: 10), - InstalledAppsCard(), - ], - ), - ), - ), - ], - ), - ), - ), - ); - } -} diff --git a/lib/ui/views/home/home_viewmodel.dart b/lib/ui/views/home/home_viewmodel.dart deleted file mode 100644 index cfc99e66de..0000000000 --- a/lib/ui/views/home/home_viewmodel.dart +++ /dev/null @@ -1,480 +0,0 @@ -// ignore_for_file: use_build_context_synchronously -import 'dart:async'; -import 'dart:io'; -import 'package:cross_connectivity/cross_connectivity.dart'; -import 'package:flutter/foundation.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; -import 'package:flutter_i18n/flutter_i18n.dart'; -import 'package:flutter_local_notifications/flutter_local_notifications.dart'; -import 'package:injectable/injectable.dart'; -import 'package:install_plugin/install_plugin.dart'; -import 'package:path_provider/path_provider.dart'; -import 'package:revanced_manager/app/app.locator.dart'; -import 'package:revanced_manager/app/app.router.dart'; -import 'package:revanced_manager/models/patched_application.dart'; -import 'package:revanced_manager/services/github_api.dart'; -import 'package:revanced_manager/services/manager_api.dart'; -import 'package:revanced_manager/services/patcher_api.dart'; -import 'package:revanced_manager/services/revanced_api.dart'; -import 'package:revanced_manager/services/toast.dart'; -import 'package:revanced_manager/ui/views/navigation/navigation_viewmodel.dart'; -import 'package:revanced_manager/ui/views/patcher/patcher_viewmodel.dart'; -import 'package:revanced_manager/ui/widgets/homeView/update_confirmation_dialog.dart'; -import 'package:revanced_manager/ui/widgets/shared/custom_material_button.dart'; -import 'package:stacked/stacked.dart'; -import 'package:stacked_services/stacked_services.dart'; - -@lazySingleton -class HomeViewModel extends BaseViewModel { - final NavigationService _navigationService = locator<NavigationService>(); - final ManagerAPI _managerAPI = locator<ManagerAPI>(); - final PatcherAPI _patcherAPI = locator<PatcherAPI>(); - final GithubAPI _githubAPI = locator<GithubAPI>(); - final RevancedAPI _revancedAPI = locator<RevancedAPI>(); - final Toast _toast = locator<Toast>(); - final flutterLocalNotificationsPlugin = FlutterLocalNotificationsPlugin(); - DateTime? _lastUpdate; - bool showUpdatableApps = false; - List<PatchedApplication> patchedInstalledApps = []; - List<PatchedApplication> patchedUpdatableApps = []; - String? _latestManagerVersion = ''; - File? downloadedApk; - - Future<void> initialize(BuildContext context) async { - _latestManagerVersion = await _managerAPI.getLatestManagerVersion(); - if (!_managerAPI.getPatchesConsent()) { - await showPatchesConsent(context); - } - await _patcherAPI.initialize(); - await flutterLocalNotificationsPlugin.initialize( - const InitializationSettings( - android: AndroidInitializationSettings('ic_notification'), - ), - onDidReceiveNotificationResponse: (response) async { - if (response.id == 0) { - _toast.showBottom('homeView.installingMessage'); - final File? managerApk = await _managerAPI.downloadManager(); - if (managerApk != null) { - await InstallPlugin.installApk(managerApk.path); - } else { - _toast.showBottom('homeView.errorDownloadMessage'); - } - } - }, - ); - flutterLocalNotificationsPlugin - .resolvePlatformSpecificImplementation< - AndroidFlutterLocalNotificationsPlugin>() - ?.requestPermission(); - final bool isConnected = await Connectivity().checkConnection(); - if (!isConnected) { - _toast.showBottom('homeView.noConnection'); - } - final NotificationAppLaunchDetails? notificationAppLaunchDetails = - await flutterLocalNotificationsPlugin.getNotificationAppLaunchDetails(); - if (notificationAppLaunchDetails?.didNotificationLaunchApp ?? false) { - _toast.showBottom('homeView.installingMessage'); - final File? managerApk = await _managerAPI.downloadManager(); - if (managerApk != null) { - await InstallPlugin.installApk(managerApk.path); - } else { - _toast.showBottom('homeView.errorDownloadMessage'); - } - } - _getPatchedApps(); - _managerAPI.reAssessSavedApps().then((_) => _getPatchedApps()); - } - - void navigateToAppInfo(PatchedApplication app) { - _navigationService.navigateTo( - Routes.appInfoView, - arguments: AppInfoViewArguments(app: app), - ); - } - - void toggleUpdatableApps(bool value) { - showUpdatableApps = value; - notifyListeners(); - } - - Future<void> navigateToPatcher(PatchedApplication app) async { - locator<PatcherViewModel>().selectedApp = app; - locator<PatcherViewModel>().selectedPatches = - await _patcherAPI.getAppliedPatches(app.appliedPatches); - locator<PatcherViewModel>().notifyListeners(); - locator<NavigationViewModel>().setIndex(1); - } - - void _getPatchedApps() { - patchedInstalledApps = _managerAPI.getPatchedApps().toList(); - patchedUpdatableApps = _managerAPI - .getPatchedApps() - .where((app) => app.hasUpdates == true) - .toList(); - notifyListeners(); - } - - Future<bool> hasManagerUpdates() async { - String currentVersion = await _managerAPI.getCurrentManagerVersion(); - - // add v to current version - if (!currentVersion.startsWith('v')) { - currentVersion = 'v$currentVersion'; - } - - _latestManagerVersion = - await _managerAPI.getLatestManagerVersion() ?? currentVersion; - - if (_latestManagerVersion != currentVersion) { - return true; - } - return false; - } - - Future<bool> hasPatchesUpdates() async { - final String? latestVersion = await _managerAPI.getLatestPatchesVersion(); - final String currentVersion = await _managerAPI.getCurrentPatchesVersion(); - if (latestVersion != null) { - try { - final int latestVersionInt = - int.parse(latestVersion.replaceAll(RegExp('[^0-9]'), '')); - final int currentVersionInt = - int.parse(currentVersion.replaceAll(RegExp('[^0-9]'), '')); - return latestVersionInt > currentVersionInt; - } on Exception catch (e) { - if (kDebugMode) { - print(e); - } - return false; - } - } - return false; - } - - Future<File?> downloadManager() async { - try { - final response = await _revancedAPI.downloadManager(); - final bytes = await response!.readAsBytes(); - final tempDir = await getTemporaryDirectory(); - final tempFile = File('${tempDir.path}/revanced-manager.apk'); - await tempFile.writeAsBytes(bytes); - return tempFile; - } on Exception catch (e) { - if (kDebugMode) { - print(e); - } - return null; - } - } - - Future<void> showPatchesConsent(BuildContext context) async { - final ValueNotifier<bool> autoUpdate = ValueNotifier(true); - await showDialog( - context: context, - barrierDismissible: false, - builder: (context) => AlertDialog( - title: const Text('Download ReVanced Patches?'), - content: ValueListenableBuilder( - valueListenable: autoUpdate, - builder: (context, value, child) { - return Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - I18nText( - 'homeView.patchesConsentDialogText', - child: Text( - '', - style: TextStyle( - fontSize: 16, - fontWeight: FontWeight.w500, - color: Theme.of(context).colorScheme.secondary, - ), - ), - ), - Padding( - padding: const EdgeInsets.symmetric(vertical: 10), - child: I18nText( - 'homeView.patchesConsentDialogText2', - translationParams: { - 'url': _managerAPI.defaultApiUrl.split('/')[2], - }, - child: Text( - '', - style: TextStyle( - fontSize: 16, - fontWeight: FontWeight.w500, - color: Theme.of(context).colorScheme.error, - ), - ), - ), - ), - CheckboxListTile( - value: value, - contentPadding: EdgeInsets.zero, - title: I18nText( - 'homeView.patchesConsentDialogText3', - ), - subtitle: I18nText( - 'homeView.patchesConsentDialogText3Sub', - ), - onChanged: (selected) { - autoUpdate.value = selected!; - }, - ), - ], - ); - }, - ), - actions: [ - CustomMaterialButton( - isFilled: false, - onPressed: () async { - await _managerAPI.setPatchesConsent(false); - SystemNavigator.pop(); - }, - label: I18nText('quitButton'), - ), - CustomMaterialButton( - onPressed: () async { - await _managerAPI.setPatchesConsent(true); - await _managerAPI.setPatchesAutoUpdate(autoUpdate.value); - Navigator.of(context).pop(); - }, - label: I18nText('okButton'), - ), - ], - ), - ); - } - - Future<void> updatePatches(BuildContext context) async { - _toast.showBottom('homeView.downloadingMessage'); - final String patchesVersion = - await _managerAPI.getLatestPatchesVersion() ?? '0.0.0'; - final String integrationsVersion = - await _managerAPI.getLatestIntegrationsVersion() ?? '0.0.0'; - if (patchesVersion != '0.0.0' && integrationsVersion != '0.0.0') { - await _managerAPI.setCurrentPatchesVersion(patchesVersion); - await _managerAPI.setCurrentIntegrationsVersion(integrationsVersion); - _toast.showBottom('homeView.downloadedMessage'); - forceRefresh(context); - } else { - _toast.showBottom('homeView.errorDownloadMessage'); - } - } - - Future<void> updateManager(BuildContext context) async { - final ValueNotifier<bool> downloaded = ValueNotifier(false); - try { - _toast.showBottom('homeView.downloadingMessage'); - showDialog( - context: context, - builder: (context) => ValueListenableBuilder( - valueListenable: downloaded, - builder: (context, value, child) { - return SimpleDialog( - contentPadding: const EdgeInsets.all(16.0), - title: I18nText( - !value - ? 'homeView.downloadingMessage' - : 'homeView.downloadedMessage', - child: Text( - '', - style: TextStyle( - fontSize: 20, - fontWeight: FontWeight.w500, - color: Theme.of(context).colorScheme.secondary, - ), - ), - ), - children: [ - Column( - children: [ - Row( - children: [ - Icon( - Icons.new_releases_outlined, - color: Theme.of(context).colorScheme.secondary, - ), - const SizedBox(width: 8.0), - Text( - '$_latestManagerVersion', - style: TextStyle( - fontSize: 18, - fontWeight: FontWeight.w500, - color: Theme.of(context).colorScheme.secondary, - ), - ), - ], - ), - const SizedBox(height: 16.0), - if (!value) - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - StreamBuilder<double>( - initialData: 0.0, - stream: _revancedAPI.managerUpdateProgress.stream, - builder: (context, snapshot) { - return LinearProgressIndicator( - value: snapshot.data! * 0.01, - valueColor: AlwaysStoppedAnimation<Color>( - Theme.of(context).colorScheme.secondary, - ), - ); - }, - ), - const SizedBox(height: 16.0), - Align( - alignment: Alignment.centerRight, - child: CustomMaterialButton( - label: I18nText('cancelButton'), - onPressed: () { - _revancedAPI.disposeManagerUpdateProgress(); - Navigator.of(context).pop(); - }, - ), - ), - ], - ), - if (value) - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - I18nText( - 'homeView.installUpdate', - child: Text( - '', - style: TextStyle( - fontSize: 20, - fontWeight: FontWeight.w500, - color: Theme.of(context).colorScheme.secondary, - ), - ), - ), - const SizedBox(height: 16.0), - Row( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - Align( - alignment: Alignment.centerRight, - child: CustomMaterialButton( - isFilled: false, - label: I18nText('cancelButton'), - onPressed: () { - Navigator.of(context).pop(); - }, - ), - ), - const SizedBox(width: 8.0), - Align( - alignment: Alignment.centerRight, - child: CustomMaterialButton( - label: I18nText('updateButton'), - onPressed: () async { - await InstallPlugin.installApk( - downloadedApk!.path, - ); - }, - ), - ), - ], - ), - ], - ), - ], - ), - ], - ); - }, - ), - ); - final File? managerApk = await downloadManager(); - if (managerApk != null) { - downloaded.value = true; - downloadedApk = managerApk; - // await flutterLocalNotificationsPlugin.zonedSchedule( - // 0, - // FlutterI18n.translate( - // context, - // 'homeView.notificationTitle', - // ), - // FlutterI18n.translate( - // context, - // 'homeView.notificationText', - // ), - // tz.TZDateTime.now(tz.local).add(const Duration(seconds: 5)), - // const NotificationDetails( - // android: AndroidNotificationDetails( - // 'revanced_manager_channel', - // 'ReVanced Manager Channel', - // importance: Importance.max, - // priority: Priority.high, - // ticker: 'ticker', - // ), - // ), - // androidAllowWhileIdle: true, - // uiLocalNotificationDateInterpretation: - // UILocalNotificationDateInterpretation.absoluteTime, - // ); - _toast.showBottom('homeView.installingMessage'); - await InstallPlugin.installApk(managerApk.path); - } else { - _toast.showBottom('homeView.errorDownloadMessage'); - } - } on Exception catch (e) { - if (kDebugMode) { - print(e); - } - _toast.showBottom('homeView.errorInstallMessage'); - } - } - - void updatesAreDisabled() { - _toast.showBottom('homeView.updatesDisabled'); - } - - Future<void> showUpdateConfirmationDialog( - BuildContext parentContext, - bool isPatches, - ) { - return showModalBottomSheet( - context: parentContext, - isScrollControlled: true, - shape: const RoundedRectangleBorder( - borderRadius: BorderRadius.vertical(top: Radius.circular(24.0)), - ), - builder: (context) => UpdateConfirmationDialog( - isPatches: isPatches, - ), - ); - } - - Future<Map<String, dynamic>?> getLatestManagerRelease() { - return _githubAPI.getLatestManagerRelease(_managerAPI.defaultManagerRepo); - } - - Future<Map<String, dynamic>?> getLatestPatchesRelease() { - return _githubAPI.getLatestPatchesRelease(_managerAPI.defaultPatchesRepo); - } - - Future<String?> getLatestPatchesReleaseTime() { - return _managerAPI.getLatestPatchesReleaseTime(); - } - - Future<String?> getLatestManagerReleaseTime() { - return _managerAPI.getLatestManagerReleaseTime(); - } - - Future<void> forceRefresh(BuildContext context) async { - await Future.delayed(const Duration(seconds: 1)); - if (_lastUpdate == null || - _lastUpdate!.difference(DateTime.now()).inSeconds > 2) { - _managerAPI.clearAllData(); - } - _toast.showBottom('homeView.refreshSuccess'); - initialize(context); - } -} diff --git a/lib/ui/views/installer/installer_view.dart b/lib/ui/views/installer/installer_view.dart deleted file mode 100644 index 508c449d3e..0000000000 --- a/lib/ui/views/installer/installer_view.dart +++ /dev/null @@ -1,106 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_i18n/flutter_i18n.dart'; -import 'package:google_fonts/google_fonts.dart'; -import 'package:revanced_manager/ui/views/installer/installer_viewmodel.dart'; -import 'package:revanced_manager/ui/widgets/installerView/gradient_progress_indicator.dart'; -import 'package:revanced_manager/ui/widgets/shared/custom_card.dart'; -import 'package:revanced_manager/ui/widgets/shared/custom_sliver_app_bar.dart'; -import 'package:stacked/stacked.dart'; - -class InstallerView extends StatelessWidget { - const InstallerView({Key? key}) : super(key: key); - - @override - Widget build(BuildContext context) { - return ViewModelBuilder<InstallerViewModel>.reactive( - onViewModelReady: (model) => model.initialize(context), - viewModelBuilder: () => InstallerViewModel(), - builder: (context, model, child) => WillPopScope( - child: SafeArea( - top: false, - bottom: false, - child: Scaffold( - floatingActionButton: Visibility( - visible: !model.isPatching, - child: FloatingActionButton.extended( - label: I18nText('installerView.installButton'), - icon: const Icon(Icons.file_download_outlined), - onPressed: () => model.installTypeDialog(context), - elevation: 0, - ), - ), - floatingActionButtonLocation: - FloatingActionButtonLocation.endContained, - bottomNavigationBar: Visibility( - visible: !model.isPatching, - child: BottomAppBar( - child: Row( - children: <Widget>[ - Visibility( - visible: !model.hasErrors, - child: IconButton.filledTonal( - tooltip: FlutterI18n.translate( - context, - 'installerView.exportApkButtonTooltip', - ), - icon: const Icon(Icons.save), - onPressed: () => model.onButtonPressed(0), - ), - ), - IconButton.filledTonal( - tooltip: FlutterI18n.translate( - context, - 'installerView.exportLogButtonTooltip', - ), - icon: const Icon(Icons.post_add), - onPressed: () => model.onButtonPressed(1), - ), - ], - ), - ), - ), - body: CustomScrollView( - controller: model.scrollController, - slivers: <Widget>[ - CustomSliverAppBar( - title: Text( - model.headerLogs, - style: GoogleFonts.inter( - color: Theme.of(context).textTheme.titleLarge!.color, - ), - maxLines: 1, - overflow: TextOverflow.ellipsis, - ), - onBackButtonPressed: () => model.onWillPop(context), - bottom: PreferredSize( - preferredSize: const Size(double.infinity, 1.0), - child: GradientProgressIndicator(progress: model.progress), - ), - ), - SliverPadding( - padding: const EdgeInsets.all(20.0), - sliver: SliverList( - delegate: SliverChildListDelegate.fixed( - <Widget>[ - CustomCard( - child: Text( - model.logs, - style: GoogleFonts.jetBrainsMono( - fontSize: 13, - height: 1.5, - ), - ), - ), - ], - ), - ), - ), - ], - ), - ), - ), - onWillPop: () => model.onWillPop(context), - ), - ); - } -} diff --git a/lib/ui/views/installer/installer_viewmodel.dart b/lib/ui/views/installer/installer_viewmodel.dart deleted file mode 100644 index b11d47da63..0000000000 --- a/lib/ui/views/installer/installer_viewmodel.dart +++ /dev/null @@ -1,391 +0,0 @@ -// ignore_for_file: use_build_context_synchronously -import 'package:device_apps/device_apps.dart'; -import 'package:flutter/foundation.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; -import 'package:flutter_background/flutter_background.dart'; -import 'package:flutter_i18n/flutter_i18n.dart'; -import 'package:permission_handler/permission_handler.dart'; -import 'package:revanced_manager/app/app.locator.dart'; -import 'package:revanced_manager/models/patch.dart'; -import 'package:revanced_manager/models/patched_application.dart'; -import 'package:revanced_manager/services/manager_api.dart'; -import 'package:revanced_manager/services/patcher_api.dart'; -import 'package:revanced_manager/services/root_api.dart'; -import 'package:revanced_manager/services/toast.dart'; -import 'package:revanced_manager/ui/views/patcher/patcher_viewmodel.dart'; -import 'package:revanced_manager/ui/widgets/shared/custom_material_button.dart'; -import 'package:stacked/stacked.dart'; -import 'package:wakelock/wakelock.dart'; - -class InstallerViewModel extends BaseViewModel { - final ManagerAPI _managerAPI = locator<ManagerAPI>(); - final PatcherAPI _patcherAPI = locator<PatcherAPI>(); - final RootAPI _rootAPI = RootAPI(); - final Toast _toast = locator<Toast>(); - final PatchedApplication _app = locator<PatcherViewModel>().selectedApp!; - final List<Patch> _patches = locator<PatcherViewModel>().selectedPatches; - static const _installerChannel = MethodChannel( - 'app.revanced.manager.flutter/installer', - ); - final ScrollController scrollController = ScrollController(); - double? progress = 0.0; - String logs = ''; - String headerLogs = ''; - bool isRooted = false; - bool isPatching = true; - bool isInstalled = false; - bool hasErrors = false; - bool isCanceled = false; - bool cancel = false; - - Future<void> initialize(BuildContext context) async { - isRooted = await _rootAPI.isRooted(); - if (await Permission.ignoreBatteryOptimizations.isGranted) { - try { - FlutterBackground.initialize( - androidConfig: FlutterBackgroundAndroidConfig( - notificationTitle: FlutterI18n.translate( - context, - 'installerView.notificationTitle', - ), - notificationText: FlutterI18n.translate( - context, - 'installerView.notificationText', - ), - notificationIcon: const AndroidResource( - name: 'ic_notification', - ), - ), - ).then((value) => FlutterBackground.enableBackgroundExecution()); - } on Exception catch (e) { - if (kDebugMode) { - print(e); - } // ignore - } - } - await Wakelock.enable(); - await handlePlatformChannelMethods(); - await runPatcher(); - } - - Future<dynamic> handlePlatformChannelMethods() async { - _installerChannel.setMethodCallHandler((call) async { - switch (call.method) { - case 'update': - if (call.arguments != null) { - final Map<dynamic, dynamic> arguments = call.arguments; - final double progress = arguments['progress']; - final String header = arguments['header']; - final String log = arguments['log']; - update(progress, header, log); - } - break; - } - }); - } - - Future<void> update(double value, String header, String log) async { - if (value >= 0.0) { - progress = value; - } - if (value == 0.0) { - logs = ''; - isPatching = true; - isInstalled = false; - hasErrors = false; - } else if (value == 1.0) { - isPatching = false; - hasErrors = false; - await _managerAPI.savePatches( - _patcherAPI.getFilteredPatches(_app.packageName), - _app.packageName, - ); - await _managerAPI.setUsedPatches(_patches, _app.packageName); - } else if (value == -100.0) { - isPatching = false; - hasErrors = true; - } - if (header.isNotEmpty) { - headerLogs = header; - } - if (log.isNotEmpty && !log.startsWith('Merging L')) { - if (logs.isNotEmpty) { - logs += '\n'; - } - logs += log; - if (logs[logs.length - 1] == '\n') { - logs = logs.substring(0, logs.length - 1); - } - Future.delayed(const Duration(milliseconds: 500)).then((value) { - scrollController.animateTo( - scrollController.position.maxScrollExtent, - duration: const Duration(milliseconds: 200), - curve: Curves.fastOutSlowIn, - ); - }); - } - notifyListeners(); - } - - Future<void> runPatcher() async { - try { - update(0.0, 'Initializing...', 'Initializing installer'); - if (_patches.isNotEmpty) { - try { - update(0.1, '', 'Creating working directory'); - await _patcherAPI.runPatcher( - _app.packageName, - _app.apkFilePath, - _patches, - ); - } on Exception catch (e) { - update( - -100.0, - 'Aborted...', - 'An error occurred! Aborted\nError:\n$e', - ); - if (kDebugMode) { - print(e); - } - } - } else { - update(-100.0, 'Aborted...', 'No app or patches selected! Aborted'); - } - if (FlutterBackground.isBackgroundExecutionEnabled) { - try { - FlutterBackground.disableBackgroundExecution(); - } on Exception catch (e) { - if (kDebugMode) { - print(e); - } // ignore - } - } - await Wakelock.disable(); - } on Exception catch (e) { - if (kDebugMode) { - print(e); - } - } - } - - Future<void> installTypeDialog(BuildContext context) async { - final ValueNotifier<int> installType = ValueNotifier(0); - if (isRooted) { - await showDialog( - context: context, - barrierDismissible: false, - builder: (context) => AlertDialog( - title: I18nText( - 'installerView.installType', - ), - backgroundColor: Theme.of(context).colorScheme.secondaryContainer, - icon: const Icon(Icons.file_download_outlined), - contentPadding: const EdgeInsets.symmetric(vertical: 16), - content: ValueListenableBuilder( - valueListenable: installType, - builder: (context, value, child) { - return Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Padding( - padding: const EdgeInsets.symmetric( - horizontal: 20, - vertical: 10, - ), - child: I18nText( - 'installerView.installTypeDescription', - child: Text( - '', - style: TextStyle( - fontSize: 16, - fontWeight: FontWeight.w500, - color: Theme.of(context).colorScheme.secondary, - ), - ), - ), - ), - RadioListTile( - title: I18nText('installerView.installNonRootType'), - subtitle: I18nText('installerView.installRecommendedType'), - contentPadding: const EdgeInsets.symmetric(horizontal: 10), - value: 0, - groupValue: value, - onChanged: (selected) { - installType.value = selected!; - }, - ), - RadioListTile( - title: I18nText('installerView.installRootType'), - contentPadding: const EdgeInsets.symmetric(horizontal: 10), - value: 1, - groupValue: value, - onChanged: (selected) { - installType.value = selected!; - }, - ), - ], - ); - }, - ), - actions: [ - CustomMaterialButton( - label: I18nText('cancelButton'), - isFilled: false, - onPressed: () { - Navigator.of(context).pop(); - }, - ), - CustomMaterialButton( - label: I18nText('installerView.installButton'), - onPressed: () { - Navigator.of(context).pop(); - installResult(context, installType.value == 1); - }, - ), - ], - ), - ); - } else { - installResult(context, false); - } - } - - Future<void> stopPatcher() async { - try { - isCanceled = true; - update(0.5, 'Aborting...', 'Canceling patching process'); - await _patcherAPI.stopPatcher(); - update(-100.0, 'Aborted...', 'Press back to exit'); - } on Exception catch (e) { - if (kDebugMode) { - print(e); - } - } - } - - Future<void> installResult(BuildContext context, bool installAsRoot) async { - try { - _app.isRooted = installAsRoot; - final bool hasMicroG = - _patches.any((p) => p.name.endsWith('MicroG support')); - final bool rootMicroG = installAsRoot && hasMicroG; - final bool rootFromStorage = installAsRoot && _app.isFromStorage; - final bool ytWithoutRootMicroG = - !installAsRoot && !hasMicroG && _app.packageName.contains('youtube'); - if (rootMicroG || rootFromStorage || ytWithoutRootMicroG) { - return showDialog( - context: context, - builder: (context) => AlertDialog( - title: I18nText('installerView.installErrorDialogTitle'), - backgroundColor: Theme.of(context).colorScheme.secondaryContainer, - content: I18nText( - rootMicroG - ? 'installerView.installErrorDialogText1' - : rootFromStorage - ? 'installerView.installErrorDialogText3' - : 'installerView.installErrorDialogText2', - ), - actions: <Widget>[ - CustomMaterialButton( - label: I18nText('okButton'), - onPressed: () => Navigator.of(context).pop(), - ), - ], - ), - ); - } else { - update( - 1.0, - 'Installing...', - _app.isRooted - ? 'Installing patched file using root method' - : 'Installing patched file using nonroot method', - ); - isInstalled = await _patcherAPI.installPatchedFile(_app); - if (isInstalled) { - update(1.0, 'Installed!', 'Installed!'); - _app.isFromStorage = false; - _app.patchDate = DateTime.now(); - _app.appliedPatches = _patches.map((p) => p.name).toList(); - if (hasMicroG) { - _app.name += ' ReVanced'; - _app.packageName = _app.packageName.replaceFirst( - 'com.google.', - 'app.revanced.', - ); - } - await _managerAPI.savePatchedApp(_app); - } - } - } on Exception catch (e) { - if (kDebugMode) { - print(e); - } - } - } - - void exportResult() { - try { - _patcherAPI.exportPatchedFile(_app.name, _app.version); - } on Exception catch (e) { - if (kDebugMode) { - print(e); - } - } - } - - void exportLog() { - _patcherAPI.exportPatcherLog(logs); - } - - Future<void> cleanPatcher() async { - try { - _patcherAPI.cleanPatcher(); - locator<PatcherViewModel>().selectedApp = null; - locator<PatcherViewModel>().selectedPatches.clear(); - locator<PatcherViewModel>().notifyListeners(); - } on Exception catch (e) { - if (kDebugMode) { - print(e); - } - } - } - - void openApp() { - DeviceApps.openApp(_app.packageName); - } - - void onButtonPressed(int value) { - switch (value) { - case 0: - exportResult(); - break; - case 1: - exportLog(); - break; - } - } - - Future<bool> onWillPop(BuildContext context) async { - if (isPatching) { - if (!cancel) { - cancel = true; - _toast.showBottom('installerView.pressBackAgain'); - } else if (!isCanceled) { - await stopPatcher(); - } else { - _toast.showBottom('installerView.noExit'); - } - return false; - } - if (!cancel) { - cleanPatcher(); - } else { - _patcherAPI.cleanPatcher(); - } - Navigator.of(context).pop(); - return true; - } -} diff --git a/lib/ui/views/navigation/navigation_view.dart b/lib/ui/views/navigation/navigation_view.dart deleted file mode 100644 index 7089d06908..0000000000 --- a/lib/ui/views/navigation/navigation_view.dart +++ /dev/null @@ -1,82 +0,0 @@ -import 'package:animations/animations.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_i18n/flutter_i18n.dart'; -import 'package:revanced_manager/app/app.locator.dart'; -import 'package:revanced_manager/ui/views/navigation/navigation_viewmodel.dart'; -import 'package:stacked/stacked.dart'; - -class NavigationView extends StatelessWidget { - const NavigationView({Key? key}) : super(key: key); - - @override - Widget build(BuildContext context) { - return ViewModelBuilder<NavigationViewModel>.reactive( - onViewModelReady: (model) => model.initialize(context), - viewModelBuilder: () => locator<NavigationViewModel>(), - builder: (context, model, child) => WillPopScope( - onWillPop: () async { - if (model.currentIndex == 0) { - return true; - } else { - model.setIndex(0); - return false; - } - }, - child: Scaffold( - body: PageTransitionSwitcher( - duration: const Duration(milliseconds: 400), - transitionBuilder: ( - Widget child, - Animation<double> animation, - Animation<double> secondaryAnimation, - ) { - return FadeThroughTransition( - animation: animation, - secondaryAnimation: secondaryAnimation, - fillColor: Theme.of(context).colorScheme.surface, - child: child, - ); - }, - child: model.getViewForIndex(model.currentIndex), - ), - bottomNavigationBar: NavigationBar( - onDestinationSelected: model.setIndex, - selectedIndex: model.currentIndex, - destinations: <Widget>[ - NavigationDestination( - icon: model.isIndexSelected(0) - ? const Icon(Icons.dashboard) - : const Icon(Icons.dashboard_outlined), - label: FlutterI18n.translate( - context, - 'navigationView.dashboardTab', - ), - tooltip: '', - ), - NavigationDestination( - icon: model.isIndexSelected(1) - ? const Icon(Icons.build) - : const Icon(Icons.build_outlined), - label: FlutterI18n.translate( - context, - 'navigationView.patcherTab', - ), - tooltip: '', - ), - NavigationDestination( - icon: model.isIndexSelected(2) - ? const Icon(Icons.settings) - : const Icon(Icons.settings_outlined), - label: FlutterI18n.translate( - context, - 'navigationView.settingsTab', - ), - tooltip: '', - ), - ], - ), - ), - ), - ); - } -} diff --git a/lib/ui/views/navigation/navigation_viewmodel.dart b/lib/ui/views/navigation/navigation_viewmodel.dart deleted file mode 100644 index 99110cc188..0000000000 --- a/lib/ui/views/navigation/navigation_viewmodel.dart +++ /dev/null @@ -1,75 +0,0 @@ -// ignore_for_file: use_build_context_synchronously -import 'package:dynamic_themes/dynamic_themes.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; -import 'package:injectable/injectable.dart'; -import 'package:permission_handler/permission_handler.dart'; -import 'package:revanced_manager/app/app.locator.dart'; -import 'package:revanced_manager/services/root_api.dart'; -import 'package:revanced_manager/services/toast.dart'; -import 'package:revanced_manager/ui/views/home/home_view.dart'; -import 'package:revanced_manager/ui/views/patcher/patcher_view.dart'; -import 'package:revanced_manager/ui/views/settings/settings_view.dart'; -import 'package:shared_preferences/shared_preferences.dart'; -import 'package:stacked/stacked.dart'; - -@lazySingleton -class NavigationViewModel extends IndexTrackingViewModel { - Future<void> initialize(BuildContext context) async { - locator<Toast>().initialize(context); - final SharedPreferences prefs = await SharedPreferences.getInstance(); - requestManageExternalStorage(); - - if (prefs.getBool('permissionsRequested') == null) { - await Permission.storage.request(); - await Permission.manageExternalStorage.request(); - await prefs.setBool('permissionsRequested', true); - RootAPI().hasRootPermissions().then( - (value) => Permission.requestInstallPackages.request().then( - (value) => Permission.ignoreBatteryOptimizations.request(), - ), - ); - } - - if (prefs.getBool('useDarkTheme') == null) { - final bool isDark = - MediaQuery.platformBrightnessOf(context) != Brightness.light; - await prefs.setBool('useDarkTheme', isDark); - await DynamicTheme.of(context)!.setTheme(isDark ? 1 : 0); - } - SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge); - SystemChrome.setSystemUIOverlayStyle( - SystemUiOverlayStyle( - systemNavigationBarColor: Colors.transparent, - systemNavigationBarIconBrightness: - DynamicTheme.of(context)!.theme.brightness == Brightness.light - ? Brightness.dark - : Brightness.light, - ), - ); - } - - Future<void> requestManageExternalStorage() async { - final manageExternalStorageStatus = - await Permission.manageExternalStorage.status; - if (manageExternalStorageStatus.isDenied) { - await Permission.manageExternalStorage.request(); - } - if (manageExternalStorageStatus.isPermanentlyDenied) { - await openAppSettings(); - } - } - - Widget getViewForIndex(int index) { - switch (index) { - case 0: - return const HomeView(); - case 1: - return const PatcherView(); - case 2: - return const SettingsView(); - default: - return const HomeView(); - } - } -} diff --git a/lib/ui/views/patcher/patcher_view.dart b/lib/ui/views/patcher/patcher_view.dart deleted file mode 100644 index ec593d18ab..0000000000 --- a/lib/ui/views/patcher/patcher_view.dart +++ /dev/null @@ -1,68 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_i18n/flutter_i18n.dart'; -import 'package:google_fonts/google_fonts.dart'; -import 'package:revanced_manager/app/app.locator.dart'; -import 'package:revanced_manager/ui/views/patcher/patcher_viewmodel.dart'; -import 'package:revanced_manager/ui/widgets/patcherView/app_selector_card.dart'; -import 'package:revanced_manager/ui/widgets/patcherView/patch_selector_card.dart'; -import 'package:revanced_manager/ui/widgets/shared/custom_sliver_app_bar.dart'; -import 'package:stacked/stacked.dart'; - -class PatcherView extends StatelessWidget { - const PatcherView({Key? key}) : super(key: key); - - @override - Widget build(BuildContext context) { - return ViewModelBuilder<PatcherViewModel>.reactive( - disposeViewModel: false, - viewModelBuilder: () => locator<PatcherViewModel>(), - builder: (context, model, child) => Scaffold( - floatingActionButton: Visibility( - visible: model.showPatchButton(), - child: FloatingActionButton.extended( - label: I18nText('patcherView.patchButton'), - icon: const Icon(Icons.build), - onPressed: () => model.showRemovedPatchesDialog(context), - ), - ), - body: CustomScrollView( - slivers: <Widget>[ - CustomSliverAppBar( - isMainView: true, - title: I18nText( - 'patcherView.widgetTitle', - child: Text( - '', - style: GoogleFonts.inter( - color: Theme.of(context).textTheme.titleLarge!.color, - ), - ), - ), - ), - SliverPadding( - padding: const EdgeInsets.all(20.0), - sliver: SliverList( - delegate: SliverChildListDelegate.fixed( - <Widget>[ - AppSelectorCard( - onPressed: () => model.navigateToAppSelector(), - ), - const SizedBox(height: 16), - Opacity( - opacity: model.dimPatchesCard() ? 0.5 : 1, - child: PatchSelectorCard( - onPressed: model.dimPatchesCard() - ? () => {} - : () => model.navigateToPatchesSelector(), - ), - ), - ], - ), - ), - ), - ], - ), - ), - ); - } -} diff --git a/lib/ui/views/patcher/patcher_viewmodel.dart b/lib/ui/views/patcher/patcher_viewmodel.dart deleted file mode 100644 index 33ceb7195d..0000000000 --- a/lib/ui/views/patcher/patcher_viewmodel.dart +++ /dev/null @@ -1,214 +0,0 @@ -// ignore_for_file: use_build_context_synchronously - -import 'package:flutter/material.dart'; -import 'package:flutter_i18n/flutter_i18n.dart'; -import 'package:injectable/injectable.dart'; -import 'package:revanced_manager/app/app.locator.dart'; -import 'package:revanced_manager/app/app.router.dart'; -import 'package:revanced_manager/models/patch.dart'; -import 'package:revanced_manager/models/patched_application.dart'; -import 'package:revanced_manager/services/manager_api.dart'; -import 'package:revanced_manager/services/patcher_api.dart'; -import 'package:revanced_manager/ui/widgets/shared/custom_material_button.dart'; -import 'package:revanced_manager/utils/about_info.dart'; -import 'package:revanced_manager/utils/check_for_supported_patch.dart'; -import 'package:stacked/stacked.dart'; -import 'package:stacked_services/stacked_services.dart'; - -@lazySingleton -class PatcherViewModel extends BaseViewModel { - final NavigationService _navigationService = locator<NavigationService>(); - final ManagerAPI _managerAPI = locator<ManagerAPI>(); - final PatcherAPI _patcherAPI = locator<PatcherAPI>(); - PatchedApplication? selectedApp; - List<Patch> selectedPatches = []; - List<String> removedPatches = []; - - void navigateToAppSelector() { - _navigationService.navigateTo(Routes.appSelectorView); - } - - void navigateToPatchesSelector() { - _navigationService.navigateTo(Routes.patchesSelectorView); - } - - void navigateToInstaller() { - _navigationService.navigateTo(Routes.installerView); - } - - bool showPatchButton() { - return selectedPatches.isNotEmpty; - } - - bool dimPatchesCard() { - return selectedApp == null; - } - - Future<bool> isValidPatchConfig() async { - final bool needsResourcePatching = await _patcherAPI.needsResourcePatching( - selectedPatches, - ); - if (needsResourcePatching && selectedApp != null) { - final bool isSplit = await _managerAPI.isSplitApk(selectedApp!); - return !isSplit; - } - return true; - } - - Future<void> showPatchConfirmationDialog(BuildContext context) async { - final bool isValid = await isValidPatchConfig(); - if (context.mounted) { - if (isValid) { - showArmv7WarningDialog(context); - } else { - return showDialog( - context: context, - builder: (context) => AlertDialog( - title: I18nText('warning'), - backgroundColor: Theme.of(context).colorScheme.secondaryContainer, - content: I18nText('patcherView.splitApkWarningDialogText'), - actions: <Widget>[ - CustomMaterialButton( - label: I18nText('noButton'), - onPressed: () => Navigator.of(context).pop(), - ), - CustomMaterialButton( - label: I18nText('yesButton'), - isFilled: false, - onPressed: () { - Navigator.of(context).pop(); - showArmv7WarningDialog(context); - }, - ), - ], - ), - ); - } - } - } - - Future<void> showRemovedPatchesDialog(BuildContext context) async { - if (removedPatches.isNotEmpty) { - return showDialog( - context: context, - builder: (context) => AlertDialog( - title: I18nText('notice'), - backgroundColor: Theme.of(context).colorScheme.secondaryContainer, - content: I18nText( - 'patcherView.removedPatchesWarningDialogText', - translationParams: {'patches': removedPatches.join('\n')}, - ), - actions: <Widget>[ - CustomMaterialButton( - isFilled: false, - label: I18nText('noButton'), - onPressed: () => Navigator.of(context).pop(), - ), - CustomMaterialButton( - label: I18nText('yesButton'), - onPressed: () { - Navigator.of(context).pop(); - navigateToInstaller(); - }, - ), - ], - ), - ); - } else { - showArmv7WarningDialog(context); - } - } - - Future<void> showArmv7WarningDialog(BuildContext context) async { - final bool armv7 = await AboutInfo.getInfo().then((info) { - final List<String> archs = info['supportedArch']; - final supportedAbis = ['arm64-v8a', 'x86', 'x86_64']; - return !archs.any((arch) => supportedAbis.contains(arch)); - }); - if (context.mounted && armv7) { - return showDialog( - context: context, - builder: (context) => AlertDialog( - title: I18nText('warning'), - backgroundColor: Theme.of(context).colorScheme.secondaryContainer, - content: I18nText('patcherView.armv7WarningDialogText'), - actions: <Widget>[ - CustomMaterialButton( - label: I18nText('noButton'), - onPressed: () => Navigator.of(context).pop(), - ), - CustomMaterialButton( - label: I18nText('yesButton'), - isFilled: false, - onPressed: () { - Navigator.of(context).pop(); - navigateToInstaller(); - }, - ), - ], - ), - ); - } else { - navigateToInstaller(); - } - } - - String getAppSelectionString() { - String text = '${selectedApp!.name} (${selectedApp!.packageName})'; - if (text.length > 32) { - text = '${text.substring(0, 32)}...)'; - } - return text; - } - - String getSuggestedVersionString(BuildContext context) { - String suggestedVersion = - _patcherAPI.getSuggestedVersion(selectedApp!.packageName); - if (suggestedVersion.isEmpty) { - suggestedVersion = FlutterI18n.translate( - context, - 'appSelectorCard.allVersions', - ); - } else { - suggestedVersion = 'v$suggestedVersion'; - } - return '${FlutterI18n.translate( - context, - 'appSelectorCard.currentVersion', - )}: v${selectedApp!.version}\n${FlutterI18n.translate( - context, - 'appSelectorCard.suggestedVersion', - )}: $suggestedVersion'; - } - - Future<void> loadLastSelectedPatches() async { - this.selectedPatches.clear(); - removedPatches.clear(); - final List<String> selectedPatches = - await _managerAPI.getSelectedPatches(selectedApp!.originalPackageName); - final List<Patch> patches = - _patcherAPI.getFilteredPatches(selectedApp!.originalPackageName); - this - .selectedPatches - .addAll(patches.where((patch) => selectedPatches.contains(patch.name))); - if (!_managerAPI.isPatchesChangeEnabled()) { - this.selectedPatches.clear(); - this.selectedPatches.addAll(patches.where((patch) => !patch.excluded)); - } - if (!_managerAPI.areExperimentalPatchesEnabled()) { - this.selectedPatches.removeWhere((patch) => !isPatchSupported(patch)); - } - if (!_managerAPI.areUniversalPatchesEnabled()) { - this - .selectedPatches - .removeWhere((patch) => patch.compatiblePackages.isEmpty); - } - final usedPatches = _managerAPI.getUsedPatches(selectedApp!.originalPackageName); - for (final patch in usedPatches){ - if (!patches.any((p) => p.name == patch.name)){ - removedPatches.add('\u2022 ${patch.name}'); - } - } - notifyListeners(); - } -} diff --git a/lib/ui/views/patches_selector/patches_selector_view.dart b/lib/ui/views/patches_selector/patches_selector_view.dart deleted file mode 100644 index 9039c2cefb..0000000000 --- a/lib/ui/views/patches_selector/patches_selector_view.dart +++ /dev/null @@ -1,264 +0,0 @@ -import 'package:flutter/material.dart' hide SearchBar; -import 'package:flutter_i18n/flutter_i18n.dart'; -import 'package:revanced_manager/app/app.locator.dart'; -import 'package:revanced_manager/services/manager_api.dart'; -import 'package:revanced_manager/ui/views/patches_selector/patches_selector_viewmodel.dart'; -import 'package:revanced_manager/ui/widgets/patchesSelectorView/patch_item.dart'; -import 'package:revanced_manager/ui/widgets/shared/custom_popup_menu.dart'; -import 'package:revanced_manager/ui/widgets/shared/search_bar.dart'; -import 'package:revanced_manager/utils/check_for_supported_patch.dart'; -import 'package:stacked/stacked.dart'; - -class PatchesSelectorView extends StatefulWidget { - const PatchesSelectorView({Key? key}) : super(key: key); - - @override - State<PatchesSelectorView> createState() => _PatchesSelectorViewState(); -} - -class _PatchesSelectorViewState extends State<PatchesSelectorView> { - String _query = ''; - final _managerAPI = locator<ManagerAPI>(); - - @override - void initState() { - super.initState(); - WidgetsBinding.instance.addPostFrameCallback((_) async { - if (!_managerAPI.isPatchesChangeEnabled() && - _managerAPI.showPatchesChangeWarning()) { - _managerAPI.showPatchesChangeWarningDialog(context); - } - }); - } - - @override - Widget build(BuildContext context) { - return ViewModelBuilder<PatchesSelectorViewModel>.reactive( - onViewModelReady: (model) => model.initialize(), - viewModelBuilder: () => PatchesSelectorViewModel(), - builder: (context, model, child) => Scaffold( - resizeToAvoidBottomInset: false, - floatingActionButton: Visibility( - visible: model.patches.isNotEmpty, - child: FloatingActionButton.extended( - label: Row( - children: <Widget>[ - I18nText('patchesSelectorView.doneButton'), - Text(' (${model.selectedPatches.length})'), - ], - ), - icon: const Icon(Icons.check), - onPressed: () { - model.selectPatches(); - Navigator.of(context).pop(); - }, - ), - ), - body: CustomScrollView( - slivers: [ - SliverAppBar( - pinned: true, - floating: true, - title: I18nText( - 'patchesSelectorView.viewTitle', - child: Text( - '', - style: TextStyle( - color: Theme.of(context).textTheme.titleLarge!.color, - ), - ), - ), - leading: IconButton( - icon: Icon( - Icons.arrow_back, - color: Theme.of(context).textTheme.titleLarge!.color, - ), - onPressed: () => Navigator.of(context).pop(), - ), - actions: [ - FittedBox( - fit: BoxFit.scaleDown, - child: Container( - margin: const EdgeInsets.only(top: 12, bottom: 12), - padding: - const EdgeInsets.symmetric(horizontal: 6, vertical: 6), - decoration: BoxDecoration( - color: Theme.of(context) - .colorScheme - .tertiary - .withOpacity(0.5), - borderRadius: BorderRadius.circular(6), - ), - child: Text( - model.patchesVersion!, - style: TextStyle( - color: Theme.of(context).textTheme.titleLarge!.color, - ), - ), - ), - ), - CustomPopupMenu( - onSelected: (value) => - {model.onMenuSelection(value, context)}, - children: { - 0: I18nText( - 'patchesSelectorView.loadPatchesSelection', - child: const Text( - '', - style: TextStyle( - fontWeight: FontWeight.bold, - ), - ), - ), - }, - ), - ], - bottom: PreferredSize( - preferredSize: const Size.fromHeight(64.0), - child: Padding( - padding: const EdgeInsets.symmetric( - vertical: 8.0, - horizontal: 12.0, - ), - child: SearchBar( - hintText: FlutterI18n.translate( - context, - 'patchesSelectorView.searchBarHint', - ), - onQueryChanged: (searchQuery) { - setState(() { - _query = searchQuery; - }); - }, - ), - ), - ), - ), - SliverToBoxAdapter( - child: model.patches.isEmpty - ? Padding( - padding: const EdgeInsets.all(8.0), - child: Center( - child: I18nText( - 'patchesSelectorView.noPatchesFound', - child: Text( - '', - style: Theme.of(context).textTheme.bodyMedium, - ), - ), - ), - ) - : Padding( - padding: - const EdgeInsets.symmetric(horizontal: 12.0).copyWith( - bottom: MediaQuery.viewPaddingOf(context).bottom + 8.0, - ), - child: Column( - children: [ - Row( - children: [ - ActionChip( - label: I18nText('patchesSelectorView.default'), - tooltip: FlutterI18n.translate( - context, - 'patchesSelectorView.defaultTooltip', - ), - onPressed: () { - if (_managerAPI.isPatchesChangeEnabled()) { - model.selectDefaultPatches(); - } else { - model.showPatchesChangeDialog(context); - } - }, - ), - const SizedBox(width: 8), - ActionChip( - label: I18nText('patchesSelectorView.none'), - tooltip: FlutterI18n.translate( - context, - 'patchesSelectorView.noneTooltip', - ), - onPressed: () { - if (_managerAPI.isPatchesChangeEnabled()) { - model.clearPatches(); - } else { - model.showPatchesChangeDialog(context); - } - }, - ), - ], - ), - ...model.getQueriedPatches(_query).map( - (patch) { - if (patch.compatiblePackages.isNotEmpty) { - return PatchItem( - name: patch.name, - simpleName: patch.getSimpleName(), - description: patch.description, - packageVersion: model.getAppInfo().version, - supportedPackageVersions: - model.getSupportedVersions(patch), - isUnsupported: !isPatchSupported(patch), - isChangeEnabled: _managerAPI.isPatchesChangeEnabled(), - isNew: model.isPatchNew( - patch, - model.getAppInfo().packageName, - ), - isSelected: model.isSelected(patch), - onChanged: (value) => - model.selectPatch(patch, value, context), - ); - } else { - return Container(); - } - }, - ), - if (_managerAPI.areUniversalPatchesEnabled()) - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Padding( - padding: const EdgeInsets.symmetric( - vertical: 10.0, - ), - child: I18nText( - 'patchesSelectorView.universalPatches', - ), - ), - ...model.getQueriedPatches(_query).map((patch) { - if (patch.compatiblePackages.isEmpty) { - return PatchItem( - name: patch.name, - simpleName: patch.getSimpleName(), - description: patch.description, - packageVersion: - model.getAppInfo().version, - supportedPackageVersions: - model.getSupportedVersions(patch), - isUnsupported: !isPatchSupported(patch), - isChangeEnabled: _managerAPI.isPatchesChangeEnabled(), - isNew: false, - isSelected: model.isSelected(patch), - onChanged: (value) => model.selectPatch( - patch, - value, - context, - ), - ); - } else { - return Container(); - } - }), - ], - ), - const SizedBox(height: 70.0), - ], - ), - ), - ), - ], - ), - ), - ); - } -} diff --git a/lib/ui/views/patches_selector/patches_selector_viewmodel.dart b/lib/ui/views/patches_selector/patches_selector_viewmodel.dart deleted file mode 100644 index 71e4a16eb1..0000000000 --- a/lib/ui/views/patches_selector/patches_selector_viewmodel.dart +++ /dev/null @@ -1,216 +0,0 @@ -import 'package:collection/collection.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_i18n/widgets/I18nText.dart'; -import 'package:revanced_manager/app/app.locator.dart'; -import 'package:revanced_manager/models/patch.dart'; -import 'package:revanced_manager/models/patched_application.dart'; -import 'package:revanced_manager/services/manager_api.dart'; -import 'package:revanced_manager/services/patcher_api.dart'; -import 'package:revanced_manager/services/toast.dart'; -import 'package:revanced_manager/ui/views/patcher/patcher_viewmodel.dart'; -import 'package:revanced_manager/ui/widgets/shared/custom_material_button.dart'; -import 'package:revanced_manager/utils/check_for_supported_patch.dart'; -import 'package:stacked/stacked.dart'; - -class PatchesSelectorViewModel extends BaseViewModel { - final PatcherAPI _patcherAPI = locator<PatcherAPI>(); - final ManagerAPI _managerAPI = locator<ManagerAPI>(); - final List<Patch> patches = []; - final List<Patch> selectedPatches = - locator<PatcherViewModel>().selectedPatches; - PatchedApplication? selectedApp = locator<PatcherViewModel>().selectedApp; - String? patchesVersion = ''; - bool isDefaultPatchesRepo() { - return _managerAPI.getPatchesRepo() == 'revanced/revanced-patches'; - } - - Future<void> initialize() async { - getPatchesVersion().whenComplete(() => notifyListeners()); - patches.addAll( - _patcherAPI.getFilteredPatches( - selectedApp!.originalPackageName, - ), - ); - patches.sort((a, b) { - if (isPatchNew(a, selectedApp!.packageName) == - isPatchNew(b, selectedApp!.packageName)) { - return a.name.compareTo(b.name); - } else { - return isPatchNew(b, selectedApp!.packageName) ? 1 : -1; - } - }); - notifyListeners(); - } - - bool isSelected(Patch patch) { - return selectedPatches.any( - (element) => element.name == patch.name, - ); - } - - void selectPatch(Patch patch, bool isSelected, BuildContext context) { - if (_managerAPI.isPatchesChangeEnabled()) { - if (isSelected && !selectedPatches.contains(patch)) { - selectedPatches.add(patch); - } else { - selectedPatches.remove(patch); - } - notifyListeners(); - } else { - showPatchesChangeDialog(context); - } - } - - Future<void> showPatchesChangeDialog(BuildContext context) async { - return showDialog( - context: context, - builder: (context) => AlertDialog( - backgroundColor: Theme.of(context).colorScheme.secondaryContainer, - title: I18nText('warning'), - content: I18nText( - 'patchItem.patchesChangeWarningDialogText', - child: const Text( - '', - style: TextStyle( - fontSize: 16, - fontWeight: FontWeight.w500, - ), - ), - ), - actions: [ - CustomMaterialButton( - isFilled: false, - label: I18nText('okButton'), - onPressed: () => Navigator.of(context).pop(), - ), - CustomMaterialButton( - label: I18nText('patchItem.patchesChangeWarningDialogButton'), - onPressed: () { - Navigator.of(context) - ..pop() - ..pop(); - }, - ), - ], - ), - ); - } - - void selectDefaultPatches() { - selectedPatches.clear(); - if (locator<PatcherViewModel>().selectedApp?.originalPackageName != null) { - selectedPatches.addAll( - _patcherAPI - .getFilteredPatches( - locator<PatcherViewModel>().selectedApp!.originalPackageName, - ) - .where( - (element) => - !element.excluded && - (_managerAPI.areExperimentalPatchesEnabled() || - isPatchSupported(element)), - ), - ); - } - notifyListeners(); - } - - void clearPatches() { - selectedPatches.clear(); - notifyListeners(); - } - - void selectPatches() { - locator<PatcherViewModel>().selectedPatches = selectedPatches; - saveSelectedPatches(); - locator<PatcherViewModel>().notifyListeners(); - } - - Future<void> getPatchesVersion() async { - patchesVersion = await _managerAPI.getCurrentPatchesVersion(); - } - - List<Patch> getQueriedPatches(String query) { - final List<Patch> patch = patches - .where( - (patch) => - query.isEmpty || - query.length < 2 || - patch.name.toLowerCase().contains(query.toLowerCase()) || - patch.getSimpleName().toLowerCase().contains(query.toLowerCase()), - ) - .toList(); - if (_managerAPI.areUniversalPatchesEnabled()) { - return patch; - } else { - return patch - .where((patch) => patch.compatiblePackages.isNotEmpty) - .toList(); - } - } - - PatchedApplication getAppInfo() { - return locator<PatcherViewModel>().selectedApp!; - } - - bool isPatchNew(Patch patch, String packageName) { - final List<Patch> savedPatches = _managerAPI.getSavedPatches(packageName); - if (savedPatches.isEmpty) { - return false; - } else { - return !savedPatches - .any((p) => p.getSimpleName() == patch.getSimpleName()); - } - } - - List<String> getSupportedVersions(Patch patch) { - final PatchedApplication app = locator<PatcherViewModel>().selectedApp!; - final Package? package = patch.compatiblePackages.firstWhereOrNull( - (pack) => pack.name == app.packageName, - ); - if (package != null) { - return package.versions; - } else { - return List.empty(); - } - } - - void onMenuSelection(value, BuildContext context) { - switch (value) { - case 0: - loadSelectedPatches(context); - break; - } - } - - Future<void> saveSelectedPatches() async { - final List<String> selectedPatches = - this.selectedPatches.map((patch) => patch.name).toList(); - await _managerAPI.setSelectedPatches( - locator<PatcherViewModel>().selectedApp!.originalPackageName, - selectedPatches, - ); - } - - Future<void> loadSelectedPatches(BuildContext context) async { - if (_managerAPI.isPatchesChangeEnabled()) { - final List<String> selectedPatches = await _managerAPI.getSelectedPatches( - locator<PatcherViewModel>().selectedApp!.originalPackageName, - ); - if (selectedPatches.isNotEmpty) { - this.selectedPatches.clear(); - this.selectedPatches.addAll( - patches.where((patch) => selectedPatches.contains(patch.name)), - ); - if (!_managerAPI.areExperimentalPatchesEnabled()) { - this.selectedPatches.removeWhere((patch) => !isPatchSupported(patch)); - } - } else { - locator<Toast>().showBottom('patchesSelectorView.noSavedPatches'); - } - notifyListeners(); - } else { - showPatchesChangeDialog(context); - } - } -} diff --git a/lib/ui/views/settings/settingsFragment/settings_manage_api_url.dart b/lib/ui/views/settings/settingsFragment/settings_manage_api_url.dart deleted file mode 100644 index 964254beb5..0000000000 --- a/lib/ui/views/settings/settingsFragment/settings_manage_api_url.dart +++ /dev/null @@ -1,121 +0,0 @@ -// ignore_for_file: use_build_context_synchronously - -import 'package:flutter/material.dart'; -import 'package:flutter_i18n/flutter_i18n.dart'; -import 'package:revanced_manager/app/app.locator.dart'; -import 'package:revanced_manager/services/manager_api.dart'; -import 'package:revanced_manager/services/toast.dart'; -import 'package:revanced_manager/ui/widgets/settingsView/custom_text_field.dart'; -import 'package:revanced_manager/ui/widgets/settingsView/settings_tile_dialog.dart'; -import 'package:revanced_manager/ui/widgets/shared/custom_material_button.dart'; -import 'package:stacked/stacked.dart'; - -class SManageApiUrl extends BaseViewModel { - final ManagerAPI _managerAPI = locator<ManagerAPI>(); - final Toast _toast = locator<Toast>(); - - final TextEditingController _apiUrlController = TextEditingController(); - - Future<void> showApiUrlDialog(BuildContext context) async { - final String apiUrl = _managerAPI.getApiUrl(); - _apiUrlController.text = apiUrl.replaceAll('https://', ''); - return showDialog( - context: context, - builder: (context) => AlertDialog( - title: Row( - children: <Widget>[ - I18nText('settingsView.apiURLLabel'), - const Spacer(), - IconButton( - icon: const Icon(Icons.manage_history_outlined), - onPressed: () => showApiUrlResetDialog(context), - color: Theme.of(context).colorScheme.secondary, - ), - ], - ), - backgroundColor: Theme.of(context).colorScheme.secondaryContainer, - content: SingleChildScrollView( - child: Column( - children: <Widget>[ - CustomTextField( - leadingIcon: Icon( - Icons.api_outlined, - color: Theme.of(context).colorScheme.secondary, - ), - inputController: _apiUrlController, - label: I18nText('settingsView.selectApiURL'), - hint: apiUrl, - onChanged: (value) => notifyListeners(), - ), - ], - ), - ), - actions: <Widget>[ - CustomMaterialButton( - isFilled: false, - label: I18nText('cancelButton'), - onPressed: () { - _apiUrlController.clear(); - Navigator.of(context).pop(); - }, - ), - CustomMaterialButton( - label: I18nText('okButton'), - onPressed: () { - String apiUrl = _apiUrlController.text; - if (!apiUrl.startsWith('https')) { - apiUrl = 'https://$apiUrl'; - } - _managerAPI.setApiUrl(apiUrl); - Navigator.of(context).pop(); - }, - ), - ], - ), - ); - } - - Future<void> showApiUrlResetDialog(BuildContext context) async { - return showDialog( - context: context, - builder: (context) => AlertDialog( - title: I18nText('settingsView.sourcesResetDialogTitle'), - backgroundColor: Theme.of(context).colorScheme.secondaryContainer, - content: I18nText('settingsView.apiURLResetDialogText'), - actions: <Widget>[ - CustomMaterialButton( - isFilled: false, - label: I18nText('noButton'), - onPressed: () => Navigator.of(context).pop(), - ), - CustomMaterialButton( - label: I18nText('yesButton'), - onPressed: () { - _managerAPI.setApiUrl(''); - _toast.showBottom('settingsView.restartAppForChanges'); - Navigator.of(context) - ..pop() - ..pop(); - }, - ), - ], - ), - ); - } -} - -final sManageApiUrl = SManageApiUrl(); - -class SManageApiUrlUI extends StatelessWidget { - const SManageApiUrlUI({super.key}); - - @override - Widget build(BuildContext context) { - return SettingsTileDialog( - padding: const EdgeInsets.symmetric(horizontal: 20.0), - title: 'settingsView.apiURLLabel', - subtitle: 'settingsView.apiURLHint', - onTap: () => sManageApiUrl.showApiUrlDialog(context), - ); - } -} diff --git a/lib/ui/views/settings/settingsFragment/settings_manage_keystore_password.dart b/lib/ui/views/settings/settingsFragment/settings_manage_keystore_password.dart deleted file mode 100644 index 4ac4689bde..0000000000 --- a/lib/ui/views/settings/settingsFragment/settings_manage_keystore_password.dart +++ /dev/null @@ -1,86 +0,0 @@ -// ignore_for_file: use_build_context_synchronously - -import 'package:flutter/material.dart'; -import 'package:flutter_i18n/flutter_i18n.dart'; -import 'package:revanced_manager/app/app.locator.dart'; -import 'package:revanced_manager/services/manager_api.dart'; -import 'package:revanced_manager/ui/widgets/settingsView/custom_text_field.dart'; -import 'package:revanced_manager/ui/widgets/settingsView/settings_tile_dialog.dart'; -import 'package:revanced_manager/ui/widgets/shared/custom_material_button.dart'; -import 'package:stacked/stacked.dart'; - -class SManageKeystorePassword extends BaseViewModel { - final ManagerAPI _managerAPI = locator<ManagerAPI>(); - - final TextEditingController _keystorePasswordController = - TextEditingController(); - - Future<void> showKeystoreDialog(BuildContext context) async { - final String keystorePasswordText = _managerAPI.getKeystorePassword(); - _keystorePasswordController.text = keystorePasswordText; - return showDialog( - context: context, - builder: (context) => AlertDialog( - title: Row( - children: <Widget>[ - I18nText('settingsView.selectKeystorePassword'), - const Spacer(), - IconButton( - icon: const Icon(Icons.manage_history_outlined), - onPressed: () => _keystorePasswordController.text = - _managerAPI.defaultKeystorePassword, - color: Theme.of(context).colorScheme.secondary, - ), - ], - ), - backgroundColor: Theme.of(context).colorScheme.secondaryContainer, - content: SingleChildScrollView( - child: Column( - children: <Widget>[ - CustomTextField( - inputController: _keystorePasswordController, - label: I18nText('settingsView.selectKeystorePassword'), - hint: '', - onChanged: (value) => notifyListeners(), - ), - ], - ), - ), - actions: <Widget>[ - CustomMaterialButton( - isFilled: false, - label: I18nText('cancelButton'), - onPressed: () { - _keystorePasswordController.clear(); - Navigator.of(context).pop(); - }, - ), - CustomMaterialButton( - label: I18nText('okButton'), - onPressed: () { - final String passwd = _keystorePasswordController.text; - _managerAPI.setKeystorePassword(passwd); - Navigator.of(context).pop(); - }, - ), - ], - ), - ); - } -} - -final sManageKeystorePassword = SManageKeystorePassword(); - -class SManageKeystorePasswordUI extends StatelessWidget { - const SManageKeystorePasswordUI({super.key}); - - @override - Widget build(BuildContext context) { - return SettingsTileDialog( - padding: const EdgeInsets.symmetric(horizontal: 20.0), - title: 'settingsView.selectKeystorePassword', - subtitle: 'settingsView.selectKeystorePasswordHint', - onTap: () => sManageKeystorePassword.showKeystoreDialog(context), - ); - } -} diff --git a/lib/ui/views/settings/settingsFragment/settings_manage_sources.dart b/lib/ui/views/settings/settingsFragment/settings_manage_sources.dart deleted file mode 100644 index 76e3171b1f..0000000000 --- a/lib/ui/views/settings/settingsFragment/settings_manage_sources.dart +++ /dev/null @@ -1,189 +0,0 @@ -// ignore_for_file: use_build_context_synchronously - -import 'package:flutter/material.dart'; -import 'package:flutter_i18n/flutter_i18n.dart'; -import 'package:revanced_manager/app/app.locator.dart'; -import 'package:revanced_manager/services/manager_api.dart'; -import 'package:revanced_manager/services/toast.dart'; -import 'package:revanced_manager/ui/widgets/settingsView/custom_text_field.dart'; -import 'package:revanced_manager/ui/widgets/settingsView/settings_tile_dialog.dart'; -import 'package:revanced_manager/ui/widgets/shared/custom_material_button.dart'; -import 'package:stacked/stacked.dart'; - -class SManageSources extends BaseViewModel { - final ManagerAPI _managerAPI = locator<ManagerAPI>(); - final Toast _toast = locator<Toast>(); - - final TextEditingController _hostSourceController = TextEditingController(); - final TextEditingController _orgPatSourceController = TextEditingController(); - final TextEditingController _patSourceController = TextEditingController(); - final TextEditingController _orgIntSourceController = TextEditingController(); - final TextEditingController _intSourceController = TextEditingController(); - - Future<void> showSourcesDialog(BuildContext context) async { - final String hostRepository = _managerAPI.getRepoUrl(); - final String patchesRepo = _managerAPI.getPatchesRepo(); - final String integrationsRepo = _managerAPI.getIntegrationsRepo(); - _hostSourceController.text = hostRepository; - _orgPatSourceController.text = patchesRepo.split('/')[0]; - _patSourceController.text = patchesRepo.split('/')[1]; - _orgIntSourceController.text = integrationsRepo.split('/')[0]; - _intSourceController.text = integrationsRepo.split('/')[1]; - return showDialog( - context: context, - builder: (context) => AlertDialog( - title: Row( - children: <Widget>[ - I18nText('settingsView.sourcesLabel'), - const Spacer(), - IconButton( - icon: const Icon(Icons.manage_history_outlined), - onPressed: () => showResetConfirmationDialog(context), - color: Theme.of(context).colorScheme.secondary, - ), - ], - ), - backgroundColor: Theme.of(context).colorScheme.secondaryContainer, - content: SingleChildScrollView( - child: Column( - children: <Widget>[ - CustomTextField( - leadingIcon: const Icon( - Icons.extension_outlined, - color: Colors.transparent, - ), - inputController: _hostSourceController, - label: I18nText('settingsView.hostRepositoryLabel'), - hint: hostRepository, - onChanged: (value) => notifyListeners(), - ), - const SizedBox(height: 20), - CustomTextField( - leadingIcon: Icon( - Icons.extension_outlined, - color: Theme.of(context).colorScheme.secondary, - ), - inputController: _orgPatSourceController, - label: I18nText('settingsView.orgPatchesLabel'), - hint: patchesRepo.split('/')[0], - onChanged: (value) => notifyListeners(), - ), - const SizedBox(height: 8), - CustomTextField( - leadingIcon: const Icon( - Icons.extension_outlined, - color: Colors.transparent, - ), - inputController: _patSourceController, - label: I18nText('settingsView.sourcesPatchesLabel'), - hint: patchesRepo.split('/')[1], - onChanged: (value) => notifyListeners(), - ), - const SizedBox(height: 20), - CustomTextField( - leadingIcon: Icon( - Icons.merge_outlined, - color: Theme.of(context).colorScheme.secondary, - ), - inputController: _orgIntSourceController, - label: I18nText('settingsView.orgIntegrationsLabel'), - hint: integrationsRepo.split('/')[0], - onChanged: (value) => notifyListeners(), - ), - const SizedBox(height: 8), - CustomTextField( - leadingIcon: const Icon( - Icons.merge_outlined, - color: Colors.transparent, - ), - inputController: _intSourceController, - label: I18nText('settingsView.sourcesIntegrationsLabel'), - hint: integrationsRepo.split('/')[1], - onChanged: (value) => notifyListeners(), - ), - const SizedBox(height: 20), - I18nText('settingsView.sourcesUpdateNote'), - ], - ), - ), - actions: <Widget>[ - CustomMaterialButton( - isFilled: false, - label: I18nText('cancelButton'), - onPressed: () { - _orgPatSourceController.clear(); - _patSourceController.clear(); - _orgIntSourceController.clear(); - _intSourceController.clear(); - Navigator.of(context).pop(); - }, - ), - CustomMaterialButton( - label: I18nText('okButton'), - onPressed: () { - _managerAPI.setRepoUrl(_hostSourceController.text.trim()); - _managerAPI.setPatchesRepo( - '${_orgPatSourceController.text.trim()}/${_patSourceController.text.trim()}', - ); - _managerAPI.setIntegrationsRepo( - '${_orgIntSourceController.text.trim()}/${_intSourceController.text.trim()}', - ); - _managerAPI.setCurrentPatchesVersion('0.0.0'); - _managerAPI.setCurrentIntegrationsVersion('0.0.0'); - _toast.showBottom('settingsView.restartAppForChanges'); - Navigator.of(context).pop(); - }, - ), - ], - ), - ); - } - - Future<void> showResetConfirmationDialog(BuildContext context) async { - return showDialog( - context: context, - builder: (context) => AlertDialog( - title: I18nText('settingsView.sourcesResetDialogTitle'), - backgroundColor: Theme.of(context).colorScheme.secondaryContainer, - content: I18nText('settingsView.sourcesResetDialogText'), - actions: <Widget>[ - CustomMaterialButton( - isFilled: false, - label: I18nText('noButton'), - onPressed: () => Navigator.of(context).pop(), - ), - CustomMaterialButton( - label: I18nText('yesButton'), - onPressed: () { - _managerAPI.setRepoUrl(''); - _managerAPI.setPatchesRepo(''); - _managerAPI.setIntegrationsRepo(''); - _managerAPI.setCurrentPatchesVersion('0.0.0'); - _managerAPI.setCurrentIntegrationsVersion('0.0.0'); - _toast.showBottom('settingsView.restartAppForChanges'); - Navigator.of(context) - ..pop() - ..pop(); - }, - ), - ], - ), - ); - } -} - -final sManageSources = SManageSources(); - -class SManageSourcesUI extends StatelessWidget { - const SManageSourcesUI({super.key}); - - @override - Widget build(BuildContext context) { - return SettingsTileDialog( - padding: const EdgeInsets.symmetric(horizontal: 20.0), - title: 'settingsView.sourcesLabel', - subtitle: 'settingsView.sourcesLabelHint', - onTap: () => sManageSources.showSourcesDialog(context), - ); - } -} diff --git a/lib/ui/views/settings/settingsFragment/settings_update_language.dart b/lib/ui/views/settings/settingsFragment/settings_update_language.dart deleted file mode 100644 index 66bb2c3e7f..0000000000 --- a/lib/ui/views/settings/settingsFragment/settings_update_language.dart +++ /dev/null @@ -1,95 +0,0 @@ -// ignore_for_file: use_build_context_synchronously - -import 'package:flutter/material.dart'; -import 'package:flutter_i18n/flutter_i18n.dart'; -import 'package:revanced_manager/app/app.locator.dart'; -import 'package:revanced_manager/main.dart'; -import 'package:revanced_manager/services/toast.dart'; -import 'package:revanced_manager/ui/views/navigation/navigation_viewmodel.dart'; -import 'package:revanced_manager/ui/views/settings/settings_viewmodel.dart'; -import 'package:revanced_manager/ui/widgets/settingsView/settings_tile_dialog.dart'; -import 'package:shared_preferences/shared_preferences.dart'; -import 'package:stacked/stacked.dart'; -import 'package:timeago/timeago.dart' as timeago; - -final _settingViewModel = SettingsViewModel(); - -class SUpdateLanguage extends BaseViewModel { - final Toast _toast = locator<Toast>(); - late SharedPreferences _prefs; - String selectedLanguage = 'English'; - String selectedLanguageLocale = prefs.getString('language') ?? 'en_US'; - List languages = []; - - Future<void> initialize() async { - _prefs = await SharedPreferences.getInstance(); - selectedLanguageLocale = - _prefs.getString('language') ?? selectedLanguageLocale; - notifyListeners(); - } - - Future<void> updateLanguage(BuildContext context, String? value) async { - if (value != null) { - selectedLanguageLocale = value; - _prefs = await SharedPreferences.getInstance(); - await _prefs.setString('language', value); - await FlutterI18n.refresh(context, Locale(value)); - timeago.setLocaleMessages(value, timeago.EnMessages()); - locator<NavigationViewModel>().notifyListeners(); - notifyListeners(); - } - } - - Future<void> initLang() async { - languages.sort((a, b) => a['name'].compareTo(b['name'])); - notifyListeners(); - } - - Future<void> showLanguagesDialog(BuildContext parentContext) { - initLang(); - return showDialog( - context: parentContext, - builder: (context) => SimpleDialog( - title: I18nText('settingsView.languageLabel'), - backgroundColor: Theme.of(context).colorScheme.secondaryContainer, - children: [ - SizedBox( - height: 500, - child: ListView.builder( - itemCount: languages.length, - itemBuilder: (context, index) { - return RadioListTile<String>( - title: Text(languages[index]['name']), - subtitle: Text(languages[index]['locale']), - value: languages[index]['locale'], - groupValue: selectedLanguageLocale, - onChanged: (value) { - selectedLanguage = languages[index]['name']; - _toast.showBottom('settingsView.restartAppForChanges'); - updateLanguage(context, value); - Navigator.pop(context); - }, - ); - }, - ), - ), - ], - ), - ); - } -} - -class SUpdateLanguageUI extends StatelessWidget { - const SUpdateLanguageUI({super.key}); - - @override - Widget build(BuildContext context) { - return SettingsTileDialog( - padding: const EdgeInsets.symmetric(horizontal: 20.0), - title: 'settingsView.languageLabel', - subtitle: _settingViewModel.sUpdateLanguage.selectedLanguage, - onTap: () => - _settingViewModel.sUpdateLanguage.showLanguagesDialog(context), - ); - } -} diff --git a/lib/ui/views/settings/settingsFragment/settings_update_theme.dart b/lib/ui/views/settings/settingsFragment/settings_update_theme.dart deleted file mode 100644 index 684abc961d..0000000000 --- a/lib/ui/views/settings/settingsFragment/settings_update_theme.dart +++ /dev/null @@ -1,116 +0,0 @@ -// ignore_for_file: use_build_context_synchronously - -import 'package:dynamic_themes/dynamic_themes.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; -import 'package:flutter_i18n/widgets/I18nText.dart'; -import 'package:revanced_manager/app/app.locator.dart'; -import 'package:revanced_manager/services/manager_api.dart'; -import 'package:revanced_manager/ui/views/settings/settings_viewmodel.dart'; -import 'package:revanced_manager/ui/widgets/settingsView/settings_section.dart'; -import 'package:stacked/stacked.dart'; - -final _settingViewModel = SettingsViewModel(); - -// ignore: constant_identifier_names -const int ANDROID_12_SDK_VERSION = 31; - -class SUpdateTheme extends BaseViewModel { - final ManagerAPI _managerAPI = locator<ManagerAPI>(); - - bool getDynamicThemeStatus() { - return _managerAPI.getUseDynamicTheme(); - } - - Future<void> setUseDynamicTheme(BuildContext context, bool value) async { - await _managerAPI.setUseDynamicTheme(value); - final int currentTheme = DynamicTheme.of(context)!.themeId; - if (currentTheme.isEven) { - await DynamicTheme.of(context)!.setTheme(value ? 2 : 0); - } else { - await DynamicTheme.of(context)!.setTheme(value ? 3 : 1); - } - notifyListeners(); - } - - bool getDarkThemeStatus() { - return _managerAPI.getUseDarkTheme(); - } - - Future<void> setUseDarkTheme(BuildContext context, bool value) async { - await _managerAPI.setUseDarkTheme(value); - final int currentTheme = DynamicTheme.of(context)!.themeId; - if (currentTheme < 2) { - await DynamicTheme.of(context)!.setTheme(value ? 1 : 0); - } else { - await DynamicTheme.of(context)!.setTheme(value ? 3 : 2); - } - SystemChrome.setSystemUIOverlayStyle( - SystemUiOverlayStyle( - systemNavigationBarIconBrightness: - value ? Brightness.light : Brightness.dark, - ), - ); - notifyListeners(); - } -} - -class SUpdateThemeUI extends StatelessWidget { - const SUpdateThemeUI({super.key}); - - @override - Widget build(BuildContext context) { - return SettingsSection( - title: 'settingsView.appearanceSectionTitle', - children: <Widget>[ - SwitchListTile( - contentPadding: const EdgeInsets.symmetric(horizontal: 20.0), - title: I18nText( - 'settingsView.darkThemeLabel', - child: const Text( - '', - style: TextStyle( - fontSize: 20, - fontWeight: FontWeight.w500, - ), - ), - ), - subtitle: I18nText('settingsView.darkThemeHint'), - value: SUpdateTheme().getDarkThemeStatus(), - onChanged: (value) => SUpdateTheme().setUseDarkTheme( - context, - value, - ), - ), - FutureBuilder<int>( - future: _settingViewModel.getSdkVersion(), - builder: (context, snapshot) => Visibility( - visible: - snapshot.hasData && snapshot.data! >= ANDROID_12_SDK_VERSION, - child: SwitchListTile( - contentPadding: const EdgeInsets.symmetric(horizontal: 20.0), - title: I18nText( - 'settingsView.dynamicThemeLabel', - child: const Text( - '', - style: TextStyle( - fontSize: 20, - fontWeight: FontWeight.w500, - ), - ), - ), - subtitle: I18nText('settingsView.dynamicThemeHint'), - value: _settingViewModel.sUpdateTheme.getDynamicThemeStatus(), - onChanged: (value) => { - _settingViewModel.sUpdateTheme.setUseDynamicTheme( - context, - value, - ), - }, - ), - ), - ), - ], - ); - } -} diff --git a/lib/ui/views/settings/settings_view.dart b/lib/ui/views/settings/settings_view.dart deleted file mode 100644 index e1002b404a..0000000000 --- a/lib/ui/views/settings/settings_view.dart +++ /dev/null @@ -1,61 +0,0 @@ -// ignore_for_file: prefer_const_constructors - -import 'package:flutter/material.dart'; -import 'package:flutter_i18n/flutter_i18n.dart'; -import 'package:google_fonts/google_fonts.dart'; -import 'package:revanced_manager/ui/views/settings/settingsFragment/settings_update_theme.dart'; -import 'package:revanced_manager/ui/views/settings/settings_viewmodel.dart'; -import 'package:revanced_manager/ui/widgets/settingsView/settings_advanced_section.dart'; -import 'package:revanced_manager/ui/widgets/settingsView/settings_export_section.dart'; -import 'package:revanced_manager/ui/widgets/settingsView/settings_info_section.dart'; -import 'package:revanced_manager/ui/widgets/settingsView/settings_team_section.dart'; -import 'package:revanced_manager/ui/widgets/shared/custom_sliver_app_bar.dart'; -import 'package:stacked/stacked.dart'; - -class SettingsView extends StatelessWidget { - const SettingsView({Key? key}) : super(key: key); - - static const _settingsDivider = - Divider(thickness: 1.0, indent: 20.0, endIndent: 20.0); - - @override - Widget build(BuildContext context) { - return ViewModelBuilder<SettingsViewModel>.reactive( - viewModelBuilder: () => SettingsViewModel(), - builder: (context, model, child) => Scaffold( - body: CustomScrollView( - slivers: <Widget>[ - CustomSliverAppBar( - isMainView: true, - title: I18nText( - 'settingsView.widgetTitle', - child: Text( - '', - style: GoogleFonts.inter( - color: Theme.of(context).textTheme.titleLarge!.color, - ), - ), - ), - ), - SliverList( - delegate: SliverChildListDelegate.fixed( - <Widget>[ - SUpdateThemeUI(), - // SUpdateLanguageUI(), - // _settingsDivider, - STeamSection(), - _settingsDivider, - SAdvancedSection(), - _settingsDivider, - SExportSection(), - _settingsDivider, - SInfoSection(), - ], - ), - ), - ], - ), - ), - ); - } -} diff --git a/lib/ui/views/settings/settings_viewmodel.dart b/lib/ui/views/settings/settings_viewmodel.dart deleted file mode 100644 index 2441b0a6bd..0000000000 --- a/lib/ui/views/settings/settings_viewmodel.dart +++ /dev/null @@ -1,284 +0,0 @@ -import 'dart:io'; -import 'package:cr_file_saver/file_saver.dart'; -import 'package:device_info_plus/device_info_plus.dart'; -import 'package:file_picker/file_picker.dart'; -import 'package:flutter/foundation.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_i18n/flutter_i18n.dart'; -import 'package:logcat/logcat.dart'; -import 'package:path_provider/path_provider.dart'; -import 'package:revanced_manager/app/app.locator.dart'; -import 'package:revanced_manager/app/app.router.dart'; -import 'package:revanced_manager/services/manager_api.dart'; -import 'package:revanced_manager/services/toast.dart'; -import 'package:revanced_manager/ui/views/patcher/patcher_viewmodel.dart'; -import 'package:revanced_manager/ui/views/patches_selector/patches_selector_viewmodel.dart'; -import 'package:revanced_manager/ui/views/settings/settingsFragment/settings_update_language.dart'; -import 'package:revanced_manager/ui/views/settings/settingsFragment/settings_update_theme.dart'; -import 'package:revanced_manager/ui/widgets/shared/custom_material_button.dart'; -import 'package:share_extend/share_extend.dart'; -import 'package:stacked/stacked.dart'; -import 'package:stacked_services/stacked_services.dart'; - -class SettingsViewModel extends BaseViewModel { - final NavigationService _navigationService = locator<NavigationService>(); - final ManagerAPI _managerAPI = locator<ManagerAPI>(); - final PatchesSelectorViewModel _patchesSelectorViewModel = - PatchesSelectorViewModel(); - final PatcherViewModel _patcherViewModel = locator<PatcherViewModel>(); - final Toast _toast = locator<Toast>(); - - final SUpdateLanguage sUpdateLanguage = SUpdateLanguage(); - final SUpdateTheme sUpdateTheme = SUpdateTheme(); - - void navigateToContributors() { - _navigationService.navigateTo(Routes.contributorsView); - } - - bool isPatchesAutoUpdate() { - return _managerAPI.isPatchesAutoUpdate(); - } - - void setPatchesAutoUpdate(bool value) { - _managerAPI.setPatchesAutoUpdate(value); - notifyListeners(); - } - - bool isPatchesChangeEnabled() { - return _managerAPI.isPatchesChangeEnabled(); - } - - Future<void> showPatchesChangeEnableDialog( - bool value, - BuildContext context, - ) async { - if (value) { - return showDialog( - context: context, - builder: (context) => AlertDialog( - backgroundColor: Theme.of(context).colorScheme.secondaryContainer, - title: I18nText('warning'), - content: I18nText( - 'settingsView.enablePatchesSelectionWarningText', - child: const Text( - '', - style: TextStyle( - fontSize: 16, - fontWeight: FontWeight.w500, - ), - ), - ), - actions: [ - CustomMaterialButton( - isFilled: false, - label: I18nText('noButton'), - onPressed: () { - Navigator.of(context).pop(); - }, - ), - CustomMaterialButton( - label: I18nText('yesButton'), - onPressed: () { - _managerAPI.setChangingToggleModified(true); - _managerAPI.setPatchesChangeEnabled(true); - Navigator.of(context).pop(); - }, - ), - ], - ), - ); - } else { - return showDialog( - context: context, - builder: (context) => AlertDialog( - backgroundColor: Theme.of(context).colorScheme.secondaryContainer, - title: I18nText('warning'), - content: I18nText( - 'settingsView.disablePatchesSelectionWarningText', - child: const Text( - '', - style: TextStyle( - fontSize: 16, - fontWeight: FontWeight.w500, - ), - ), - ), - actions: [ - CustomMaterialButton( - isFilled: false, - label: I18nText('noButton'), - onPressed: () { - Navigator.of(context).pop(); - }, - ), - CustomMaterialButton( - label: I18nText('yesButton'), - onPressed: () { - _managerAPI.setChangingToggleModified(true); - _patchesSelectorViewModel.selectDefaultPatches(); - _managerAPI.setPatchesChangeEnabled(false); - Navigator.of(context).pop(); - }, - ), - ], - ), - ); - } - } - - bool areUniversalPatchesEnabled() { - return _managerAPI.areUniversalPatchesEnabled(); - } - - void showUniversalPatches(bool value) { - _managerAPI.enableUniversalPatchesStatus(value); - notifyListeners(); - } - - bool areExperimentalPatchesEnabled() { - return _managerAPI.areExperimentalPatchesEnabled(); - } - - void useExperimentalPatches(bool value) { - _managerAPI.enableExperimentalPatchesStatus(value); - notifyListeners(); - } - - void deleteKeystore() { - _managerAPI.deleteKeystore(); - _toast.showBottom('settingsView.regeneratedKeystore'); - notifyListeners(); - } - - void deleteTempDir() { - _managerAPI.deleteTempFolder(); - _toast.showBottom('settingsView.deletedTempDir'); - notifyListeners(); - } - - Future<void> exportPatches() async { - try { - final File outFile = File(_managerAPI.storedPatchesFile); - if (outFile.existsSync()) { - final String dateTime = - DateTime.now().toString().replaceAll(' ', '_').split('.').first; - await CRFileSaver.saveFileWithDialog( - SaveFileDialogParams( - sourceFilePath: outFile.path, - destinationFileName: 'selected_patches_$dateTime.json', - ), - ); - _toast.showBottom('settingsView.exportedPatches'); - } else { - _toast.showBottom('settingsView.noExportFileFound'); - } - } on Exception catch (e) { - if (kDebugMode) { - print(e); - } - } - } - - Future<void> importPatches(BuildContext context) async { - if (isPatchesChangeEnabled()) { - try { - final FilePickerResult? result = await FilePicker.platform.pickFiles( - type: FileType.custom, - allowedExtensions: ['json'], - ); - if (result != null && result.files.single.path != null) { - final File inFile = File(result.files.single.path!); - inFile.copySync(_managerAPI.storedPatchesFile); - inFile.delete(); - if (_patcherViewModel.selectedApp != null) { - _patcherViewModel.loadLastSelectedPatches(); - } - _toast.showBottom('settingsView.importedPatches'); - } - } on Exception catch (e) { - if (kDebugMode) { - print(e); - } - _toast.showBottom('settingsView.jsonSelectorErrorMessage'); - } - } else { - _managerAPI.showPatchesChangeWarningDialog(context); - } - } - - Future<void> exportKeystore() async { - try { - final File outFile = File(_managerAPI.keystoreFile); - if (outFile.existsSync()) { - final String dateTime = - DateTime.now().toString().replaceAll(' ', '_').split('.').first; - await CRFileSaver.saveFileWithDialog( - SaveFileDialogParams( - sourceFilePath: outFile.path, - destinationFileName: 'keystore_$dateTime.keystore', - ), - ); - _toast.showBottom('settingsView.exportedKeystore'); - } else { - _toast.showBottom('settingsView.noKeystoreExportFileFound'); - } - } on Exception catch (e) { - if (kDebugMode) { - print(e); - } - } - } - - Future<void> importKeystore() async { - try { - final FilePickerResult? result = await FilePicker.platform.pickFiles(); - if (result != null && result.files.single.path != null) { - final File inFile = File(result.files.single.path!); - inFile.copySync(_managerAPI.keystoreFile); - - _toast.showBottom('settingsView.importedKeystore'); - } - } on Exception catch (e) { - if (kDebugMode) { - print(e); - } - _toast.showBottom('settingsView.keystoreSelectorErrorMessage'); - } - } - - void resetSelectedPatches() { - _managerAPI.resetLastSelectedPatches(); - _toast.showBottom('settingsView.resetStoredPatches'); - } - - Future<int> getSdkVersion() async { - final AndroidDeviceInfo info = await DeviceInfoPlugin().androidInfo; - return info.version.sdkInt; - } - - Future<void> deleteLogs() async { - final Directory appCacheDir = await getTemporaryDirectory(); - final Directory logsDir = Directory('${appCacheDir.path}/logs'); - if (logsDir.existsSync()) { - logsDir.deleteSync(recursive: true); - } - _toast.showBottom('settingsView.deletedLogs'); - } - - Future<void> exportLogcatLogs() async { - final Directory appCache = await getTemporaryDirectory(); - final Directory logDir = Directory('${appCache.path}/logs'); - logDir.createSync(); - final String dateTime = DateTime.now() - .toIso8601String() - .replaceAll('-', '') - .replaceAll(':', '') - .replaceAll('T', '') - .replaceAll('.', ''); - final File logcat = - File('${logDir.path}/revanced-manager_logcat_$dateTime.log'); - final String logs = await Logcat.execute(); - logcat.writeAsStringSync(logs); - ShareExtend.share(logcat.path, 'file'); - } -} diff --git a/lib/ui/widgets/appInfoView/app_info_view.dart b/lib/ui/widgets/appInfoView/app_info_view.dart deleted file mode 100644 index 2d0f17e13e..0000000000 --- a/lib/ui/widgets/appInfoView/app_info_view.dart +++ /dev/null @@ -1,311 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_i18n/flutter_i18n.dart'; -import 'package:google_fonts/google_fonts.dart'; -import 'package:revanced_manager/models/patched_application.dart'; -import 'package:revanced_manager/ui/widgets/appInfoView/app_info_viewmodel.dart'; -import 'package:revanced_manager/ui/widgets/shared/custom_card.dart'; -import 'package:revanced_manager/ui/widgets/shared/custom_sliver_app_bar.dart'; -import 'package:stacked/stacked.dart'; - -class AppInfoView extends StatelessWidget { - const AppInfoView({ - Key? key, - required this.app, - }) : super(key: key); - final PatchedApplication app; - - @override - Widget build(BuildContext context) { - return ViewModelBuilder<AppInfoViewModel>.reactive( - viewModelBuilder: () => AppInfoViewModel(), - builder: (context, model, child) => Scaffold( - body: CustomScrollView( - slivers: <Widget>[ - CustomSliverAppBar( - title: I18nText( - 'appInfoView.widgetTitle', - child: Text( - '', - style: GoogleFonts.inter( - color: Theme.of(context).textTheme.titleLarge!.color, - ), - ), - ), - ), - SliverPadding( - padding: const EdgeInsets.symmetric(vertical: 20.0), - sliver: SliverList( - delegate: SliverChildListDelegate.fixed( - <Widget>[ - SizedBox( - height: 64.0, - child: CircleAvatar( - backgroundColor: Colors.transparent, - child: Image.memory( - app.icon, - fit: BoxFit.cover, - ), - ), - ), - const SizedBox(height: 20), - Text( - app.name, - textAlign: TextAlign.center, - style: Theme.of(context).textTheme.titleLarge, - ), - const SizedBox(height: 4), - Text( - app.version, - textAlign: TextAlign.center, - style: Theme.of(context).textTheme.titleLarge, - ), - const SizedBox(height: 20), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 20.0), - child: CustomCard( - padding: EdgeInsets.zero, - child: SizedBox( - height: 94.0, - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: <Widget>[ - Expanded( - child: Material( - type: MaterialType.transparency, - child: InkWell( - borderRadius: BorderRadius.circular(16.0), - onTap: () => model.openApp(app), - child: Column( - mainAxisAlignment: - MainAxisAlignment.center, - children: <Widget>[ - Icon( - Icons.open_in_new_outlined, - color: Theme.of(context) - .colorScheme - .primary, - ), - const SizedBox(height: 10), - I18nText( - 'appInfoView.openButton', - child: Text( - '', - style: TextStyle( - color: Theme.of(context) - .colorScheme - .primary, - fontWeight: FontWeight.bold, - ), - ), - ), - ], - ), - ), - ), - ), - VerticalDivider( - color: Theme.of(context).canvasColor, - indent: 12.0, - endIndent: 12.0, - width: 1.0, - ), - Expanded( - child: Material( - type: MaterialType.transparency, - child: InkWell( - borderRadius: BorderRadius.circular(16.0), - onTap: () => model.showUninstallDialog( - context, - app, - false, - ), - child: Column( - mainAxisAlignment: - MainAxisAlignment.center, - children: <Widget>[ - Icon( - Icons.delete_outline, - color: Theme.of(context) - .colorScheme - .primary, - ), - const SizedBox(height: 10), - I18nText( - 'appInfoView.uninstallButton', - child: Text( - '', - style: TextStyle( - color: Theme.of(context) - .colorScheme - .primary, - fontWeight: FontWeight.bold, - ), - ), - ), - ], - ), - ), - ), - ), - VerticalDivider( - color: Theme.of(context).canvasColor, - indent: 12.0, - endIndent: 12.0, - width: 1.0, - ), - if (app.isRooted) - VerticalDivider( - color: Theme.of(context).canvasColor, - indent: 12.0, - endIndent: 12.0, - width: 1.0, - ), - if (app.isRooted) - Expanded( - child: Material( - type: MaterialType.transparency, - child: InkWell( - borderRadius: BorderRadius.circular(16.0), - onTap: () => model.showUninstallDialog( - context, - app, - true, - ), - child: Column( - mainAxisAlignment: - MainAxisAlignment.center, - children: <Widget>[ - Icon( - Icons - .settings_backup_restore_outlined, - color: Theme.of(context) - .colorScheme - .primary, - ), - const SizedBox(height: 10), - I18nText( - 'appInfoView.unpatchButton', - child: Text( - '', - style: TextStyle( - color: Theme.of(context) - .colorScheme - .primary, - fontWeight: FontWeight.bold, - ), - ), - ), - ], - ), - ), - ), - ), - ], - ), - ), - ), - ), - const SizedBox(height: 20), - ListTile( - contentPadding: - const EdgeInsets.symmetric(horizontal: 20.0), - title: I18nText( - 'appInfoView.packageNameLabel', - child: const Text( - '', - style: TextStyle( - fontSize: 20, - fontWeight: FontWeight.w500, - ), - ), - ), - subtitle: Text(app.packageName), - ), - const SizedBox(height: 4), - ListTile( - contentPadding: - const EdgeInsets.symmetric(horizontal: 20.0), - title: I18nText( - 'appInfoView.originalPackageNameLabel', - child: const Text( - '', - style: TextStyle( - fontSize: 20, - fontWeight: FontWeight.w500, - ), - ), - ), - subtitle: Text(app.originalPackageName), - ), - const SizedBox(height: 4), - ListTile( - contentPadding: - const EdgeInsets.symmetric(horizontal: 20.0), - title: I18nText( - 'appInfoView.installTypeLabel', - child: const Text( - '', - style: TextStyle( - fontSize: 20, - fontWeight: FontWeight.w500, - ), - ), - ), - subtitle: app.isRooted - ? I18nText('appInfoView.rootTypeLabel') - : I18nText('appInfoView.nonRootTypeLabel'), - ), - const SizedBox(height: 4), - ListTile( - contentPadding: - const EdgeInsets.symmetric(horizontal: 20.0), - title: I18nText( - 'appInfoView.patchedDateLabel', - child: const Text( - '', - style: TextStyle( - fontSize: 20, - fontWeight: FontWeight.w500, - ), - ), - ), - subtitle: I18nText( - 'appInfoView.patchedDateHint', - translationParams: { - 'date': model.getPrettyDate(context, app.patchDate), - 'time': model.getPrettyTime(context, app.patchDate), - }, - ), - ), - const SizedBox(height: 4), - ListTile( - contentPadding: - const EdgeInsets.symmetric(horizontal: 20.0), - title: I18nText( - 'appInfoView.appliedPatchesLabel', - child: const Text( - '', - style: TextStyle( - fontSize: 20, - fontWeight: FontWeight.w500, - ), - ), - ), - subtitle: I18nText( - 'appInfoView.appliedPatchesHint', - translationParams: { - 'quantity': app.appliedPatches.length.toString(), - }, - ), - onTap: () => model.showAppliedPatchesDialog(context, app), - ), - ], - ), - ), - ), - ], - ), - ), - ); - } -} diff --git a/lib/ui/widgets/appInfoView/app_info_viewmodel.dart b/lib/ui/widgets/appInfoView/app_info_viewmodel.dart deleted file mode 100644 index dd364c7d6b..0000000000 --- a/lib/ui/widgets/appInfoView/app_info_viewmodel.dart +++ /dev/null @@ -1,166 +0,0 @@ -// ignore_for_file: use_build_context_synchronously -import 'package:device_apps/device_apps.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_i18n/flutter_i18n.dart'; -import 'package:intl/intl.dart'; -import 'package:revanced_manager/app/app.locator.dart'; -import 'package:revanced_manager/models/patched_application.dart'; -import 'package:revanced_manager/services/manager_api.dart'; -import 'package:revanced_manager/services/patcher_api.dart'; -import 'package:revanced_manager/services/root_api.dart'; -import 'package:revanced_manager/services/toast.dart'; -import 'package:revanced_manager/ui/views/home/home_viewmodel.dart'; -import 'package:revanced_manager/ui/views/navigation/navigation_viewmodel.dart'; -import 'package:revanced_manager/ui/views/patcher/patcher_viewmodel.dart'; -import 'package:revanced_manager/ui/widgets/shared/custom_material_button.dart'; -import 'package:revanced_manager/utils/string.dart'; -import 'package:stacked/stacked.dart'; - -class AppInfoViewModel extends BaseViewModel { - final ManagerAPI _managerAPI = locator<ManagerAPI>(); - final PatcherAPI _patcherAPI = locator<PatcherAPI>(); - final RootAPI _rootAPI = RootAPI(); - final Toast _toast = locator<Toast>(); - - Future<void> uninstallApp( - BuildContext context, - PatchedApplication app, - bool onlyUnpatch, - ) async { - bool isUninstalled = true; - if (app.isRooted) { - final bool hasRootPermissions = await _rootAPI.hasRootPermissions(); - if (hasRootPermissions) { - await _rootAPI.deleteApp(app.packageName, app.apkFilePath); - if (!onlyUnpatch) { - await DeviceApps.uninstallApp(app.packageName); - } - } - } else { - isUninstalled = await DeviceApps.uninstallApp(app.packageName); - } - if (isUninstalled) { - await _managerAPI.deletePatchedApp(app); - locator<HomeViewModel>().initialize(context); - } - } - - Future<void> navigateToPatcher(PatchedApplication app) async { - locator<PatcherViewModel>().selectedApp = app; - locator<PatcherViewModel>().selectedPatches = - await _patcherAPI.getAppliedPatches(app.appliedPatches); - locator<PatcherViewModel>().notifyListeners(); - locator<NavigationViewModel>().setIndex(1); - } - - void updateNotImplemented(BuildContext context) { - _toast.showBottom('appInfoView.updateNotImplemented'); - } - - Future<void> showUninstallDialog( - BuildContext context, - PatchedApplication app, - bool onlyUnpatch, - ) async { - final bool hasRootPermissions = await _rootAPI.hasRootPermissions(); - if (app.isRooted && !hasRootPermissions) { - return showDialog( - context: context, - builder: (context) => AlertDialog( - title: I18nText('appInfoView.rootDialogTitle'), - backgroundColor: Theme.of(context).colorScheme.secondaryContainer, - content: I18nText('appInfoView.rootDialogText'), - actions: <Widget>[ - CustomMaterialButton( - label: I18nText('okButton'), - onPressed: () => Navigator.of(context).pop(), - ), - ], - ), - ); - } else { - if (onlyUnpatch) { - return showDialog( - context: context, - builder: (context) => AlertDialog( - title: I18nText( - 'appInfoView.unpatchButton', - ), - backgroundColor: Theme.of(context).colorScheme.secondaryContainer, - content: I18nText( - 'appInfoView.unpatchDialogText', - ), - actions: <Widget>[ - CustomMaterialButton( - isFilled: false, - label: I18nText('noButton'), - onPressed: () => Navigator.of(context).pop(), - ), - CustomMaterialButton( - label: I18nText('yesButton'), - onPressed: () { - uninstallApp(context, app, onlyUnpatch); - Navigator.of(context).pop(); - Navigator.of(context).pop(); - }, - ), - ], - ), - ); - } else { - uninstallApp(context, app, onlyUnpatch); - Navigator.of(context).pop(); - } - } - } - - String getPrettyDate(BuildContext context, DateTime dateTime) { - return DateFormat.yMMMMd(Localizations.localeOf(context).languageCode) - .format(dateTime); - } - - String getPrettyTime(BuildContext context, DateTime dateTime) { - return DateFormat.jm(Localizations.localeOf(context).languageCode) - .format(dateTime); - } - - Future<void> showAppliedPatchesDialog( - BuildContext context, - PatchedApplication app, - ) async { - return showDialog( - context: context, - builder: (context) => AlertDialog( - title: I18nText('appInfoView.appliedPatchesLabel'), - backgroundColor: Theme.of(context).colorScheme.secondaryContainer, - content: SingleChildScrollView( - child: Text(getAppliedPatchesString(app.appliedPatches)), - ), - actions: <Widget>[ - CustomMaterialButton( - label: I18nText('okButton'), - onPressed: () => Navigator.of(context).pop(), - ), - ], - ), - ); - } - - String getAppliedPatchesString(List<String> appliedPatches) { - final List<String> names = appliedPatches - .map( - (p) => p - .replaceAll('-', ' ') - .split('-') - .join(' ') - .toTitleCase() - .replaceFirst('Microg', 'MicroG'), - ) - .toList(); - return '\u2022 ${names.join('\n\u2022 ')}'; - } - - void openApp(PatchedApplication app) { - DeviceApps.openApp(app.packageName); - } -} diff --git a/lib/ui/widgets/appSelectorView/app_skeleton_loader.dart b/lib/ui/widgets/appSelectorView/app_skeleton_loader.dart deleted file mode 100644 index 0cb80428a6..0000000000 --- a/lib/ui/widgets/appSelectorView/app_skeleton_loader.dart +++ /dev/null @@ -1,74 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:revanced_manager/ui/widgets/shared/custom_card.dart'; -import 'package:skeletons/skeletons.dart'; - -class AppSkeletonLoader extends StatelessWidget { - const AppSkeletonLoader({Key? key}) : super(key: key); - - @override - Widget build(BuildContext context) { - final screenWidth = MediaQuery.sizeOf(context).width; - return ListView.builder( - shrinkWrap: true, - itemCount: 7, - padding: EdgeInsets.zero, - itemBuilder: (context, index) => Padding( - padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 12.0), - child: CustomCard( - child: Row( - children: [ - SkeletonAvatar( - style: SkeletonAvatarStyle( - width: screenWidth * 0.10, - height: screenWidth * 0.10, - borderRadius: const BorderRadius.all(Radius.circular(12)), - ), - ), - const SizedBox(width: 16), - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: <Widget>[ - SizedBox( - width: screenWidth * 0.4, - child: SkeletonLine( - style: SkeletonLineStyle( - height: 20, - width: screenWidth * 0.4, - borderRadius: - const BorderRadius.all(Radius.circular(10)), - ), - ), - ), - const SizedBox(height: 12), - SizedBox( - width: screenWidth * 0.6, - child: SkeletonLine( - style: SkeletonLineStyle( - height: 15, - width: screenWidth * 0.6, - borderRadius: - const BorderRadius.all(Radius.circular(10)), - ), - ), - ), - const SizedBox(height: 5), - SizedBox( - width: screenWidth * 0.5, - child: SkeletonLine( - style: SkeletonLineStyle( - height: 15, - width: screenWidth * 0.5, - borderRadius: - const BorderRadius.all(Radius.circular(10)), - ), - ), - ), - ], - ), - ], - ), - ), - ), - ); - } -} diff --git a/lib/ui/widgets/appSelectorView/installed_app_item.dart b/lib/ui/widgets/appSelectorView/installed_app_item.dart deleted file mode 100644 index ae129cbfdc..0000000000 --- a/lib/ui/widgets/appSelectorView/installed_app_item.dart +++ /dev/null @@ -1,107 +0,0 @@ -import 'dart:typed_data'; -import 'package:flutter/material.dart'; -import 'package:flutter_i18n/flutter_i18n.dart'; -import 'package:revanced_manager/ui/widgets/shared/custom_card.dart'; - -class InstalledAppItem extends StatefulWidget { - const InstalledAppItem({ - Key? key, - required this.name, - required this.pkgName, - required this.icon, - required this.patchesCount, - required this.suggestedVersion, - required this.installedVersion, - this.onTap, - }) : super(key: key); - final String name; - final String pkgName; - final Uint8List icon; - final int patchesCount; - final String suggestedVersion; - final String installedVersion; - final Function()? onTap; - - @override - State<InstalledAppItem> createState() => _InstalledAppItemState(); -} - -class _InstalledAppItemState extends State<InstalledAppItem> { - @override - Widget build(BuildContext context) { - return Padding( - padding: const EdgeInsets.symmetric(vertical: 4.0), - child: CustomCard( - onTap: widget.onTap, - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: <Widget>[ - Container( - width: 48, - height: 48, - padding: const EdgeInsets.symmetric(vertical: 4.0), - alignment: Alignment.center, - child: CircleAvatar( - backgroundColor: Colors.transparent, - child: Image.memory(widget.icon), - ), - ), - const SizedBox(width: 12), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: <Widget>[ - Text( - widget.name, - maxLines: 2, - overflow: TextOverflow.visible, - style: const TextStyle( - fontSize: 16, - fontWeight: FontWeight.w500, - ), - ), - Text(widget.pkgName), - I18nText( - FlutterI18n.translate( - context, - 'installed', - translationParams: { - 'version': 'v${widget.installedVersion}', - }, - ), - ), - Wrap( - children: [ - I18nText( - 'suggested', - translationParams: { - 'version': widget.suggestedVersion.isEmpty - ? FlutterI18n.translate( - context, - 'appSelectorCard.allVersions', - ) - : 'v${widget.suggestedVersion}', - }, - ), - const SizedBox(width: 4), - Text( - widget.patchesCount == 1 - ? '• ${widget.patchesCount} patch' - : '• ${widget.patchesCount} patches', - maxLines: 1, - overflow: TextOverflow.ellipsis, - style: TextStyle( - color: Theme.of(context).colorScheme.secondary, - ), - ), - ], - ), - ], - ), - ), - ], - ), - ), - ); - } -} diff --git a/lib/ui/widgets/appSelectorView/not_installed_app_item.dart b/lib/ui/widgets/appSelectorView/not_installed_app_item.dart deleted file mode 100644 index c2f3f52e09..0000000000 --- a/lib/ui/widgets/appSelectorView/not_installed_app_item.dart +++ /dev/null @@ -1,101 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_i18n/flutter_i18n.dart'; -import 'package:revanced_manager/ui/widgets/shared/custom_card.dart'; - -class NotInstalledAppItem extends StatefulWidget { - const NotInstalledAppItem({ - Key? key, - required this.name, - required this.patchesCount, - required this.suggestedVersion, - this.onTap, - }) : super(key: key); - final String name; - final int patchesCount; - final String suggestedVersion; - final Function()? onTap; - - @override - State<NotInstalledAppItem> createState() => _NotInstalledAppItem(); -} - -class _NotInstalledAppItem extends State<NotInstalledAppItem> { - @override - Widget build(BuildContext context) { - return Padding( - padding: const EdgeInsets.symmetric(vertical: 4.0), - child: CustomCard( - onTap: widget.onTap, - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: <Widget>[ - Container( - height: 48, - padding: const EdgeInsets.symmetric(vertical: 4.0), - alignment: Alignment.center, - child: const CircleAvatar( - backgroundColor: Colors.transparent, - child: Icon( - Icons.square_rounded, - color: Colors.grey, - size: 44, - ), - ), - ), - const SizedBox(width: 12), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: <Widget>[ - Text( - widget.name, - style: const TextStyle( - fontSize: 16, - fontWeight: FontWeight.w500, - ), - ), - const SizedBox(height: 4), - I18nText( - 'appSelectorCard.notInstalled', - child: Text( - '', - style: TextStyle( - color: Theme.of(context).textTheme.titleLarge!.color, - ), - ), - ), - Wrap( - children: [ - I18nText( - 'suggested', - translationParams: { - 'version': widget.suggestedVersion.isEmpty - ? FlutterI18n.translate( - context, - 'appSelectorCard.allVersions', - ) - : 'v${widget.suggestedVersion}', - }, - ), - const SizedBox(width: 4), - Text( - widget.patchesCount == 1 - ? '• ${widget.patchesCount} patch' - : '• ${widget.patchesCount} patches', - maxLines: 1, - overflow: TextOverflow.ellipsis, - style: TextStyle( - color: Theme.of(context).colorScheme.secondary, - ), - ), - ], - ), - ], - ), - ), - ], - ), - ), - ); - } -} diff --git a/lib/ui/widgets/contributorsView/contributors_card.dart b/lib/ui/widgets/contributorsView/contributors_card.dart deleted file mode 100644 index 574699a84b..0000000000 --- a/lib/ui/widgets/contributorsView/contributors_card.dart +++ /dev/null @@ -1,77 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_cache_manager/file.dart'; -import 'package:flutter_cache_manager/flutter_cache_manager.dart'; -import 'package:flutter_i18n/flutter_i18n.dart'; -import 'package:revanced_manager/ui/widgets/shared/custom_card.dart'; -import 'package:url_launcher/url_launcher.dart'; - -class ContributorsCard extends StatefulWidget { - const ContributorsCard({ - Key? key, - required this.title, - required this.contributors, - }) : super(key: key); - final String title; - final List<dynamic> contributors; - - @override - State<ContributorsCard> createState() => _ContributorsCardState(); -} - -class _ContributorsCardState extends State<ContributorsCard> { - @override - Widget build(BuildContext context) { - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: <Widget>[ - Padding( - padding: const EdgeInsets.only(bottom: 8.0), - child: I18nText( - widget.title, - child: const Text( - '', - style: TextStyle( - fontSize: 20, - fontWeight: FontWeight.w500, - ), - ), - ), - ), - CustomCard( - child: GridView.builder( - shrinkWrap: true, - padding: EdgeInsets.zero, - physics: const NeverScrollableScrollPhysics(), - gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( - crossAxisCount: 6, - mainAxisSpacing: 8, - crossAxisSpacing: 8, - ), - itemCount: widget.contributors.length, - itemBuilder: (context, index) => ClipRRect( - borderRadius: BorderRadius.circular(100), - child: GestureDetector( - onTap: () => launchUrl( - Uri.parse( - widget.contributors[index]['html_url'], - ), - mode: LaunchMode.externalApplication, - ), - child: FutureBuilder<File?>( - future: DefaultCacheManager().getSingleFile( - widget.contributors[index]['avatar_url'], - ), - builder: (context, snapshot) => snapshot.hasData - ? Image.file(snapshot.data!) - : Image.network( - widget.contributors[index]['avatar_url'], - ), - ), - ), - ), - ), - ), - ], - ); - } -} diff --git a/lib/ui/widgets/homeView/installed_apps_card.dart b/lib/ui/widgets/homeView/installed_apps_card.dart deleted file mode 100644 index 7a98985641..0000000000 --- a/lib/ui/widgets/homeView/installed_apps_card.dart +++ /dev/null @@ -1,96 +0,0 @@ -import 'package:device_apps/device_apps.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_i18n/flutter_i18n.dart'; -import 'package:revanced_manager/app/app.locator.dart'; -import 'package:revanced_manager/models/patched_application.dart'; -import 'package:revanced_manager/services/manager_api.dart'; -import 'package:revanced_manager/ui/views/home/home_viewmodel.dart'; -import 'package:revanced_manager/ui/widgets/shared/application_item.dart'; -import 'package:revanced_manager/ui/widgets/shared/custom_card.dart'; - -//ignore: must_be_immutable -class InstalledAppsCard extends StatelessWidget { - InstalledAppsCard({Key? key}) : super(key: key); - - List<PatchedApplication> apps = locator<HomeViewModel>().patchedInstalledApps; - final ManagerAPI _managerAPI = locator<ManagerAPI>(); - List<PatchedApplication> patchedApps = []; - - Future _getApps() async { - if (apps.isNotEmpty) { - patchedApps = [...apps]; - for (final element in apps) { - await DeviceApps.getApp(element.packageName).then((value) { - if (element.version != value?.versionName) { - patchedApps.remove(element); - } - }); - } - if (apps.length != patchedApps.length) { - await _managerAPI.setPatchedApps(patchedApps); - apps.clear(); - apps = [...patchedApps]; - } - } - } - - @override - Widget build(BuildContext context) { - return FutureBuilder( - future: _getApps(), - builder: (context, snapshot) { - if (snapshot.connectionState == ConnectionState.done) { - return apps.isEmpty - ? CustomCard( - child: Center( - child: Column( - children: <Widget>[ - Icon( - size: 40, - Icons.file_download_off, - color: Theme.of(context).colorScheme.secondary, - ), - const SizedBox(height: 16), - I18nText( - 'homeView.noInstallations', - child: Text( - '', - textAlign: TextAlign.center, - style: Theme.of(context) - .textTheme - .titleMedium! - .copyWith( - color: - Theme.of(context).colorScheme.secondary, - ), - ), - ), - ], - ), - ), - ) - : ListView( - shrinkWrap: true, - padding: EdgeInsets.zero, - physics: const NeverScrollableScrollPhysics(), - children: apps - .map( - (app) => ApplicationItem( - icon: app.icon, - name: app.name, - patchDate: app.patchDate, - changelog: app.changelog, - isUpdatableApp: false, - onPressed: () => - locator<HomeViewModel>().navigateToAppInfo(app), - ), - ) - .toList(), - ); - } else { - return const Center(child: CircularProgressIndicator()); - } - }, - ); - } -} diff --git a/lib/ui/widgets/homeView/latest_commit_card.dart b/lib/ui/widgets/homeView/latest_commit_card.dart deleted file mode 100644 index 2d62286f52..0000000000 --- a/lib/ui/widgets/homeView/latest_commit_card.dart +++ /dev/null @@ -1,133 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_i18n/flutter_i18n.dart'; -import 'package:revanced_manager/app/app.locator.dart'; -import 'package:revanced_manager/ui/views/home/home_viewmodel.dart'; -import 'package:revanced_manager/ui/widgets/shared/custom_card.dart'; -import 'package:revanced_manager/ui/widgets/shared/custom_material_button.dart'; - -class LatestCommitCard extends StatefulWidget { - const LatestCommitCard({ - Key? key, - required this.model, - required this.parentContext, - }) : super(key: key); - final HomeViewModel model; - final BuildContext parentContext; - - @override - State<LatestCommitCard> createState() => _LatestCommitCardState(); -} - -class _LatestCommitCardState extends State<LatestCommitCard> { - final HomeViewModel model = locator<HomeViewModel>(); - - @override - Widget build(BuildContext context) { - return Column( - children: [ - // ReVanced Manager - CustomCard( - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: <Widget>[ - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: <Widget>[ - const Text('ReVanced Manager'), - const SizedBox(height: 4), - Row( - children: <Widget>[ - FutureBuilder<String?>( - future: model.getLatestManagerReleaseTime(), - builder: (context, snapshot) => snapshot.hasData && - snapshot.data!.isNotEmpty - ? I18nText( - 'latestCommitCard.timeagoLabel', - translationParams: {'time': snapshot.data!}, - ) - : I18nText('latestCommitCard.loadingLabel'), - ), - ], - ), - ], - ), - ), - FutureBuilder<bool>( - future: model.hasManagerUpdates(), - initialData: false, - builder: (context, snapshot) => Opacity( - opacity: snapshot.hasData && snapshot.data! ? 1.0 : 0.25, - child: CustomMaterialButton( - label: I18nText('updateButton'), - onPressed: snapshot.hasData && snapshot.data! - ? () => widget.model.showUpdateConfirmationDialog( - widget.parentContext, - false, - ) - : () => {}, - ), - ), - ), - ], - ), - ), - - const SizedBox(height: 16), - - // ReVanced Patches - CustomCard( - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: <Widget>[ - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: <Widget>[ - const Text('ReVanced Patches'), - const SizedBox(height: 4), - Row( - children: <Widget>[ - FutureBuilder<String?>( - future: model.getLatestPatchesReleaseTime(), - builder: (context, snapshot) => Text( - snapshot.hasData && snapshot.data!.isNotEmpty - ? FlutterI18n.translate( - context, - 'latestCommitCard.timeagoLabel', - translationParams: {'time': snapshot.data!}, - ) - : FlutterI18n.translate( - context, - 'latestCommitCard.loadingLabel', - ), - ), - ), - ], - ), - ], - ), - ), - FutureBuilder<bool>( - future: locator<HomeViewModel>().hasPatchesUpdates(), - initialData: false, - builder: (context, snapshot) => Opacity( - opacity: snapshot.hasData && snapshot.data! ? 1.0 : 0.25, - child: CustomMaterialButton( - label: I18nText('updateButton'), - onPressed: snapshot.hasData && snapshot.data! - ? () => widget.model.showUpdateConfirmationDialog( - widget.parentContext, - true, - ) - : () => {}, - ), - ), - ), - ], - ), - ), - ], - ); - } -} diff --git a/lib/ui/widgets/homeView/update_confirmation_dialog.dart b/lib/ui/widgets/homeView/update_confirmation_dialog.dart deleted file mode 100644 index 7839536ad5..0000000000 --- a/lib/ui/widgets/homeView/update_confirmation_dialog.dart +++ /dev/null @@ -1,139 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_i18n/flutter_i18n.dart'; -import 'package:flutter_markdown/flutter_markdown.dart'; -import 'package:revanced_manager/app/app.locator.dart'; -import 'package:revanced_manager/ui/views/home/home_viewmodel.dart'; -import 'package:revanced_manager/ui/widgets/shared/custom_material_button.dart'; - -class UpdateConfirmationDialog extends StatelessWidget { - const UpdateConfirmationDialog({super.key, required this.isPatches}); - - final bool isPatches; - @override - Widget build(BuildContext context) { - final HomeViewModel model = locator<HomeViewModel>(); - - return DraggableScrollableSheet( - expand: false, - snap: true, - snapSizes: const [0.5], - builder: (_, scrollController) => SingleChildScrollView( - controller: scrollController, - child: SafeArea( - child: FutureBuilder<Map<String, dynamic>?>( - future: !isPatches - ? model.getLatestManagerRelease() - : model.getLatestPatchesRelease(), - builder: (_, snapshot) { - if (!snapshot.hasData) { - return const SizedBox( - height: 300, - child: Center( - child: CircularProgressIndicator(), - ), - ); - } - - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Padding( - padding: const EdgeInsets.only( - top: 40.0, - left: 24.0, - right: 24.0, - bottom: 32.0, - ), - child: Row( - children: [ - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - I18nText( - isPatches - ? 'homeView.updatePatchesDialogTitle' - : 'homeView.updateDialogTitle', - child: const Text( - '', - style: TextStyle( - fontSize: 24, - fontWeight: FontWeight.bold, - ), - ), - ), - const SizedBox(height: 4.0), - Row( - children: [ - Icon( - Icons.new_releases_outlined, - color: - Theme.of(context).colorScheme.secondary, - ), - const SizedBox(width: 8.0), - Text( - snapshot.data!['tag_name'] ?? 'Unknown', - style: TextStyle( - fontSize: 20, - fontWeight: FontWeight.w500, - color: Theme.of(context) - .colorScheme - .secondary, - ), - ), - ], - ), - ], - ), - ), - CustomMaterialButton( - isExpanded: true, - label: I18nText('updateButton'), - onPressed: () { - Navigator.of(context).pop(); - isPatches - ? model.updatePatches(context) - : model.updateManager(context); - }, - ), - ], - ), - ), - Padding( - padding: const EdgeInsets.only(left: 24.0, bottom: 12.0), - child: I18nText( - 'homeView.updateChangelogTitle', - child: Text( - '', - style: TextStyle( - fontSize: 20, - fontWeight: FontWeight.w500, - color: Theme.of(context) - .colorScheme - .onSecondaryContainer, - ), - ), - ), - ), - Container( - margin: const EdgeInsets.symmetric(horizontal: 24.0), - decoration: BoxDecoration( - color: Theme.of(context).colorScheme.secondaryContainer, - borderRadius: BorderRadius.circular(12.0), - ), - child: Markdown( - shrinkWrap: true, - physics: const NeverScrollableScrollPhysics(), - padding: const EdgeInsets.all(20.0), - data: snapshot.data!['body'] ?? '', - ), - ), - ], - ); - }, - ), - ), - ), - ); - } -} diff --git a/lib/ui/widgets/installerView/gradient_progress_indicator.dart b/lib/ui/widgets/installerView/gradient_progress_indicator.dart deleted file mode 100644 index d403218436..0000000000 --- a/lib/ui/widgets/installerView/gradient_progress_indicator.dart +++ /dev/null @@ -1,32 +0,0 @@ -import 'package:flutter/material.dart'; - -class GradientProgressIndicator extends StatefulWidget { - const GradientProgressIndicator({required this.progress, super.key}); - final double? progress; - - @override - State<GradientProgressIndicator> createState() => - _GradientProgressIndicatorState(); -} - -class _GradientProgressIndicatorState extends State<GradientProgressIndicator> { - @override - Widget build(BuildContext context) { - return Align( - alignment: Alignment.centerLeft, - child: AnimatedContainer( - duration: const Duration(milliseconds: 500), - decoration: BoxDecoration( - gradient: LinearGradient( - colors: [ - Theme.of(context).colorScheme.primary, - Theme.of(context).colorScheme.secondary, - ], - ), - ), - height: 5, - width: MediaQuery.sizeOf(context).width * widget.progress!, - ), - ); - } -} diff --git a/lib/ui/widgets/patcherView/app_selector_card.dart b/lib/ui/widgets/patcherView/app_selector_card.dart deleted file mode 100644 index e97a004d6e..0000000000 --- a/lib/ui/widgets/patcherView/app_selector_card.dart +++ /dev/null @@ -1,76 +0,0 @@ -import 'dart:typed_data'; -import 'package:flutter/material.dart'; -import 'package:flutter_i18n/flutter_i18n.dart'; -import 'package:revanced_manager/app/app.locator.dart'; -import 'package:revanced_manager/ui/views/patcher/patcher_viewmodel.dart'; -import 'package:revanced_manager/ui/widgets/shared/custom_card.dart'; - -class AppSelectorCard extends StatelessWidget { - const AppSelectorCard({ - Key? key, - required this.onPressed, - }) : super(key: key); - final Function() onPressed; - - @override - Widget build(BuildContext context) { - return CustomCard( - onTap: onPressed, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: <Widget>[ - I18nText( - locator<PatcherViewModel>().selectedApp == null - ? 'appSelectorCard.widgetTitle' - : 'appSelectorCard.widgetTitleSelected', - child: const Text( - '', - style: TextStyle( - fontSize: 18, - fontWeight: FontWeight.w500, - ), - ), - ), - const SizedBox(height: 8), - if (locator<PatcherViewModel>().selectedApp == null) - I18nText('appSelectorCard.widgetSubtitle') - else - Row( - children: <Widget>[ - SizedBox( - height: 18.0, - child: ClipOval( - child: Image.memory( - locator<PatcherViewModel>().selectedApp == null - ? Uint8List(0) - : locator<PatcherViewModel>().selectedApp!.icon, - fit: BoxFit.cover, - ), - ), - ), - const SizedBox(width: 6), - Flexible( - child: Text( - locator<PatcherViewModel>().getAppSelectionString(), - style: const TextStyle(fontWeight: FontWeight.w600), - ), - ), - ], - ), - if (locator<PatcherViewModel>().selectedApp == null) - Container() - else - Column( - children: [ - const SizedBox(height: 4), - Text( - locator<PatcherViewModel>() - .getSuggestedVersionString(context), - ), - ], - ), - ], - ), - ); - } -} diff --git a/lib/ui/widgets/patcherView/patch_selector_card.dart b/lib/ui/widgets/patcherView/patch_selector_card.dart deleted file mode 100644 index 1b8265de06..0000000000 --- a/lib/ui/widgets/patcherView/patch_selector_card.dart +++ /dev/null @@ -1,66 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_i18n/flutter_i18n.dart'; -import 'package:revanced_manager/app/app.locator.dart'; -import 'package:revanced_manager/models/patch.dart'; -import 'package:revanced_manager/ui/views/patcher/patcher_viewmodel.dart'; -import 'package:revanced_manager/ui/widgets/shared/custom_card.dart'; - -class PatchSelectorCard extends StatelessWidget { - const PatchSelectorCard({ - Key? key, - required this.onPressed, - }) : super(key: key); - final Function() onPressed; - - @override - Widget build(BuildContext context) { - return CustomCard( - onTap: onPressed, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: <Widget>[ - Row( - children: <Widget>[ - I18nText( - locator<PatcherViewModel>().selectedPatches.isEmpty - ? 'patchSelectorCard.widgetTitle' - : 'patchSelectorCard.widgetTitleSelected', - child: const Text( - '', - style: TextStyle( - fontSize: 18, - fontWeight: FontWeight.w500, - ), - ), - ), - Text( - locator<PatcherViewModel>().selectedPatches.isEmpty - ? '' - : ' (${locator<PatcherViewModel>().selectedPatches.length})', - style: const TextStyle( - fontSize: 18, - fontWeight: FontWeight.w500, - ), - ), - ], - ), - const SizedBox(height: 4), - if (locator<PatcherViewModel>().selectedApp == null) - I18nText('patchSelectorCard.widgetSubtitle') - else - locator<PatcherViewModel>().selectedPatches.isEmpty - ? I18nText('patchSelectorCard.widgetEmptySubtitle') - : Text(_getPatchesSelection()), - ], - ), - ); - } - - String _getPatchesSelection() { - String text = ''; - for (final Patch p in locator<PatcherViewModel>().selectedPatches) { - text += '\u2022 ${p.getSimpleName()}\n'; - } - return text.substring(0, text.length - 1); - } -} diff --git a/lib/ui/widgets/patchesSelectorView/patch_item.dart b/lib/ui/widgets/patchesSelectorView/patch_item.dart deleted file mode 100644 index 78a92e3612..0000000000 --- a/lib/ui/widgets/patchesSelectorView/patch_item.dart +++ /dev/null @@ -1,248 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_i18n/flutter_i18n.dart'; -import 'package:revanced_manager/app/app.locator.dart'; -import 'package:revanced_manager/services/manager_api.dart'; -import 'package:revanced_manager/services/toast.dart'; -import 'package:revanced_manager/ui/widgets/shared/custom_card.dart'; -import 'package:revanced_manager/ui/widgets/shared/custom_material_button.dart'; - -// ignore: must_be_immutable -class PatchItem extends StatefulWidget { - PatchItem({ - Key? key, - required this.name, - required this.simpleName, - required this.description, - required this.packageVersion, - required this.supportedPackageVersions, - required this.isUnsupported, - required this.isNew, - required this.isSelected, - required this.onChanged, - required this.isChangeEnabled, - this.child, - }) : super(key: key); - final String name; - final String simpleName; - final String description; - final String packageVersion; - final List<String> supportedPackageVersions; - final bool isUnsupported; - final bool isNew; - bool isSelected; - final Function(bool) onChanged; - final bool isChangeEnabled; - final Widget? child; - final toast = locator<Toast>(); - final _managerAPI = locator<ManagerAPI>(); - - @override - State<PatchItem> createState() => _PatchItemState(); -} - -class _PatchItemState extends State<PatchItem> { - @override - Widget build(BuildContext context) { - widget.isSelected = widget.isSelected && - (!widget.isUnsupported || - widget._managerAPI.areExperimentalPatchesEnabled()); - return Padding( - padding: const EdgeInsets.symmetric(vertical: 4.0), - child: Opacity( - opacity: widget.isUnsupported && - widget._managerAPI.areExperimentalPatchesEnabled() == false - ? 0.5 - : 1, - child: CustomCard( - onTap: () { - setState(() { - if (widget.isUnsupported && - !widget._managerAPI.areExperimentalPatchesEnabled()) { - widget.isSelected = false; - widget.toast.showBottom('patchItem.unsupportedPatchVersion'); - } else if (widget.isChangeEnabled) { - widget.isSelected = !widget.isSelected; - } - }); - if (!widget.isUnsupported || widget._managerAPI.areExperimentalPatchesEnabled()) { - widget.onChanged(widget.isSelected); - } - }, - child: Column( - children: <Widget>[ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: <Widget>[ - Flexible( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: <Widget>[ - Row( - crossAxisAlignment: CrossAxisAlignment.end, - children: <Widget>[ - Expanded( - child: Text( - widget.simpleName, - maxLines: 2, - overflow: TextOverflow.visible, - style: const TextStyle( - fontSize: 16, - fontWeight: FontWeight.w600, - ), - ), - ), - ], - ), - const SizedBox(height: 4), - Text( - widget.description, - softWrap: true, - overflow: TextOverflow.visible, - style: TextStyle( - fontSize: 14, - color: Theme.of(context) - .colorScheme - .onSecondaryContainer, - ), - ), - ], - ), - ), - Transform.scale( - scale: 1.2, - child: Checkbox( - value: widget.isSelected, - activeColor: Theme.of(context).colorScheme.primary, - checkColor: - Theme.of(context).colorScheme.secondaryContainer, - side: BorderSide( - width: 2.0, - color: Theme.of(context).colorScheme.primary, - ), - onChanged: (newValue) { - setState(() { - if (widget.isUnsupported && - !widget._managerAPI - .areExperimentalPatchesEnabled()) { - widget.isSelected = false; - widget.toast.showBottom( - 'patchItem.unsupportedPatchVersion', - ); - } else if (widget.isChangeEnabled) { - widget.isSelected = newValue!; - } - }); - if (!widget.isUnsupported || widget._managerAPI.areExperimentalPatchesEnabled()) { - widget.onChanged(widget.isSelected); - } - }, - ), - ), - ], - ), - Row( - children: [ - if (widget.isUnsupported && - widget._managerAPI.areExperimentalPatchesEnabled()) - Padding( - padding: const EdgeInsets.only(top: 8, right: 8), - child: TextButton.icon( - label: I18nText('warning'), - icon: const Icon(Icons.warning, size: 20.0), - onPressed: () => _showUnsupportedWarningDialog(), - style: ButtonStyle( - shape: MaterialStateProperty.all( - RoundedRectangleBorder( - borderRadius: BorderRadius.circular(8), - side: BorderSide( - color: Theme.of(context).colorScheme.secondary, - ), - ), - ), - backgroundColor: MaterialStateProperty.all( - Colors.transparent, - ), - foregroundColor: MaterialStateProperty.all( - Theme.of(context).colorScheme.secondary, - ), - ), - ), - ), - if (widget.isNew) - Padding( - padding: const EdgeInsets.only(top: 8), - child: TextButton.icon( - label: I18nText('new'), - icon: const Icon(Icons.star, size: 20.0), - onPressed: () => _showNewPatchDialog(), - style: ButtonStyle( - shape: MaterialStateProperty.all( - RoundedRectangleBorder( - borderRadius: BorderRadius.circular(8), - side: BorderSide( - color: Theme.of(context).colorScheme.secondary, - ), - ), - ), - backgroundColor: MaterialStateProperty.all( - Colors.transparent, - ), - foregroundColor: MaterialStateProperty.all( - Theme.of(context).colorScheme.secondary, - ), - ), - ), - ), - ], - ), - widget.child ?? const SizedBox(), - ], - ), - ), - ), - ); - } - - Future<void> _showUnsupportedWarningDialog() { - return showDialog( - context: context, - builder: (context) => AlertDialog( - title: I18nText('warning'), - backgroundColor: Theme.of(context).colorScheme.secondaryContainer, - content: I18nText( - 'patchItem.unsupportedDialogText', - translationParams: { - 'packageVersion': widget.packageVersion, - 'supportedVersions': - '\u2022 ${widget.supportedPackageVersions.reversed.join('\n\u2022 ')}', - }, - ), - actions: <Widget>[ - CustomMaterialButton( - label: I18nText('okButton'), - onPressed: () => Navigator.of(context).pop(), - ), - ], - ), - ); - } - - Future<void> _showNewPatchDialog() { - return showDialog( - context: context, - builder: (context) => AlertDialog( - title: I18nText('patchItem.newPatch'), - backgroundColor: Theme.of(context).colorScheme.secondaryContainer, - content: I18nText( - 'patchItem.newPatchDialogText', - ), - actions: <Widget>[ - CustomMaterialButton( - label: I18nText('okButton'), - onPressed: () => Navigator.of(context).pop(), - ), - ], - ), - ); - } -} diff --git a/lib/ui/widgets/patchesSelectorView/patch_options_fields.dart b/lib/ui/widgets/patchesSelectorView/patch_options_fields.dart deleted file mode 100644 index 45a138432a..0000000000 --- a/lib/ui/widgets/patchesSelectorView/patch_options_fields.dart +++ /dev/null @@ -1,73 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_i18n/flutter_i18n.dart'; -import 'package:google_fonts/google_fonts.dart'; - -class OptionsTextField extends StatelessWidget { - const OptionsTextField({Key? key, required this.hint}) : super(key: key); - final String hint; - - @override - Widget build(BuildContext context) { - final size = MediaQuery.sizeOf(context); - final sHeight = size.height; - final sWidth = size.width; - return Container( - margin: const EdgeInsets.only(top: 12, bottom: 6), - padding: EdgeInsets.zero, - child: TextField( - decoration: InputDecoration( - constraints: BoxConstraints( - maxHeight: sHeight * 0.05, - maxWidth: sWidth * 1, - ), - border: const OutlineInputBorder(), - labelText: hint, - ), - ), - ); - } -} - -class OptionsFilePicker extends StatelessWidget { - const OptionsFilePicker({Key? key, required this.optionName}) - : super(key: key); - final String optionName; - - @override - Widget build(BuildContext context) { - return Padding( - padding: const EdgeInsets.symmetric(horizontal: 4.0), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: <Widget>[ - I18nText( - optionName, - child: Text( - '', - style: GoogleFonts.inter( - fontSize: 16, - fontWeight: FontWeight.w500, - ), - ), - ), - ElevatedButton( - style: ButtonStyle( - backgroundColor: MaterialStateProperty.all( - Theme.of(context).colorScheme.primary, - ), - ), - onPressed: () { - // pick files - }, - child: Text( - 'Select File', - style: TextStyle( - color: Theme.of(context).textTheme.bodyLarge?.color, - ), - ), - ), - ], - ), - ); - } -} diff --git a/lib/ui/widgets/settingsView/about_widget.dart b/lib/ui/widgets/settingsView/about_widget.dart deleted file mode 100644 index ebad5f92e8..0000000000 --- a/lib/ui/widgets/settingsView/about_widget.dart +++ /dev/null @@ -1,103 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; -import 'package:flutter_i18n/flutter_i18n.dart'; -import 'package:revanced_manager/utils/about_info.dart'; - -class AboutWidget extends StatefulWidget { - const AboutWidget({Key? key, this.padding}) : super(key: key); - - final EdgeInsetsGeometry? padding; - - @override - State<AboutWidget> createState() => _AboutWidgetState(); -} - -class _AboutWidgetState extends State<AboutWidget> { - @override - Widget build(BuildContext context) { - return FutureBuilder<Map<String, dynamic>>( - future: AboutInfo.getInfo(), - builder: (context, snapshot) { - return Padding( - padding: const EdgeInsets.symmetric(vertical: 8.0), - child: ListTile( - contentPadding: widget.padding ?? EdgeInsets.zero, - onLongPress: snapshot.hasData - ? () { - Clipboard.setData( - ClipboardData( - text: 'Version: ${snapshot.data!['version']}\n' - 'Model: ${snapshot.data!['model']}\n' - 'Android Version: ${snapshot.data!['androidVersion']}\n' - '${snapshot.data!['supportedArch'].length > 1 ? 'Supported Archs' : 'Supported Arch'}: ${snapshot.data!['supportedArch'].join(", ")}\n', - ), - ); - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: I18nText('settingsView.snackbarMessage'), - backgroundColor: - Theme.of(context).colorScheme.secondary, - ), - ); - } - : null, - title: I18nText( - 'settingsView.aboutLabel', - child: const Text( - '', - style: TextStyle( - fontSize: 20, - fontWeight: FontWeight.w500, - ), - ), - ), - subtitle: snapshot.hasData - ? Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: <Widget>[ - Text( - 'Version: ${snapshot.data!['version']}', - style: const TextStyle( - fontSize: 13, - fontWeight: FontWeight.w300, - ), - ), - Text( - 'Build: ${snapshot.data!['flavor']}', - style: const TextStyle( - fontSize: 13, - fontWeight: FontWeight.w300, - ), - ), - Text( - 'Model: ${snapshot.data!['model']}', - style: const TextStyle( - fontSize: 13, - fontWeight: FontWeight.w300, - ), - ), - Text( - 'Android Version: ${snapshot.data!['androidVersion']}', - style: const TextStyle( - fontSize: 13, - fontWeight: FontWeight.w300, - ), - ), - Text( - snapshot.data!['supportedArch'].length > 1 - ? 'Supported Archs: ${snapshot.data!['supportedArch'].join(", ")}' - : 'Supported Arch: ${snapshot.data!['supportedArch']}', - style: const TextStyle( - fontSize: 13, - fontWeight: FontWeight.w300, - ), - ), - ], - ) - : const SizedBox(), - ), - ); - }, - ); - } -} diff --git a/lib/ui/widgets/settingsView/custom_switch.dart b/lib/ui/widgets/settingsView/custom_switch.dart deleted file mode 100644 index 8328c90b26..0000000000 --- a/lib/ui/widgets/settingsView/custom_switch.dart +++ /dev/null @@ -1,63 +0,0 @@ -import 'package:flutter/material.dart'; - -class CustomSwitch extends StatelessWidget { - const CustomSwitch({ - Key? key, - required this.onChanged, - required this.value, - }) : super(key: key); - final ValueChanged<bool> onChanged; - final bool value; - - @override - Widget build(BuildContext context) { - return GestureDetector( - onTap: () => onChanged(!value), - child: SizedBox( - height: 25, - width: 50, - child: Stack( - children: <Widget>[ - AnimatedContainer( - height: 25, - width: 50, - curve: Curves.ease, - duration: const Duration(milliseconds: 400), - decoration: BoxDecoration( - borderRadius: const BorderRadius.all( - Radius.circular(25.0), - ), - color: value - ? Theme.of(context).colorScheme.primary - : Theme.of(context).colorScheme.secondary, - ), - ), - AnimatedAlign( - curve: Curves.ease, - duration: const Duration(milliseconds: 400), - alignment: !value ? Alignment.centerLeft : Alignment.centerRight, - child: Container( - height: 20, - width: 20, - margin: const EdgeInsets.symmetric(horizontal: 3), - decoration: BoxDecoration( - shape: BoxShape.circle, - color: value - ? Theme.of(context).colorScheme.primaryContainer - : Theme.of(context).colorScheme.surface, - boxShadow: [ - BoxShadow( - color: Colors.black12.withOpacity(0.1), - spreadRadius: 0.5, - blurRadius: 1, - ), - ], - ), - ), - ), - ], - ), - ), - ); - } -} diff --git a/lib/ui/widgets/settingsView/custom_switch_tile.dart b/lib/ui/widgets/settingsView/custom_switch_tile.dart deleted file mode 100644 index f17f967082..0000000000 --- a/lib/ui/widgets/settingsView/custom_switch_tile.dart +++ /dev/null @@ -1,32 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:revanced_manager/ui/widgets/settingsView/custom_switch.dart'; - -class CustomSwitchTile extends StatelessWidget { - const CustomSwitchTile({ - Key? key, - required this.title, - required this.subtitle, - required this.value, - required this.onTap, - this.padding, - }) : super(key: key); - final Widget title; - final Widget subtitle; - final bool value; - final Function(bool) onTap; - final EdgeInsetsGeometry? padding; - - @override - Widget build(BuildContext context) { - return ListTile( - contentPadding: padding ?? EdgeInsets.zero, - title: title, - subtitle: subtitle, - onTap: () => onTap(!value), - trailing: CustomSwitch( - value: value, - onChanged: onTap, - ), - ); - } -} diff --git a/lib/ui/widgets/settingsView/custom_text_field.dart b/lib/ui/widgets/settingsView/custom_text_field.dart deleted file mode 100644 index 6f2b8a76a0..0000000000 --- a/lib/ui/widgets/settingsView/custom_text_field.dart +++ /dev/null @@ -1,73 +0,0 @@ -import 'package:flutter/material.dart'; - -class CustomTextField extends StatelessWidget { - const CustomTextField({ - Key? key, - required this.inputController, - required this.label, - required this.hint, - this.leadingIcon, - required this.onChanged, - }) : super(key: key); - final TextEditingController inputController; - final Widget label; - final String hint; - final Widget? leadingIcon; - final Function(String)? onChanged; - - @override - Widget build(BuildContext context) { - return Padding( - padding: const EdgeInsets.only(top: 4.0), - child: TextField( - controller: inputController, - onChanged: onChanged, - keyboardType: TextInputType.text, - decoration: InputDecoration( - icon: leadingIcon, - label: label, - filled: true, - fillColor: Theme.of(context).colorScheme.secondaryContainer, - hintText: hint, - hintStyle: TextStyle( - color: Theme.of(context).colorScheme.secondary, - ), - floatingLabelStyle: MaterialStateTextStyle.resolveWith( - (states) => states.contains(MaterialState.focused) - ? TextStyle(color: Theme.of(context).colorScheme.primary) - : TextStyle(color: Theme.of(context).colorScheme.secondary), - ), - contentPadding: const EdgeInsets.symmetric( - vertical: 8.0, - horizontal: 16.0, - ), - border: OutlineInputBorder( - borderSide: BorderSide( - color: Theme.of(context).colorScheme.primary, - ), - borderRadius: BorderRadius.circular(10), - ), - focusedBorder: OutlineInputBorder( - borderSide: BorderSide( - color: Theme.of(context).colorScheme.primary, - width: 2.0, - ), - borderRadius: BorderRadius.circular(10), - ), - errorBorder: OutlineInputBorder( - borderSide: BorderSide( - color: Theme.of(context).colorScheme.error, - ), - borderRadius: BorderRadius.circular(10), - ), - enabledBorder: OutlineInputBorder( - borderSide: BorderSide( - color: Theme.of(context).colorScheme.primary, - ), - borderRadius: BorderRadius.circular(10), - ), - ), - ), - ); - } -} diff --git a/lib/ui/widgets/settingsView/settings_advanced_section.dart b/lib/ui/widgets/settingsView/settings_advanced_section.dart deleted file mode 100644 index 2d9be3fcb7..0000000000 --- a/lib/ui/widgets/settingsView/settings_advanced_section.dart +++ /dev/null @@ -1,105 +0,0 @@ -// ignore_for_file: prefer_const_constructors - -import 'package:flutter/material.dart'; -import 'package:flutter_i18n/widgets/I18nText.dart'; -import 'package:revanced_manager/ui/views/settings/settingsFragment/settings_manage_api_url.dart'; -import 'package:revanced_manager/ui/views/settings/settingsFragment/settings_manage_sources.dart'; -import 'package:revanced_manager/ui/views/settings/settings_viewmodel.dart'; -import 'package:revanced_manager/ui/widgets/settingsView/settings_enable_patches_selection.dart'; -import 'package:revanced_manager/ui/widgets/settingsView/settings_auto_update_patches.dart'; -import 'package:revanced_manager/ui/widgets/settingsView/settings_experimental_patches.dart'; -import 'package:revanced_manager/ui/widgets/settingsView/settings_experimental_universal_patches.dart'; -import 'package:revanced_manager/ui/widgets/settingsView/settings_section.dart'; -import 'package:revanced_manager/ui/widgets/shared/custom_material_button.dart'; - -final _settingsViewModel = SettingsViewModel(); - -class SAdvancedSection extends StatelessWidget { - const SAdvancedSection({super.key}); - - @override - Widget build(BuildContext context) { - return SettingsSection( - title: 'settingsView.advancedSectionTitle', - children: <Widget>[ - SManageApiUrlUI(), - SManageSourcesUI(), - // SManageKeystorePasswordUI(), - SAutoUpdatePatches(), - SEnablePatchesSelection(), - SExperimentalUniversalPatches(), - SExperimentalPatches(), - ListTile( - contentPadding: const EdgeInsets.symmetric(horizontal: 20.0), - title: I18nText( - 'settingsView.regenerateKeystoreLabel', - child: const Text( - '', - style: TextStyle( - fontSize: 20, - fontWeight: FontWeight.w500, - ), - ), - ), - subtitle: I18nText('settingsView.regenerateKeystoreHint'), - onTap: () => _showDeleteKeystoreDialog(context), - ), - ListTile( - contentPadding: const EdgeInsets.symmetric(horizontal: 20.0), - title: I18nText( - 'settingsView.deleteTempDirLabel', - child: const Text( - '', - style: TextStyle( - fontSize: 20, - fontWeight: FontWeight.w500, - ), - ), - ), - subtitle: I18nText('settingsView.deleteTempDirHint'), - onTap: () => _settingsViewModel.deleteTempDir(), - ), - ListTile( - contentPadding: const EdgeInsets.symmetric(horizontal: 20.0), - title: I18nText( - 'settingsView.deleteLogsLabel', - child: const Text( - '', - style: TextStyle( - fontSize: 20, - fontWeight: FontWeight.w500, - ), - ), - ), - subtitle: I18nText('settingsView.deleteLogsHint'), - onTap: () => _settingsViewModel.deleteLogs(), - ), - ], - ); - } - - Future<void> _showDeleteKeystoreDialog(context) { - return showDialog( - context: context, - builder: (context) => AlertDialog( - title: I18nText('settingsView.regenerateKeystoreDialogTitle'), - backgroundColor: Theme.of(context).colorScheme.secondaryContainer, - content: I18nText('settingsView.regenerateKeystoreDialogText'), - actions: <Widget>[ - CustomMaterialButton( - isFilled: false, - label: I18nText('noButton'), - onPressed: () => Navigator.of(context).pop(), - ), - CustomMaterialButton( - label: I18nText('yesButton'), - onPressed: () => { - Navigator.of(context).pop(), - _settingsViewModel.deleteKeystore(), - }, - ), - ], - ), - ); - } -} diff --git a/lib/ui/widgets/settingsView/settings_auto_update_patches.dart b/lib/ui/widgets/settingsView/settings_auto_update_patches.dart deleted file mode 100644 index 2063d658e6..0000000000 --- a/lib/ui/widgets/settingsView/settings_auto_update_patches.dart +++ /dev/null @@ -1,38 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_i18n/widgets/I18nText.dart'; -import 'package:revanced_manager/ui/views/settings/settings_viewmodel.dart'; - -class SAutoUpdatePatches extends StatefulWidget { - const SAutoUpdatePatches({super.key}); - - @override - State<SAutoUpdatePatches> createState() => _SAutoUpdatePatchesState(); -} - -final _settingsViewModel = SettingsViewModel(); - -class _SAutoUpdatePatchesState extends State<SAutoUpdatePatches> { - @override - Widget build(BuildContext context) { - return SwitchListTile( - contentPadding: const EdgeInsets.symmetric(horizontal: 20.0), - title: I18nText( - 'settingsView.autoUpdatePatchesLabel', - child: const Text( - '', - style: TextStyle( - fontSize: 20, - fontWeight: FontWeight.w500, - ), - ), - ), - subtitle: I18nText('settingsView.autoUpdatePatchesHint'), - value: _settingsViewModel.isPatchesAutoUpdate(), - onChanged: (value) { - setState(() { - _settingsViewModel.setPatchesAutoUpdate(value); - }); - }, - ); - } -} diff --git a/lib/ui/widgets/settingsView/settings_enable_patches_selection.dart b/lib/ui/widgets/settingsView/settings_enable_patches_selection.dart deleted file mode 100644 index a0c5b463ef..0000000000 --- a/lib/ui/widgets/settingsView/settings_enable_patches_selection.dart +++ /dev/null @@ -1,37 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_i18n/widgets/I18nText.dart'; -import 'package:revanced_manager/ui/views/settings/settings_viewmodel.dart'; - -class SEnablePatchesSelection extends StatefulWidget { - const SEnablePatchesSelection({super.key}); - - @override - State<SEnablePatchesSelection> createState() => _SEnablePatchesSelectionState(); -} - -final _settingsViewModel = SettingsViewModel(); - -class _SEnablePatchesSelectionState extends State<SEnablePatchesSelection> { - @override - Widget build(BuildContext context) { - return SwitchListTile( - contentPadding: const EdgeInsets.symmetric(horizontal: 20.0), - title: I18nText( - 'settingsView.enablePatchesSelectionLabel', - child: const Text( - '', - style: TextStyle( - fontSize: 20, - fontWeight: FontWeight.w500, - ), - ), - ), - subtitle: I18nText('settingsView.enablePatchesSelectionHint'), - value: _settingsViewModel.isPatchesChangeEnabled(), - onChanged: (value) async { - await _settingsViewModel.showPatchesChangeEnableDialog(value, context); - setState(() {}); - }, - ); - } -} diff --git a/lib/ui/widgets/settingsView/settings_experimental_patches.dart b/lib/ui/widgets/settingsView/settings_experimental_patches.dart deleted file mode 100644 index be704c73f5..0000000000 --- a/lib/ui/widgets/settingsView/settings_experimental_patches.dart +++ /dev/null @@ -1,49 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_i18n/widgets/I18nText.dart'; -import 'package:revanced_manager/ui/views/patcher/patcher_viewmodel.dart'; -import 'package:revanced_manager/ui/views/patches_selector/patches_selector_viewmodel.dart'; -import 'package:revanced_manager/ui/views/settings/settings_viewmodel.dart'; -import 'package:revanced_manager/utils/check_for_supported_patch.dart'; - -class SExperimentalPatches extends StatefulWidget { - const SExperimentalPatches({super.key}); - - @override - State<SExperimentalPatches> createState() => _SExperimentalPatchesState(); -} - -final _settingsViewModel = SettingsViewModel(); -final _patchesSelectorViewModel = PatchesSelectorViewModel(); -final _patcherViewModel = PatcherViewModel(); - -class _SExperimentalPatchesState extends State<SExperimentalPatches> { - @override - Widget build(BuildContext context) { - return SwitchListTile( - contentPadding: const EdgeInsets.symmetric(horizontal: 20.0), - title: I18nText( - 'settingsView.experimentalPatchesLabel', - child: const Text( - '', - style: TextStyle( - fontSize: 20, - fontWeight: FontWeight.w500, - ), - ), - ), - subtitle: I18nText('settingsView.experimentalPatchesHint'), - value: _settingsViewModel.areExperimentalPatchesEnabled(), - onChanged: (value) { - setState(() { - _settingsViewModel.useExperimentalPatches(value); - }); - if (!value) { - _patcherViewModel.selectedPatches - .removeWhere((patch) => !isPatchSupported(patch)); - _patchesSelectorViewModel.selectedPatches - .removeWhere((patch) => !isPatchSupported(patch)); - } - }, - ); - } -} diff --git a/lib/ui/widgets/settingsView/settings_experimental_universal_patches.dart b/lib/ui/widgets/settingsView/settings_experimental_universal_patches.dart deleted file mode 100644 index b8a5841250..0000000000 --- a/lib/ui/widgets/settingsView/settings_experimental_universal_patches.dart +++ /dev/null @@ -1,50 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_i18n/widgets/I18nText.dart'; -import 'package:revanced_manager/ui/views/patcher/patcher_viewmodel.dart'; -import 'package:revanced_manager/ui/views/patches_selector/patches_selector_viewmodel.dart'; -import 'package:revanced_manager/ui/views/settings/settings_viewmodel.dart'; - -class SExperimentalUniversalPatches extends StatefulWidget { - const SExperimentalUniversalPatches({super.key}); - - @override - State<SExperimentalUniversalPatches> createState() => - _SExperimentalUniversalPatchesState(); -} - -final _settingsViewModel = SettingsViewModel(); -final _patchesSelectorViewModel = PatchesSelectorViewModel(); -final _patcherViewModel = PatcherViewModel(); - -class _SExperimentalUniversalPatchesState - extends State<SExperimentalUniversalPatches> { - @override - Widget build(BuildContext context) { - return SwitchListTile( - contentPadding: const EdgeInsets.symmetric(horizontal: 20.0), - title: I18nText( - 'settingsView.experimentalUniversalPatchesLabel', - child: const Text( - '', - style: TextStyle( - fontSize: 20, - fontWeight: FontWeight.w500, - ), - ), - ), - subtitle: I18nText('settingsView.experimentalUniversalPatchesHint'), - value: _settingsViewModel.areUniversalPatchesEnabled(), - onChanged: (value) { - setState(() { - _settingsViewModel.showUniversalPatches(value); - }); - if (!value) { - _patcherViewModel.selectedPatches - .removeWhere((patch) => patch.compatiblePackages.isEmpty); - _patchesSelectorViewModel.selectedPatches - .removeWhere((patch) => patch.compatiblePackages.isEmpty); - } - }, - ); - } -} diff --git a/lib/ui/widgets/settingsView/settings_export_section.dart b/lib/ui/widgets/settingsView/settings_export_section.dart deleted file mode 100644 index bb69373989..0000000000 --- a/lib/ui/widgets/settingsView/settings_export_section.dart +++ /dev/null @@ -1,129 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_i18n/widgets/I18nText.dart'; -import 'package:revanced_manager/ui/views/settings/settingsFragment/settings_manage_keystore_password.dart'; -import 'package:revanced_manager/ui/views/settings/settings_viewmodel.dart'; -import 'package:revanced_manager/ui/widgets/settingsView/settings_section.dart'; -import 'package:revanced_manager/ui/widgets/shared/custom_material_button.dart'; - -final _settingsViewModel = SettingsViewModel(); - -class SExportSection extends StatelessWidget { - const SExportSection({super.key}); - - @override - Widget build(BuildContext context) { - return SettingsSection( - title: 'settingsView.exportSectionTitle', - children: <Widget>[ - ListTile( - contentPadding: const EdgeInsets.symmetric(horizontal: 20.0), - title: I18nText( - 'settingsView.exportPatchesLabel', - child: const Text( - '', - style: TextStyle( - fontSize: 20, - fontWeight: FontWeight.w500, - ), - ), - ), - subtitle: I18nText('settingsView.exportPatchesHint'), - onTap: () => _settingsViewModel.exportPatches(), - ), - ListTile( - contentPadding: const EdgeInsets.symmetric(horizontal: 20.0), - title: I18nText( - 'settingsView.importPatchesLabel', - child: const Text( - '', - style: TextStyle( - fontSize: 20, - fontWeight: FontWeight.w500, - ), - ), - ), - subtitle: I18nText('settingsView.importPatchesHint'), - onTap: () => _settingsViewModel.importPatches(context), - ), - ListTile( - contentPadding: const EdgeInsets.symmetric(horizontal: 20.0), - title: I18nText( - 'settingsView.exportKeystoreLabel', - child: const Text( - '', - style: TextStyle( - fontSize: 20, - fontWeight: FontWeight.w500, - ), - ), - ), - subtitle: I18nText('settingsView.exportKeystoreHint'), - onTap: () => _settingsViewModel.exportKeystore(), - ), - ListTile( - contentPadding: const EdgeInsets.symmetric(horizontal: 20.0), - title: I18nText( - 'settingsView.importKeystoreLabel', - child: const Text( - '', - style: TextStyle( - fontSize: 20, - fontWeight: FontWeight.w500, - ), - ), - ), - subtitle: I18nText('settingsView.importKeystoreHint'), - onTap: () async { - await _settingsViewModel.importKeystore(); - final sManageKeystorePassword = SManageKeystorePassword(); - if (context.mounted) { - sManageKeystorePassword.showKeystoreDialog(context); - } - }, - ), - ListTile( - contentPadding: const EdgeInsets.symmetric(horizontal: 20.0), - title: I18nText( - 'settingsView.resetStoredPatchesLabel', - child: const Text( - '', - style: TextStyle( - fontSize: 20, - fontWeight: FontWeight.w500, - ), - ), - ), - subtitle: I18nText('settingsView.resetStoredPatchesHint'), - onTap: () => _showResetStoredPatchesDialog(context), - ), - ], - ); - } - - Future<void> _showResetStoredPatchesDialog(context) { - return showDialog( - context: context, - builder: (context) => AlertDialog( - title: I18nText('settingsView.resetStoredPatchesDialogTitle'), - backgroundColor: Theme.of(context).colorScheme.secondaryContainer, - content: I18nText( - 'settingsView.resetStoredPatchesDialogText', - ), - actions: <Widget>[ - CustomMaterialButton( - isFilled: false, - label: I18nText('noButton'), - onPressed: () => Navigator.of(context).pop(), - ), - CustomMaterialButton( - label: I18nText('yesButton'), - onPressed: () => { - Navigator.of(context).pop(), - _settingsViewModel.resetSelectedPatches(), - }, - ), - ], - ), - ); - } -} diff --git a/lib/ui/widgets/settingsView/settings_info_section.dart b/lib/ui/widgets/settingsView/settings_info_section.dart deleted file mode 100644 index 0a71458101..0000000000 --- a/lib/ui/widgets/settingsView/settings_info_section.dart +++ /dev/null @@ -1,38 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_i18n/widgets/I18nText.dart'; -import 'package:revanced_manager/ui/views/settings/settings_viewmodel.dart'; -import 'package:revanced_manager/ui/widgets/settingsView/about_widget.dart'; -import 'package:revanced_manager/ui/widgets/settingsView/settings_section.dart'; - -final _settingsViewModel = SettingsViewModel(); - -class SInfoSection extends StatelessWidget { - const SInfoSection({super.key}); - - @override - Widget build(BuildContext context) { - return SettingsSection( - title: 'settingsView.infoSectionTitle', - children: <Widget>[ - ListTile( - contentPadding: const EdgeInsets.symmetric(horizontal: 20.0), - title: I18nText( - 'settingsView.logsLabel', - child: const Text( - '', - style: TextStyle( - fontSize: 20, - fontWeight: FontWeight.w500, - ), - ), - ), - subtitle: I18nText('settingsView.logsHint'), - onTap: () => _settingsViewModel.exportLogcatLogs(), - ), - const AboutWidget( - padding: EdgeInsets.symmetric(horizontal: 20.0), - ), - ], - ); - } -} diff --git a/lib/ui/widgets/settingsView/settings_section.dart b/lib/ui/widgets/settingsView/settings_section.dart deleted file mode 100644 index 74570a14af..0000000000 --- a/lib/ui/widgets/settingsView/settings_section.dart +++ /dev/null @@ -1,37 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_i18n/flutter_i18n.dart'; - -class SettingsSection extends StatelessWidget { - const SettingsSection({ - Key? key, - required this.title, - required this.children, - }) : super(key: key); - final String title; - final List<Widget> children; - - @override - Widget build(BuildContext context) { - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: <Widget>[ - Container( - padding: const EdgeInsets.only(top: 16.0, bottom: 10.0, left: 20.0), - child: I18nText( - title, - child: Text( - '', - style: TextStyle( - color: Theme.of(context).colorScheme.primary, - ), - ), - ), - ), - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: children, - ), - ], - ); - } -} diff --git a/lib/ui/widgets/settingsView/settings_team_section.dart b/lib/ui/widgets/settingsView/settings_team_section.dart deleted file mode 100644 index aa2d81a6f3..0000000000 --- a/lib/ui/widgets/settingsView/settings_team_section.dart +++ /dev/null @@ -1,38 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_i18n/widgets/I18nText.dart'; -import 'package:revanced_manager/ui/views/settings/settings_viewmodel.dart'; -import 'package:revanced_manager/ui/widgets/settingsView/settings_section.dart'; -import 'package:revanced_manager/ui/widgets/settingsView/social_media_widget.dart'; - -final _settingsViewModel = SettingsViewModel(); - -class STeamSection extends StatelessWidget { - const STeamSection({super.key}); - - @override - Widget build(BuildContext context) { - return SettingsSection( - title: 'settingsView.teamSectionTitle', - children: <Widget>[ - ListTile( - contentPadding: const EdgeInsets.symmetric(horizontal: 20.0), - title: I18nText( - 'settingsView.contributorsLabel', - child: const Text( - '', - style: TextStyle( - fontSize: 20, - fontWeight: FontWeight.w500, - ), - ), - ), - subtitle: I18nText('settingsView.contributorsHint'), - onTap: () => _settingsViewModel.navigateToContributors(), - ), - const SocialMediaWidget( - padding: EdgeInsets.symmetric(horizontal: 20.0), - ), - ], - ); - } -} diff --git a/lib/ui/widgets/settingsView/settings_tile_dialog.dart b/lib/ui/widgets/settingsView/settings_tile_dialog.dart deleted file mode 100644 index ce3a817d70..0000000000 --- a/lib/ui/widgets/settingsView/settings_tile_dialog.dart +++ /dev/null @@ -1,35 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_i18n/flutter_i18n.dart'; - -class SettingsTileDialog extends StatelessWidget { - const SettingsTileDialog({ - Key? key, - required this.title, - required this.subtitle, - required this.onTap, - this.padding, - }) : super(key: key); - final String title; - final String subtitle; - final Function()? onTap; - final EdgeInsetsGeometry? padding; - - @override - Widget build(BuildContext context) { - return ListTile( - contentPadding: padding ?? EdgeInsets.zero, - title: I18nText( - title, - child: const Text( - '', - style: TextStyle( - fontSize: 20, - fontWeight: FontWeight.w500, - ), - ), - ), - subtitle: I18nText(subtitle), - onTap: onTap, - ); - } -} diff --git a/lib/ui/widgets/settingsView/social_media_item.dart b/lib/ui/widgets/settingsView/social_media_item.dart deleted file mode 100644 index 86971a2762..0000000000 --- a/lib/ui/widgets/settingsView/social_media_item.dart +++ /dev/null @@ -1,50 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:url_launcher/url_launcher.dart'; - -class SocialMediaItem extends StatelessWidget { - const SocialMediaItem({ - Key? key, - this.icon, - required this.title, - this.subtitle, - this.url, - }) : super(key: key); - final Widget? icon; - final Widget title; - final Widget? subtitle; - final String? url; - - @override - Widget build(BuildContext context) { - return ListTile( - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16.0)), - contentPadding: EdgeInsets.zero, - leading: SizedBox( - width: 48.0, - child: Center( - child: icon, - ), - ), - title: DefaultTextStyle( - style: Theme.of(context).textTheme.bodyMedium!.copyWith( - color: Theme.of(context).colorScheme.onSecondaryContainer, - ), - child: title, - ), - subtitle: subtitle != null - ? DefaultTextStyle( - style: Theme.of(context).textTheme.bodyMedium!.copyWith( - color: Theme.of(context).colorScheme.primary, - ), - child: subtitle!, - ) - : null, - onTap: () => url != null - ? launchUrl( - Uri.parse(url!), - mode: LaunchMode.externalApplication, - ) - : null, - ); - } -} diff --git a/lib/ui/widgets/settingsView/social_media_widget.dart b/lib/ui/widgets/settingsView/social_media_widget.dart deleted file mode 100644 index 73a6a2eebe..0000000000 --- a/lib/ui/widgets/settingsView/social_media_widget.dart +++ /dev/null @@ -1,88 +0,0 @@ -import 'package:expandable/expandable.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_i18n/flutter_i18n.dart'; -import 'package:font_awesome_flutter/font_awesome_flutter.dart'; -import 'package:revanced_manager/ui/widgets/settingsView/social_media_item.dart'; -import 'package:revanced_manager/ui/widgets/shared/custom_card.dart'; - -class SocialMediaWidget extends StatelessWidget { - const SocialMediaWidget({ - Key? key, - this.padding, - }) : super(key: key); - final EdgeInsetsGeometry? padding; - - @override - Widget build(BuildContext context) { - return ExpandablePanel( - theme: ExpandableThemeData( - hasIcon: true, - iconColor: Theme.of(context).iconTheme.color, - iconPadding: const EdgeInsets.symmetric(vertical: 16.0) - .add(padding ?? EdgeInsets.zero) - .resolve(Directionality.of(context)), - animationDuration: const Duration(milliseconds: 400), - ), - header: ListTile( - contentPadding: padding ?? EdgeInsets.zero, - title: I18nText( - 'socialMediaCard.widgetTitle', - child: const Text( - '', - style: TextStyle( - fontSize: 20, - fontWeight: FontWeight.w500, - ), - ), - ), - subtitle: I18nText('socialMediaCard.widgetSubtitle'), - ), - expanded: Padding( - padding: padding ?? EdgeInsets.zero, - child: const CustomCard( - child: Column( - children: <Widget>[ - SocialMediaItem( - icon: FaIcon(FontAwesomeIcons.github), - title: Text('GitHub'), - subtitle: Text('github.com/revanced'), - url: 'https://github.com/revanced', - ), - SocialMediaItem( - icon: FaIcon(FontAwesomeIcons.discord), - title: Text('Discord'), - subtitle: Text('discord.gg/revanced'), - url: 'https://discord.gg/rF2YcEjcrT', - ), - SocialMediaItem( - icon: FaIcon(FontAwesomeIcons.telegram), - title: Text('Telegram'), - subtitle: Text('t.me/app_revanced'), - url: 'https://t.me/app_revanced', - ), - SocialMediaItem( - icon: FaIcon(FontAwesomeIcons.reddit), - title: Text('Reddit'), - subtitle: Text('r/revancedapp'), - url: 'https://reddit.com/r/revancedapp', - ), - SocialMediaItem( - icon: FaIcon(FontAwesomeIcons.twitter), - title: Text('Twitter'), - subtitle: Text('@revancedapp'), - url: 'https://twitter.com/revancedapp', - ), - SocialMediaItem( - icon: FaIcon(FontAwesomeIcons.youtube), - title: Text('YouTube'), - subtitle: Text('youtube.com/revanced'), - url: 'https://youtube.com/revanced', - ), - ], - ), - ), - ), - collapsed: const SizedBox(), - ); - } -} diff --git a/lib/ui/widgets/shared/application_item.dart b/lib/ui/widgets/shared/application_item.dart deleted file mode 100644 index 42eee351b5..0000000000 --- a/lib/ui/widgets/shared/application_item.dart +++ /dev/null @@ -1,165 +0,0 @@ -import 'dart:typed_data'; - -import 'package:expandable/expandable.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_i18n/flutter_i18n.dart'; -import 'package:revanced_manager/ui/widgets/shared/custom_card.dart'; -import 'package:revanced_manager/ui/widgets/shared/custom_material_button.dart'; -import 'package:timeago/timeago.dart'; - -class ApplicationItem extends StatefulWidget { - const ApplicationItem({ - Key? key, - required this.icon, - required this.name, - required this.patchDate, - required this.changelog, - required this.isUpdatableApp, - required this.onPressed, - }) : super(key: key); - final Uint8List icon; - final String name; - final DateTime patchDate; - final List<String> changelog; - final bool isUpdatableApp; - final Function() onPressed; - - @override - State<ApplicationItem> createState() => _ApplicationItemState(); -} - -class _ApplicationItemState extends State<ApplicationItem> - with TickerProviderStateMixin { - late AnimationController _animationController; - - @override - void initState() { - super.initState(); - _animationController = AnimationController( - vsync: this, - duration: const Duration(milliseconds: 300), - ); - } - - @override - void dispose() { - _animationController.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - final ExpandableController expController = ExpandableController(); - return Container( - margin: const EdgeInsets.only(bottom: 16.0), - child: CustomCard( - onTap: () { - expController.toggle(); - _animationController.isCompleted - ? _animationController.reverse() - : _animationController.forward(); - }, - child: ExpandablePanel( - controller: expController, - theme: const ExpandableThemeData( - inkWellBorderRadius: BorderRadius.all(Radius.circular(16)), - tapBodyToCollapse: false, - tapBodyToExpand: false, - tapHeaderToExpand: false, - hasIcon: false, - animationDuration: Duration(milliseconds: 450), - ), - header: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Flexible( - child: Row( - children: [ - SizedBox( - width: 40, - child: Image.memory(widget.icon, height: 40, width: 40), - ), - const SizedBox(width: 19), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: <Widget>[ - Text( - widget.name, - maxLines: 1, - overflow: TextOverflow.ellipsis, - style: const TextStyle( - fontSize: 16, - fontWeight: FontWeight.w500, - ), - ), - Text( - format(widget.patchDate), - maxLines: 1, - overflow: TextOverflow.ellipsis, - style: const TextStyle( - fontSize: 16, - fontWeight: FontWeight.w500, - ), - ), - ], - ), - ), - ], - ), - ), - Row( - children: [ - RotationTransition( - turns: Tween(begin: 0.0, end: 0.50) - .animate(_animationController), - child: const Padding( - padding: EdgeInsets.all(8.0), - child: Icon(Icons.arrow_drop_down), - ), - ), - const SizedBox(width: 8), - Column( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.end, - children: <Widget>[ - CustomMaterialButton( - label: widget.isUpdatableApp - ? I18nText('applicationItem.patchButton') - : I18nText('applicationItem.infoButton'), - onPressed: widget.onPressed, - ), - ], - ), - ], - ), - ], - ), - collapsed: const SizedBox(), - expanded: Padding( - padding: const EdgeInsets.only( - top: 16.0, - left: 4.0, - right: 4.0, - bottom: 4.0, - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: <Widget>[ - I18nText( - 'applicationItem.changelogLabel', - child: const Text( - '', - style: TextStyle(fontWeight: FontWeight.w700), - ), - ), - const SizedBox(height: 4), - Text('\u2022 ${widget.changelog.join('\n\u2022 ')}'), - ], - ), - ), - ), - ), - ); - } -} diff --git a/lib/ui/widgets/shared/custom_card.dart b/lib/ui/widgets/shared/custom_card.dart deleted file mode 100644 index 34b3c72860..0000000000 --- a/lib/ui/widgets/shared/custom_card.dart +++ /dev/null @@ -1,37 +0,0 @@ -import 'package:flutter/material.dart'; - -class CustomCard extends StatelessWidget { - const CustomCard({ - Key? key, - this.isFilled = true, - required this.child, - this.onTap, - this.padding, - this.backgroundColor, - }) : super(key: key); - final bool isFilled; - final Widget child; - final Function()? onTap; - final EdgeInsetsGeometry? padding; - final Color? backgroundColor; - - @override - Widget build(BuildContext context) { - return Material( - type: isFilled ? MaterialType.card : MaterialType.transparency, - color: isFilled - ? backgroundColor?.withOpacity(0.4) ?? - Theme.of(context).colorScheme.secondaryContainer.withOpacity(0.4) - : backgroundColor ?? Colors.transparent, - borderRadius: BorderRadius.circular(16), - child: InkWell( - onTap: onTap, - borderRadius: BorderRadius.circular(16), - child: Padding( - padding: padding ?? const EdgeInsets.all(20.0), - child: child, - ), - ), - ); - } -} diff --git a/lib/ui/widgets/shared/custom_chip.dart b/lib/ui/widgets/shared/custom_chip.dart deleted file mode 100644 index 8f3bb41831..0000000000 --- a/lib/ui/widgets/shared/custom_chip.dart +++ /dev/null @@ -1,41 +0,0 @@ -import 'package:flutter/material.dart'; - -class CustomChip extends StatelessWidget { - const CustomChip({ - Key? key, - required this.label, - this.isSelected = false, - this.onSelected, - }) : super(key: key); - final Widget label; - final bool isSelected; - final Function(bool)? onSelected; - - @override - Widget build(BuildContext context) { - return RawChip( - showCheckmark: false, - label: label, - selected: isSelected, - labelStyle: Theme.of(context).textTheme.titleSmall!.copyWith( - color: isSelected - ? Theme.of(context).colorScheme.primary - : Theme.of(context).colorScheme.secondary, - fontWeight: FontWeight.w500, - ), - backgroundColor: Colors.transparent, - selectedColor: Theme.of(context).colorScheme.secondaryContainer, - padding: const EdgeInsets.all(10), - onSelected: onSelected, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(12), - side: isSelected - ? BorderSide.none - : BorderSide( - width: 0.2, - color: Theme.of(context).colorScheme.secondary, - ), - ), - ); - } -} diff --git a/lib/ui/widgets/shared/custom_material_button.dart b/lib/ui/widgets/shared/custom_material_button.dart deleted file mode 100644 index 2ff80e4cf2..0000000000 --- a/lib/ui/widgets/shared/custom_material_button.dart +++ /dev/null @@ -1,126 +0,0 @@ -import 'package:flutter/material.dart'; - -class CustomMaterialButton extends StatelessWidget { - const CustomMaterialButton({ - Key? key, - required this.label, - this.isFilled = true, - this.isExpanded = false, - required this.onPressed, - }) : super(key: key); - final Widget label; - final bool isFilled; - final bool isExpanded; - final Function()? onPressed; - - @override - Widget build(BuildContext context) { - return TextButton( - style: ButtonStyle( - padding: MaterialStateProperty.all( - isExpanded - ? const EdgeInsets.symmetric(horizontal: 24, vertical: 12) - : const EdgeInsets.symmetric(horizontal: 20, vertical: 12), - ), - shape: MaterialStateProperty.all( - StadiumBorder( - side: isFilled - ? BorderSide.none - : BorderSide( - color: Theme.of(context).colorScheme.primary, - ), - ), - ), - backgroundColor: MaterialStateProperty.all( - isFilled ? Theme.of(context).colorScheme.primary : Colors.transparent, - ), - foregroundColor: MaterialStateProperty.all( - isFilled - ? Theme.of(context).colorScheme.surface - : Theme.of(context).colorScheme.primary, - ), - ), - onPressed: onPressed, - child: label, - ); - } -} - -// ignore: must_be_immutable -class TimerButton extends StatefulWidget { - TimerButton({ - Key? key, - required this.seconds, - required this.isRunning, - required this.onTimerEnd, - this.label = const Text(''), - this.isFilled = true, - }) : super(key: key); - Widget label; - bool isFilled; - int seconds; - final bool isRunning; - final Function()? onTimerEnd; - - @override - State<TimerButton> createState() => _TimerButtonState(); -} - -class _TimerButtonState extends State<TimerButton> { - void timer(int seconds) { - Future.delayed(const Duration(seconds: 1), () { - if (seconds > 0) { - setState(() { - seconds--; - }); - timer(seconds); - } else { - widget.onTimerEnd!(); - } - }); - } - - @override - void initState() { - //decrement seconds - if (widget.isRunning) { - timer(widget.seconds); - } - super.initState(); - } - - @override - Widget build(BuildContext build) { - return TextButton( - style: ButtonStyle( - shape: MaterialStateProperty.all( - StadiumBorder( - side: widget.isFilled - ? BorderSide.none - : BorderSide( - color: Theme.of(context).colorScheme.primary, - ), - ), - ), - backgroundColor: MaterialStateProperty.all( - widget.isFilled - ? Theme.of(context).colorScheme.primary - : Colors.transparent, - ), - foregroundColor: MaterialStateProperty.all( - widget.isFilled - ? Theme.of(context).colorScheme.surface - : Theme.of(context).colorScheme.primary, - ), - ), - onPressed: widget.isRunning ? null : widget.onTimerEnd, - child: Text( - widget.isRunning ? '${widget.seconds}' : 'Install', - style: const TextStyle( - fontSize: 16, - fontWeight: FontWeight.w600, - ), - ), - ); - } -} diff --git a/lib/ui/widgets/shared/custom_popup_menu.dart b/lib/ui/widgets/shared/custom_popup_menu.dart deleted file mode 100644 index aaf2412563..0000000000 --- a/lib/ui/widgets/shared/custom_popup_menu.dart +++ /dev/null @@ -1,39 +0,0 @@ -import 'package:flutter/material.dart'; - -class CustomPopupMenu extends StatelessWidget { - const CustomPopupMenu({ - Key? key, - required this.onSelected, - required this.children, - }) : super(key: key); - final Function(dynamic) onSelected; - final Map<int, Widget> children; - - @override - Widget build(BuildContext context) { - return Theme( - data: Theme.of(context).copyWith(useMaterial3: false), - child: PopupMenuButton<int>( - icon: Icon( - Icons.more_vert, - color: Theme.of(context).colorScheme.secondary, - ), - onSelected: onSelected, - itemBuilder: (context) => children.entries - .map( - (entry) => PopupMenuItem<int>( - padding: const EdgeInsets.all(16.0).copyWith(right: 20), - value: entry.key, - child: entry.value, - ), - ) - .toList(), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(24), - ), - color: Theme.of(context).colorScheme.secondaryContainer, - position: PopupMenuPosition.under, - ), - ); - } -} diff --git a/lib/ui/widgets/shared/custom_sliver_app_bar.dart b/lib/ui/widgets/shared/custom_sliver_app_bar.dart deleted file mode 100644 index 144bd6ab8e..0000000000 --- a/lib/ui/widgets/shared/custom_sliver_app_bar.dart +++ /dev/null @@ -1,50 +0,0 @@ -import 'package:flutter/material.dart'; - -class CustomSliverAppBar extends StatelessWidget { - const CustomSliverAppBar({ - Key? key, - required this.title, - this.actions, - this.bottom, - this.isMainView = false, - this.onBackButtonPressed, - }) : super(key: key); - final Widget title; - final List<Widget>? actions; - final PreferredSizeWidget? bottom; - final bool isMainView; - final Function()? onBackButtonPressed; - - @override - Widget build(BuildContext context) { - return SliverAppBar( - pinned: true, - expandedHeight: 100.0, - automaticallyImplyLeading: !isMainView, - flexibleSpace: FlexibleSpaceBar( - titlePadding: EdgeInsets.only( - bottom: bottom != null ? 16.0 : 14.0, - left: isMainView ? 20.0 : 55.0, - ), - title: title, - ), - leading: isMainView - ? null - : IconButton( - icon: Icon( - Icons.arrow_back, - color: Theme.of(context).textTheme.titleLarge!.color, - ), - onPressed: - onBackButtonPressed ?? () => Navigator.of(context).pop(), - ), - backgroundColor: MaterialStateColor.resolveWith( - (states) => states.contains(MaterialState.scrolledUnder) - ? Theme.of(context).colorScheme.surface - : Theme.of(context).canvasColor, - ), - actions: actions, - bottom: bottom, - ); - } -} diff --git a/lib/ui/widgets/shared/open_container_wrapper.dart b/lib/ui/widgets/shared/open_container_wrapper.dart deleted file mode 100644 index f5b1c642b9..0000000000 --- a/lib/ui/widgets/shared/open_container_wrapper.dart +++ /dev/null @@ -1,26 +0,0 @@ -import 'package:animations/animations.dart'; -import 'package:flutter/material.dart'; - -class OpenContainerWrapper extends StatelessWidget { - const OpenContainerWrapper({ - Key? key, - required this.openBuilder, - required this.closedBuilder, - }) : super(key: key); - final OpenContainerBuilder openBuilder; - final CloseContainerBuilder closedBuilder; - - @override - Widget build(BuildContext context) { - return OpenContainer( - openBuilder: openBuilder, - closedBuilder: closedBuilder, - transitionDuration: const Duration(milliseconds: 400), - openColor: Theme.of(context).colorScheme.primary, - closedColor: Colors.transparent, - closedShape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(16), - ), - ); - } -} diff --git a/lib/ui/widgets/shared/search_bar.dart b/lib/ui/widgets/shared/search_bar.dart deleted file mode 100644 index e48e3031f0..0000000000 --- a/lib/ui/widgets/shared/search_bar.dart +++ /dev/null @@ -1,84 +0,0 @@ -import 'package:flutter/material.dart'; - -class SearchBar extends StatefulWidget { - const SearchBar({ - Key? key, - required this.hintText, - this.showSelectIcon = false, - this.onSelectAll, - required this.onQueryChanged, - }) : super(key: key); - final String? hintText; - final bool showSelectIcon; - final Function(bool)? onSelectAll; - - final Function(String) onQueryChanged; - - @override - State<SearchBar> createState() => _SearchBarState(); -} - -class _SearchBarState extends State<SearchBar> { - final TextEditingController _textController = TextEditingController(); - bool _toggleSelectAll = false; - - @override - Widget build(BuildContext context) { - return Container( - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(48), - color: Theme.of(context).colorScheme.secondaryContainer, - ), - child: Row( - children: <Widget>[ - Expanded( - child: TextFormField( - onChanged: widget.onQueryChanged, - controller: _textController, - style: TextStyle( - color: Theme.of(context).colorScheme.secondary, - ), - decoration: InputDecoration( - filled: true, - fillColor: Theme.of(context).colorScheme.secondaryContainer, - contentPadding: const EdgeInsets.all(12.0), - hintText: widget.hintText, - prefixIcon: Icon( - Icons.search, - color: Theme.of(context).colorScheme.secondary, - ), - suffixIcon: _textController.text.isNotEmpty - ? IconButton( - icon: const Icon(Icons.clear), - onPressed: () { - _textController.clear(); - widget.onQueryChanged(''); - }, - ) - : widget.showSelectIcon - ? IconButton( - icon: _toggleSelectAll - ? const Icon(Icons.deselect) - : const Icon(Icons.select_all), - onPressed: widget.onSelectAll != null - ? () { - setState(() { - _toggleSelectAll = !_toggleSelectAll; - }); - widget.onSelectAll!(_toggleSelectAll); - } - : () => {}, - ) - : null, - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(100), - borderSide: BorderSide.none, - ), - ), - ), - ), - ], - ), - ); - } -} diff --git a/lib/utils/about_info.dart b/lib/utils/about_info.dart deleted file mode 100644 index 0b7dcffc47..0000000000 --- a/lib/utils/about_info.dart +++ /dev/null @@ -1,17 +0,0 @@ -import 'package:device_info_plus/device_info_plus.dart'; -import 'package:flutter/foundation.dart'; -import 'package:package_info_plus/package_info_plus.dart'; - -class AboutInfo { - static Future<Map<String, dynamic>> getInfo() async { - final packageInfo = await PackageInfo.fromPlatform(); - final info = await DeviceInfoPlugin().androidInfo; - return { - 'version': packageInfo.version, - 'flavor': kReleaseMode ? 'release' : 'debug', - 'model': info.model, - 'androidVersion': info.version.release, - 'supportedArch': info.supportedAbis, - }; - } -} diff --git a/lib/utils/check_for_supported_patch.dart b/lib/utils/check_for_supported_patch.dart deleted file mode 100644 index 11c665d076..0000000000 --- a/lib/utils/check_for_supported_patch.dart +++ /dev/null @@ -1,14 +0,0 @@ -import 'package:revanced_manager/app/app.locator.dart'; -import 'package:revanced_manager/models/patch.dart'; -import 'package:revanced_manager/models/patched_application.dart'; -import 'package:revanced_manager/ui/views/patcher/patcher_viewmodel.dart'; - -bool isPatchSupported(Patch patch) { - final PatchedApplication app = locator<PatcherViewModel>().selectedApp!; - return patch.compatiblePackages.isEmpty || - patch.compatiblePackages.any( - (pack) => - pack.name == app.packageName && - (pack.versions.isEmpty || pack.versions.contains(app.version)), - ); -} diff --git a/lib/utils/string.dart b/lib/utils/string.dart deleted file mode 100644 index 47e5426449..0000000000 --- a/lib/utils/string.dart +++ /dev/null @@ -1,8 +0,0 @@ -extension StringCasingExtension on String { - String toCapitalized() => - length > 0 ? '${this[0].toUpperCase()}${substring(1).toLowerCase()}' : ''; - String toTitleCase() => replaceAll(RegExp(' +'), ' ') - .split(' ') - .map((str) => str.toCapitalized()) - .join(' '); -} diff --git a/package-lock.json b/package-lock.json deleted file mode 100644 index 04a34f64ca..0000000000 --- a/package-lock.json +++ /dev/null @@ -1,7179 +0,0 @@ -{ - "name": "revanced-manager", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "devDependencies": { - "@saithodev/semantic-release-backmerge": "^3.1.0", - "@semantic-release/changelog": "^6.0.3", - "@semantic-release/git": "^10.0.1", - "semantic-release": "^21.0.1", - "semantic-release-export-data": "^1.0.1", - "semantic-release-flutter-plugin": "^1.1.2" - } - }, - "node_modules/@actions/core": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/@actions/core/-/core-1.10.0.tgz", - "integrity": "sha512-2aZDDa3zrrZbP5ZYg159sNoLRb61nQ7awl5pSvIq5Qpj81vwDzdMRKzkWJGJuwVvWpvZKx7vspJALyvaaIQyug==", - "dev": true, - "dependencies": { - "@actions/http-client": "^2.0.1", - "uuid": "^8.3.2" - } - }, - "node_modules/@actions/http-client": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@actions/http-client/-/http-client-2.1.0.tgz", - "integrity": "sha512-BonhODnXr3amchh4qkmjPMUO8mFi/zLaaCeCAJZqch8iQqyDnVIkySjB38VHAC8IJ+bnlgfOqlhpyCUZHlQsqw==", - "dev": true, - "dependencies": { - "tunnel": "^0.0.6" - } - }, - "node_modules/@babel/code-frame": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.18.6.tgz", - "integrity": "sha512-TDCmlK5eOvH+eH7cdAFlNXeVJqWIQ7gW9tY1GJIpUtFb6CmjVyq2VM3u71bOyR8CRihcCgMUYoDNyLXao3+70Q==", - "dev": true, - "dependencies": { - "@babel/highlight": "^7.18.6" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-validator-identifier": { - "version": "7.19.1", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.19.1.tgz", - "integrity": "sha512-awrNfaMtnHUr653GgGEs++LlAvW6w+DcPrOliSMXWCKo597CwL5Acf/wWdNkf/tfEQE3mjkeD1YOVZOUV/od1w==", - "dev": true, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/highlight": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.18.6.tgz", - "integrity": "sha512-u7stbOuYjaPezCuLj29hNW1v64M2Md2qupEKP1fHc7WdOA3DgLh37suiSrZYY7haUB7iBeQZ9P1uiRF359do3g==", - "dev": true, - "dependencies": { - "@babel/helper-validator-identifier": "^7.18.6", - "chalk": "^2.0.0", - "js-tokens": "^4.0.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/highlight/node_modules/ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dev": true, - "dependencies": { - "color-convert": "^1.9.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/@babel/highlight/node_modules/chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, - "dependencies": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/@babel/highlight/node_modules/color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "dev": true, - "dependencies": { - "color-name": "1.1.3" - } - }, - "node_modules/@babel/highlight/node_modules/color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", - "dev": true - }, - "node_modules/@babel/highlight/node_modules/escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", - "dev": true, - "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/@babel/highlight/node_modules/has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/@babel/highlight/node_modules/supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, - "dependencies": { - "has-flag": "^3.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/@colors/colors": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz", - "integrity": "sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==", - "dev": true, - "optional": true, - "engines": { - "node": ">=0.1.90" - } - }, - "node_modules/@nodelib/fs.scandir": { - "version": "2.1.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", - "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", - "dev": true, - "dependencies": { - "@nodelib/fs.stat": "2.0.5", - "run-parallel": "^1.1.9" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/@nodelib/fs.stat": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", - "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", - "dev": true, - "engines": { - "node": ">= 8" - } - }, - "node_modules/@nodelib/fs.walk": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", - "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", - "dev": true, - "dependencies": { - "@nodelib/fs.scandir": "2.1.5", - "fastq": "^1.6.0" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/@octokit/auth-token": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/@octokit/auth-token/-/auth-token-3.0.3.tgz", - "integrity": "sha512-/aFM2M4HVDBT/jjDBa84sJniv1t9Gm/rLkalaz9htOm+L+8JMj1k9w0CkUdcxNyNxZPlTxKPVko+m1VlM58ZVA==", - "dev": true, - "dependencies": { - "@octokit/types": "^9.0.0" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/@octokit/core": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/@octokit/core/-/core-4.2.0.tgz", - "integrity": "sha512-AgvDRUg3COpR82P7PBdGZF/NNqGmtMq2NiPqeSsDIeCfYFOZ9gddqWNQHnFdEUf+YwOj4aZYmJnlPp7OXmDIDg==", - "dev": true, - "dependencies": { - "@octokit/auth-token": "^3.0.0", - "@octokit/graphql": "^5.0.0", - "@octokit/request": "^6.0.0", - "@octokit/request-error": "^3.0.0", - "@octokit/types": "^9.0.0", - "before-after-hook": "^2.2.0", - "universal-user-agent": "^6.0.0" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/@octokit/endpoint": { - "version": "7.0.5", - "resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-7.0.5.tgz", - "integrity": "sha512-LG4o4HMY1Xoaec87IqQ41TQ+glvIeTKqfjkCEmt5AIwDZJwQeVZFIEYXrYY6yLwK+pAScb9Gj4q+Nz2qSw1roA==", - "dev": true, - "dependencies": { - "@octokit/types": "^9.0.0", - "is-plain-object": "^5.0.0", - "universal-user-agent": "^6.0.0" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/@octokit/graphql": { - "version": "5.0.5", - "resolved": "https://registry.npmjs.org/@octokit/graphql/-/graphql-5.0.5.tgz", - "integrity": "sha512-Qwfvh3xdqKtIznjX9lz2D458r7dJPP8l6r4GQkIdWQouZwHQK0mVT88uwiU2bdTU2OtT1uOlKpRciUWldpG0yQ==", - "dev": true, - "dependencies": { - "@octokit/request": "^6.0.0", - "@octokit/types": "^9.0.0", - "universal-user-agent": "^6.0.0" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/@octokit/openapi-types": { - "version": "16.0.0", - "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-16.0.0.tgz", - "integrity": "sha512-JbFWOqTJVLHZSUUoF4FzAZKYtqdxWu9Z5m2QQnOyEa04fOFljvyh7D3GYKbfuaSWisqehImiVIMG4eyJeP5VEA==", - "dev": true - }, - "node_modules/@octokit/plugin-paginate-rest": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-6.0.0.tgz", - "integrity": "sha512-Sq5VU1PfT6/JyuXPyt04KZNVsFOSBaYOAq2QRZUwzVlI10KFvcbUo8lR258AAQL1Et60b0WuVik+zOWKLuDZxw==", - "dev": true, - "dependencies": { - "@octokit/types": "^9.0.0" - }, - "engines": { - "node": ">= 14" - }, - "peerDependencies": { - "@octokit/core": ">=4" - } - }, - "node_modules/@octokit/plugin-request-log": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@octokit/plugin-request-log/-/plugin-request-log-1.0.4.tgz", - "integrity": "sha512-mLUsMkgP7K/cnFEw07kWqXGF5LKrOkD+lhCrKvPHXWDywAwuDUeDwWBpc69XK3pNX0uKiVt8g5z96PJ6z9xCFA==", - "dev": true, - "peerDependencies": { - "@octokit/core": ">=3" - } - }, - "node_modules/@octokit/plugin-rest-endpoint-methods": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/@octokit/plugin-rest-endpoint-methods/-/plugin-rest-endpoint-methods-7.0.1.tgz", - "integrity": "sha512-pnCaLwZBudK5xCdrR823xHGNgqOzRnJ/mpC/76YPpNP7DybdsJtP7mdOwh+wYZxK5jqeQuhu59ogMI4NRlBUvA==", - "dev": true, - "dependencies": { - "@octokit/types": "^9.0.0", - "deprecation": "^2.3.1" - }, - "engines": { - "node": ">= 14" - }, - "peerDependencies": { - "@octokit/core": ">=3" - } - }, - "node_modules/@octokit/request": { - "version": "6.2.3", - "resolved": "https://registry.npmjs.org/@octokit/request/-/request-6.2.3.tgz", - "integrity": "sha512-TNAodj5yNzrrZ/VxP+H5HiYaZep0H3GU0O7PaF+fhDrt8FPrnkei9Aal/txsN/1P7V3CPiThG0tIvpPDYUsyAA==", - "dev": true, - "dependencies": { - "@octokit/endpoint": "^7.0.0", - "@octokit/request-error": "^3.0.0", - "@octokit/types": "^9.0.0", - "is-plain-object": "^5.0.0", - "node-fetch": "^2.6.7", - "universal-user-agent": "^6.0.0" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/@octokit/request-error": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-3.0.3.tgz", - "integrity": "sha512-crqw3V5Iy2uOU5Np+8M/YexTlT8zxCfI+qu+LxUB7SZpje4Qmx3mub5DfEKSO8Ylyk0aogi6TYdf6kxzh2BguQ==", - "dev": true, - "dependencies": { - "@octokit/types": "^9.0.0", - "deprecation": "^2.0.0", - "once": "^1.4.0" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/@octokit/rest": { - "version": "19.0.7", - "resolved": "https://registry.npmjs.org/@octokit/rest/-/rest-19.0.7.tgz", - "integrity": "sha512-HRtSfjrWmWVNp2uAkEpQnuGMJsu/+dBr47dRc5QVgsCbnIc1+GFEaoKBWkYG+zjrsHpSqcAElMio+n10c0b5JA==", - "dev": true, - "dependencies": { - "@octokit/core": "^4.1.0", - "@octokit/plugin-paginate-rest": "^6.0.0", - "@octokit/plugin-request-log": "^1.0.4", - "@octokit/plugin-rest-endpoint-methods": "^7.0.0" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/@octokit/types": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/@octokit/types/-/types-9.0.0.tgz", - "integrity": "sha512-LUewfj94xCMH2rbD5YJ+6AQ4AVjFYTgpp6rboWM5T7N3IsIF65SBEOVcYMGAEzO/kKNiNaW4LoWtoThOhH06gw==", - "dev": true, - "dependencies": { - "@octokit/openapi-types": "^16.0.0" - } - }, - "node_modules/@pnpm/config.env-replace": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@pnpm/config.env-replace/-/config.env-replace-1.0.0.tgz", - "integrity": "sha512-ZVPVDi1E8oeXlYqkGRtX0CkzLTwE2zt62bjWaWKaAvI8NZqHzlMvGeSNDpW+JB3+aKanYb4UETJOF1/CxGPemA==", - "dev": true, - "engines": { - "node": ">=12.22.0" - } - }, - "node_modules/@pnpm/network.ca-file": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@pnpm/network.ca-file/-/network.ca-file-1.0.2.tgz", - "integrity": "sha512-YcPQ8a0jwYU9bTdJDpXjMi7Brhkr1mXsXrUJvjqM2mQDgkRiz8jFaQGOdaLxgjtUfQgZhKy/O3cG/YwmgKaxLA==", - "dev": true, - "dependencies": { - "graceful-fs": "4.2.10" - }, - "engines": { - "node": ">=12.22.0" - } - }, - "node_modules/@pnpm/network.ca-file/node_modules/graceful-fs": { - "version": "4.2.10", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.10.tgz", - "integrity": "sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==", - "dev": true - }, - "node_modules/@pnpm/npm-conf": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@pnpm/npm-conf/-/npm-conf-2.1.0.tgz", - "integrity": "sha512-Oe6ntvgsMTE3hDIqy6sajqHF+MnzJrOF06qC2QSiUEybLL7cp6tjoKUa32gpd9+KPVl4QyMs3E3nsXrx/Vdnlw==", - "dev": true, - "dependencies": { - "@pnpm/config.env-replace": "^1.0.0", - "@pnpm/network.ca-file": "^1.0.1", - "config-chain": "^1.1.11" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/@saithodev/semantic-release-backmerge": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/@saithodev/semantic-release-backmerge/-/semantic-release-backmerge-3.1.0.tgz", - "integrity": "sha512-92AN5eI8svpxeUD6cw2JjCrHHZVlWIxQ67SiSSwoI1UP4N5QohCOf9O/W3OUApxKg3C8Y0RpGt7TUpGEwGhXhw==", - "dev": true, - "dependencies": { - "@semantic-release/error": "^3.0.0", - "aggregate-error": "^3.1.0", - "debug": "^4.3.4", - "execa": "^5.1.1", - "lodash": "^4.17.21", - "semantic-release": ">=20.0.0" - } - }, - "node_modules/@semantic-release/changelog": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/@semantic-release/changelog/-/changelog-6.0.3.tgz", - "integrity": "sha512-dZuR5qByyfe3Y03TpmCvAxCyTnp7r5XwtHRf/8vD9EAn4ZWbavUX8adMtXYzE86EVh0gyLA7lm5yW4IV30XUag==", - "dev": true, - "dependencies": { - "@semantic-release/error": "^3.0.0", - "aggregate-error": "^3.0.0", - "fs-extra": "^11.0.0", - "lodash": "^4.17.4" - }, - "engines": { - "node": ">=14.17" - }, - "peerDependencies": { - "semantic-release": ">=18.0.0" - } - }, - "node_modules/@semantic-release/commit-analyzer": { - "version": "9.0.2", - "resolved": "https://registry.npmjs.org/@semantic-release/commit-analyzer/-/commit-analyzer-9.0.2.tgz", - "integrity": "sha512-E+dr6L+xIHZkX4zNMe6Rnwg4YQrWNXK+rNsvwOPpdFppvZO1olE2fIgWhv89TkQErygevbjsZFSIxp+u6w2e5g==", - "dev": true, - "dependencies": { - "conventional-changelog-angular": "^5.0.0", - "conventional-commits-filter": "^2.0.0", - "conventional-commits-parser": "^3.2.3", - "debug": "^4.0.0", - "import-from": "^4.0.0", - "lodash": "^4.17.4", - "micromatch": "^4.0.2" - }, - "engines": { - "node": ">=14.17" - }, - "peerDependencies": { - "semantic-release": ">=18.0.0-beta.1" - } - }, - "node_modules/@semantic-release/error": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@semantic-release/error/-/error-3.0.0.tgz", - "integrity": "sha512-5hiM4Un+tpl4cKw3lV4UgzJj+SmfNIDCLLw0TepzQxz9ZGV5ixnqkzIVF+3tp0ZHgcMKE+VNGHJjEeyFG2dcSw==", - "dev": true, - "engines": { - "node": ">=14.17" - } - }, - "node_modules/@semantic-release/git": { - "version": "10.0.1", - "resolved": "https://registry.npmjs.org/@semantic-release/git/-/git-10.0.1.tgz", - "integrity": "sha512-eWrx5KguUcU2wUPaO6sfvZI0wPafUKAMNC18aXY4EnNcrZL86dEmpNVnC9uMpGZkmZJ9EfCVJBQx4pV4EMGT1w==", - "dev": true, - "dependencies": { - "@semantic-release/error": "^3.0.0", - "aggregate-error": "^3.0.0", - "debug": "^4.0.0", - "dir-glob": "^3.0.0", - "execa": "^5.0.0", - "lodash": "^4.17.4", - "micromatch": "^4.0.0", - "p-reduce": "^2.0.0" - }, - "engines": { - "node": ">=14.17" - }, - "peerDependencies": { - "semantic-release": ">=18.0.0" - } - }, - "node_modules/@semantic-release/github": { - "version": "8.0.7", - "resolved": "https://registry.npmjs.org/@semantic-release/github/-/github-8.0.7.tgz", - "integrity": "sha512-VtgicRIKGvmTHwm//iqTh/5NGQwsncOMR5vQK9pMT92Aem7dv37JFKKRuulUsAnUOIlO4G8wH3gPiBAA0iW0ww==", - "dev": true, - "dependencies": { - "@octokit/rest": "^19.0.0", - "@semantic-release/error": "^3.0.0", - "aggregate-error": "^3.0.0", - "bottleneck": "^2.18.1", - "debug": "^4.0.0", - "dir-glob": "^3.0.0", - "fs-extra": "^11.0.0", - "globby": "^11.0.0", - "http-proxy-agent": "^5.0.0", - "https-proxy-agent": "^5.0.0", - "issue-parser": "^6.0.0", - "lodash": "^4.17.4", - "mime": "^3.0.0", - "p-filter": "^2.0.0", - "p-retry": "^4.0.0", - "url-join": "^4.0.0" - }, - "engines": { - "node": ">=14.17" - }, - "peerDependencies": { - "semantic-release": ">=18.0.0-beta.1" - } - }, - "node_modules/@semantic-release/npm": { - "version": "10.0.2", - "resolved": "https://registry.npmjs.org/@semantic-release/npm/-/npm-10.0.2.tgz", - "integrity": "sha512-Mo0XoBza4pUapxiBhLLYXeSZ9tkuHDUd/WvMbpilwuPRfJDnQXMqx5tBVon8d2mBk8JXmXpqB+ExhlWJmVT40A==", - "dev": true, - "dependencies": { - "@semantic-release/error": "^3.0.0", - "aggregate-error": "^4.0.1", - "execa": "^7.0.0", - "fs-extra": "^11.0.0", - "lodash-es": "^4.17.21", - "nerf-dart": "^1.0.0", - "normalize-url": "^8.0.0", - "npm": "^9.5.0", - "rc": "^1.2.8", - "read-pkg": "^7.0.0", - "registry-auth-token": "^5.0.0", - "semver": "^7.1.2", - "tempy": "^3.0.0" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "semantic-release": ">=20.1.0" - } - }, - "node_modules/@semantic-release/npm/node_modules/aggregate-error": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-4.0.1.tgz", - "integrity": "sha512-0poP0T7el6Vq3rstR8Mn4V/IQrpBLO6POkUSrN7RhyY+GF/InCFShQzsQ39T25gkHhLgSLByyAz+Kjb+c2L98w==", - "dev": true, - "dependencies": { - "clean-stack": "^4.0.0", - "indent-string": "^5.0.0" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@semantic-release/npm/node_modules/clean-stack": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-4.2.0.tgz", - "integrity": "sha512-LYv6XPxoyODi36Dp976riBtSY27VmFo+MKqEU9QCCWyTrdEPDog+RWA7xQWHi6Vbp61j5c4cdzzX1NidnwtUWg==", - "dev": true, - "dependencies": { - "escape-string-regexp": "5.0.0" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@semantic-release/npm/node_modules/crypto-random-string": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-4.0.0.tgz", - "integrity": "sha512-x8dy3RnvYdlUcPOjkEHqozhiwzKNSq7GcPuXFbnyMOCHxX8V3OgIg/pYuabl2sbUPfIJaeAQB7PMOK8DFIdoRA==", - "dev": true, - "dependencies": { - "type-fest": "^1.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@semantic-release/npm/node_modules/crypto-random-string/node_modules/type-fest": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-1.4.0.tgz", - "integrity": "sha512-yGSza74xk0UG8k+pLh5oeoYirvIiWo5t0/o3zHHAO2tRDiZcxWP7fywNlXhqb6/r6sWvwi+RsyQMWhVLe4BVuA==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@semantic-release/npm/node_modules/execa": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/execa/-/execa-7.1.1.tgz", - "integrity": "sha512-wH0eMf/UXckdUYnO21+HDztteVv05rq2GXksxT4fCGeHkBhw1DROXh40wcjMcRqDOWE7iPJ4n3M7e2+YFP+76Q==", - "dev": true, - "dependencies": { - "cross-spawn": "^7.0.3", - "get-stream": "^6.0.1", - "human-signals": "^4.3.0", - "is-stream": "^3.0.0", - "merge-stream": "^2.0.0", - "npm-run-path": "^5.1.0", - "onetime": "^6.0.0", - "signal-exit": "^3.0.7", - "strip-final-newline": "^3.0.0" - }, - "engines": { - "node": "^14.18.0 || ^16.14.0 || >=18.0.0" - }, - "funding": { - "url": "https://github.com/sindresorhus/execa?sponsor=1" - } - }, - "node_modules/@semantic-release/npm/node_modules/human-signals": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-4.3.1.tgz", - "integrity": "sha512-nZXjEF2nbo7lIw3mgYjItAfgQXog3OjJogSbKa2CQIIvSGWcKgeJnQlNXip6NglNzYH45nSRiEVimMvYL8DDqQ==", - "dev": true, - "engines": { - "node": ">=14.18.0" - } - }, - "node_modules/@semantic-release/npm/node_modules/indent-string": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-5.0.0.tgz", - "integrity": "sha512-m6FAo/spmsW2Ab2fU35JTYwtOKa2yAwXSwgjSv1TJzh4Mh7mC3lzAOVLBprb72XsTrgkEIsl7YrFNAiDiRhIGg==", - "dev": true, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@semantic-release/npm/node_modules/is-stream": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-3.0.0.tgz", - "integrity": "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==", - "dev": true, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@semantic-release/npm/node_modules/mimic-fn": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-4.0.0.tgz", - "integrity": "sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==", - "dev": true, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@semantic-release/npm/node_modules/normalize-url": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-8.0.0.tgz", - "integrity": "sha512-uVFpKhj5MheNBJRTiMZ9pE/7hD1QTeEvugSJW/OmLzAp78PB5O6adfMNTvmfKhXBkvCzC+rqifWcVYpGFwTjnw==", - "dev": true, - "engines": { - "node": ">=14.16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@semantic-release/npm/node_modules/npm-run-path": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.1.0.tgz", - "integrity": "sha512-sJOdmRGrY2sjNTRMbSvluQqg+8X7ZK61yvzBEIDhz4f8z1TZFYABsqjjCBd/0PUNE9M6QDgHJXQkGUEm7Q+l9Q==", - "dev": true, - "dependencies": { - "path-key": "^4.0.0" - }, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@semantic-release/npm/node_modules/onetime": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/onetime/-/onetime-6.0.0.tgz", - "integrity": "sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==", - "dev": true, - "dependencies": { - "mimic-fn": "^4.0.0" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@semantic-release/npm/node_modules/path-key": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz", - "integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==", - "dev": true, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@semantic-release/npm/node_modules/read-pkg": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-7.1.0.tgz", - "integrity": "sha512-5iOehe+WF75IccPc30bWTbpdDQLOCc3Uu8bi3Dte3Eueij81yx1Mrufk8qBx/YAbR4uL1FdUr+7BKXDwEtisXg==", - "dev": true, - "dependencies": { - "@types/normalize-package-data": "^2.4.1", - "normalize-package-data": "^3.0.2", - "parse-json": "^5.2.0", - "type-fest": "^2.0.0" - }, - "engines": { - "node": ">=12.20" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@semantic-release/npm/node_modules/strip-final-newline": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-3.0.0.tgz", - "integrity": "sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==", - "dev": true, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@semantic-release/npm/node_modules/tempy": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/tempy/-/tempy-3.0.0.tgz", - "integrity": "sha512-B2I9X7+o2wOaW4r/CWMkpOO9mdiTRCxXNgob6iGvPmfPWgH/KyUD6Uy5crtWBxIBe3YrNZKR2lSzv1JJKWD4vA==", - "dev": true, - "dependencies": { - "is-stream": "^3.0.0", - "temp-dir": "^2.0.0", - "type-fest": "^2.12.2", - "unique-string": "^3.0.0" - }, - "engines": { - "node": ">=14.16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@semantic-release/npm/node_modules/type-fest": { - "version": "2.19.0", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-2.19.0.tgz", - "integrity": "sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==", - "dev": true, - "engines": { - "node": ">=12.20" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@semantic-release/npm/node_modules/unique-string": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/unique-string/-/unique-string-3.0.0.tgz", - "integrity": "sha512-VGXBUVwxKMBUznyffQweQABPRRW1vHZAbadFZud4pLFAqRGvv/96vafgjWFqzourzr8YonlQiPgH0YCJfawoGQ==", - "dev": true, - "dependencies": { - "crypto-random-string": "^4.0.0" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@semantic-release/release-notes-generator": { - "version": "10.0.3", - "resolved": "https://registry.npmjs.org/@semantic-release/release-notes-generator/-/release-notes-generator-10.0.3.tgz", - "integrity": "sha512-k4x4VhIKneOWoBGHkx0qZogNjCldLPRiAjnIpMnlUh6PtaWXp/T+C9U7/TaNDDtgDa5HMbHl4WlREdxHio6/3w==", - "dev": true, - "dependencies": { - "conventional-changelog-angular": "^5.0.0", - "conventional-changelog-writer": "^5.0.0", - "conventional-commits-filter": "^2.0.0", - "conventional-commits-parser": "^3.2.3", - "debug": "^4.0.0", - "get-stream": "^6.0.0", - "import-from": "^4.0.0", - "into-stream": "^6.0.0", - "lodash": "^4.17.4", - "read-pkg-up": "^7.0.0" - }, - "engines": { - "node": ">=14.17" - }, - "peerDependencies": { - "semantic-release": ">=18.0.0-beta.1" - } - }, - "node_modules/@semantic-release/release-notes-generator/node_modules/find-up": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", - "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", - "dev": true, - "dependencies": { - "locate-path": "^5.0.0", - "path-exists": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/@semantic-release/release-notes-generator/node_modules/locate-path": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", - "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", - "dev": true, - "dependencies": { - "p-locate": "^4.1.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/@semantic-release/release-notes-generator/node_modules/p-limit": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", - "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", - "dev": true, - "dependencies": { - "p-try": "^2.0.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@semantic-release/release-notes-generator/node_modules/p-locate": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", - "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", - "dev": true, - "dependencies": { - "p-limit": "^2.2.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/@semantic-release/release-notes-generator/node_modules/path-exists": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", - "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/@semantic-release/release-notes-generator/node_modules/read-pkg-up": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-7.0.1.tgz", - "integrity": "sha512-zK0TB7Xd6JpCLmlLmufqykGE+/TlOePD6qKClNW7hHDKFh/J7/7gCWGR7joEQEW1bKq3a3yUZSObOoWLFQ4ohg==", - "dev": true, - "dependencies": { - "find-up": "^4.1.0", - "read-pkg": "^5.2.0", - "type-fest": "^0.8.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@semantic-release/release-notes-generator/node_modules/type-fest": { - "version": "0.8.1", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz", - "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/@tootallnate/once": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz", - "integrity": "sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==", - "dev": true, - "engines": { - "node": ">= 10" - } - }, - "node_modules/@types/minimist": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/@types/minimist/-/minimist-1.2.2.tgz", - "integrity": "sha512-jhuKLIRrhvCPLqwPcx6INqmKeiA5EWrsCOPhrlFSrbrmU4ZMPjj5Ul/oLCMDO98XRUIwVm78xICz4EPCektzeQ==", - "dev": true - }, - "node_modules/@types/normalize-package-data": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/@types/normalize-package-data/-/normalize-package-data-2.4.1.tgz", - "integrity": "sha512-Gj7cI7z+98M282Tqmp2K5EIsoouUEzbBJhQQzDE3jSIRk6r9gsz0oUokqIUR4u1R3dMHo0pDHM7sNOHyhulypw==", - "dev": true - }, - "node_modules/@types/retry": { - "version": "0.12.0", - "resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.0.tgz", - "integrity": "sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA==", - "dev": true - }, - "node_modules/agent-base": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", - "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", - "dev": true, - "dependencies": { - "debug": "4" - }, - "engines": { - "node": ">= 6.0.0" - } - }, - "node_modules/aggregate-error": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz", - "integrity": "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==", - "dev": true, - "dependencies": { - "clean-stack": "^2.0.0", - "indent-string": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/ansi-escapes": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-5.0.0.tgz", - "integrity": "sha512-5GFMVX8HqE/TB+FuBJGuO5XG0WrsA6ptUqoODaT/n9mmUaZFkqnBueB4leqGBCmrUHnCnC4PCZTCd0E7QQ83bA==", - "dev": true, - "dependencies": { - "type-fest": "^1.0.2" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/ansicolors": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/ansicolors/-/ansicolors-0.3.2.tgz", - "integrity": "sha512-QXu7BPrP29VllRxH8GwB7x5iX5qWKAAMLqKQGWTeLWVlNHNOpVMJ91dsxQAIWXpjuW5wqvxu3Jd/nRjrJ+0pqg==", - "dev": true - }, - "node_modules/argparse": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true - }, - "node_modules/argv-formatter": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/argv-formatter/-/argv-formatter-1.0.0.tgz", - "integrity": "sha512-F2+Hkm9xFaRg+GkaNnbwXNDV5O6pnCFEmqyhvfC/Ic5LbgOWjJh3L+mN/s91rxVL3znE7DYVpW0GJFT+4YBgWw==", - "dev": true - }, - "node_modules/array-ify": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/array-ify/-/array-ify-1.0.0.tgz", - "integrity": "sha512-c5AMf34bKdvPhQ7tBGhqkgKNUzMr4WUs+WDtC2ZUGOUncbxKMTvqxYctiseW3+L4bA8ec+GcZ6/A/FW4m8ukng==", - "dev": true - }, - "node_modules/array-union": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", - "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/arrify": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/arrify/-/arrify-1.0.1.tgz", - "integrity": "sha512-3CYzex9M9FGQjCGMGyi6/31c8GJbgb0qGyrx5HWxPd0aCwh4cB2YjMb2Xf9UuoogrMrlO9cTqnB5rI5GHZTcUA==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/before-after-hook": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/before-after-hook/-/before-after-hook-2.2.3.tgz", - "integrity": "sha512-NzUnlZexiaH/46WDhANlyR2bXRopNg4F/zuSA3OpZnllCUgRaOF2znDioDWrmbNVsuZk6l9pMquQB38cfBZwkQ==", - "dev": true - }, - "node_modules/bottleneck": { - "version": "2.19.5", - "resolved": "https://registry.npmjs.org/bottleneck/-/bottleneck-2.19.5.tgz", - "integrity": "sha512-VHiNCbI1lKdl44tGrhNfU3lup0Tj/ZBMJB5/2ZbNXRCPuRCO7ed2mgcK4r17y+KB2EfuYuRaVlwNbAeaWGSpbw==", - "dev": true - }, - "node_modules/braces": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", - "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", - "dev": true, - "dependencies": { - "fill-range": "^7.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/callsites": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", - "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", - "dev": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/camelcase": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", - "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", - "dev": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/camelcase-keys": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/camelcase-keys/-/camelcase-keys-6.2.2.tgz", - "integrity": "sha512-YrwaA0vEKazPBkn0ipTiMpSajYDSe+KjQfrjhcBMxJt/znbvlHd8Pw/Vamaz5EB4Wfhs3SUR3Z9mwRu/P3s3Yg==", - "dev": true, - "dependencies": { - "camelcase": "^5.3.1", - "map-obj": "^4.0.0", - "quick-lru": "^4.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/cardinal": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/cardinal/-/cardinal-2.1.1.tgz", - "integrity": "sha512-JSr5eOgoEymtYHBjNWyjrMqet9Am2miJhlfKNdqLp6zoeAh0KN5dRAcxlecj5mAJrmQomgiOBj35xHLrFjqBpw==", - "dev": true, - "dependencies": { - "ansicolors": "~0.3.2", - "redeyed": "~2.1.0" - }, - "bin": { - "cdl": "bin/cdl.js" - } - }, - "node_modules/chalk": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.2.0.tgz", - "integrity": "sha512-ree3Gqw/nazQAPuJJEy+avdl7QfZMcUvmHIKgEZkGL+xOBzRvup5Hxo6LHuMceSxOabuJLJm5Yp/92R9eMmMvA==", - "dev": true, - "engines": { - "node": "^12.17.0 || ^14.13 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/clean-stack": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", - "integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==", - "dev": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/cli-table3": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/cli-table3/-/cli-table3-0.6.3.tgz", - "integrity": "sha512-w5Jac5SykAeZJKntOxJCrm63Eg5/4dhMWIcuTbo9rpE+brgaSZo0RuNJZeOyMgsUdhDeojvgyQLmjI+K50ZGyg==", - "dev": true, - "dependencies": { - "string-width": "^4.2.0" - }, - "engines": { - "node": "10.* || >= 12.*" - }, - "optionalDependencies": { - "@colors/colors": "1.5.0" - } - }, - "node_modules/cliui": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", - "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", - "dev": true, - "dependencies": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.1", - "wrap-ansi": "^7.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "node_modules/compare-func": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/compare-func/-/compare-func-2.0.0.tgz", - "integrity": "sha512-zHig5N+tPWARooBnb0Zx1MFcdfpyJrfTJ3Y5L+IFvUm8rM74hHz66z0gw0x4tijh5CorKkKUCnW82R2vmpeCRA==", - "dev": true, - "dependencies": { - "array-ify": "^1.0.0", - "dot-prop": "^5.1.0" - } - }, - "node_modules/config-chain": { - "version": "1.1.13", - "resolved": "https://registry.npmjs.org/config-chain/-/config-chain-1.1.13.tgz", - "integrity": "sha512-qj+f8APARXHrM0hraqXYb2/bOVSV4PvJQlNZ/DVj0QrmNM2q2euizkeuVckQ57J+W0mRH6Hvi+k50M4Jul2VRQ==", - "dev": true, - "dependencies": { - "ini": "^1.3.4", - "proto-list": "~1.2.1" - } - }, - "node_modules/conventional-changelog-angular": { - "version": "5.0.13", - "resolved": "https://registry.npmjs.org/conventional-changelog-angular/-/conventional-changelog-angular-5.0.13.tgz", - "integrity": "sha512-i/gipMxs7s8L/QeuavPF2hLnJgH6pEZAttySB6aiQLWcX3puWDL3ACVmvBhJGxnAy52Qc15ua26BufY6KpmrVA==", - "dev": true, - "dependencies": { - "compare-func": "^2.0.0", - "q": "^1.5.1" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/conventional-changelog-writer": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/conventional-changelog-writer/-/conventional-changelog-writer-5.0.1.tgz", - "integrity": "sha512-5WsuKUfxW7suLblAbFnxAcrvf6r+0b7GvNaWUwUIk0bXMnENP/PEieGKVUQrjPqwPT4o3EPAASBXiY6iHooLOQ==", - "dev": true, - "dependencies": { - "conventional-commits-filter": "^2.0.7", - "dateformat": "^3.0.0", - "handlebars": "^4.7.7", - "json-stringify-safe": "^5.0.1", - "lodash": "^4.17.15", - "meow": "^8.0.0", - "semver": "^6.0.0", - "split": "^1.0.0", - "through2": "^4.0.0" - }, - "bin": { - "conventional-changelog-writer": "cli.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/conventional-changelog-writer/node_modules/semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", - "dev": true, - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/conventional-commits-filter": { - "version": "2.0.7", - "resolved": "https://registry.npmjs.org/conventional-commits-filter/-/conventional-commits-filter-2.0.7.tgz", - "integrity": "sha512-ASS9SamOP4TbCClsRHxIHXRfcGCnIoQqkvAzCSbZzTFLfcTqJVugB0agRgsEELsqaeWgsXv513eS116wnlSSPA==", - "dev": true, - "dependencies": { - "lodash.ismatch": "^4.4.0", - "modify-values": "^1.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/conventional-commits-parser": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/conventional-commits-parser/-/conventional-commits-parser-3.2.4.tgz", - "integrity": "sha512-nK7sAtfi+QXbxHCYfhpZsfRtaitZLIA6889kFIouLvz6repszQDgxBu7wf2WbU+Dco7sAnNCJYERCwt54WPC2Q==", - "dev": true, - "dependencies": { - "is-text-path": "^1.0.1", - "JSONStream": "^1.0.4", - "lodash": "^4.17.15", - "meow": "^8.0.0", - "split2": "^3.0.0", - "through2": "^4.0.0" - }, - "bin": { - "conventional-commits-parser": "cli.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/core-util-is": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", - "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", - "dev": true - }, - "node_modules/cosmiconfig": { - "version": "8.1.2", - "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-8.1.2.tgz", - "integrity": "sha512-rmpUFKMZiawLfug8sP4NbpBSOpWftZB6UACOLEiNbnRAYM1TzgQuTWlMYFRuPgmoTCkcOxSMwQJQpJmiXv/eHw==", - "dev": true, - "dependencies": { - "import-fresh": "^3.2.1", - "js-yaml": "^4.1.0", - "parse-json": "^5.0.0", - "path-type": "^4.0.0" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/d-fischer" - } - }, - "node_modules/cross-spawn": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", - "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", - "dev": true, - "dependencies": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/dateformat": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/dateformat/-/dateformat-3.0.3.tgz", - "integrity": "sha512-jyCETtSl3VMZMWeRo7iY1FL19ges1t55hMo5yaam4Jrsm5EPL89UQkoQRyiI+Yf4k8r2ZpdngkV8hr1lIdjb3Q==", - "dev": true, - "engines": { - "node": "*" - } - }, - "node_modules/debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", - "dev": true, - "dependencies": { - "ms": "2.1.2" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/decamelize": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", - "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/decamelize-keys": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/decamelize-keys/-/decamelize-keys-1.1.1.tgz", - "integrity": "sha512-WiPxgEirIV0/eIOMcnFBA3/IJZAZqKnwAwWyvvdi4lsr1WCN22nhdf/3db3DoZcUjTV2SqfzIwNyp6y2xs3nmg==", - "dev": true, - "dependencies": { - "decamelize": "^1.1.0", - "map-obj": "^1.0.0" - }, - "engines": { - "node": ">=0.10.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/decamelize-keys/node_modules/map-obj": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-1.0.1.tgz", - "integrity": "sha512-7N/q3lyZ+LVCp7PzuxrJr4KMbBE2hW7BT7YNia330OFxIf4d3r5zVpicP2650l7CPN6RM9zOJRl3NGpqSiw3Eg==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/deep-extend": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", - "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", - "dev": true, - "engines": { - "node": ">=4.0.0" - } - }, - "node_modules/deprecation": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/deprecation/-/deprecation-2.3.1.tgz", - "integrity": "sha512-xmHIy4F3scKVwMsQ4WnVaS8bHOx0DmVwRywosKhaILI0ywMDWPtBSku2HNxRvF7jtwDRsoEwYQSfbxj8b7RlJQ==", - "dev": true - }, - "node_modules/dir-glob": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", - "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", - "dev": true, - "dependencies": { - "path-type": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/dot-prop": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-5.3.0.tgz", - "integrity": "sha512-QM8q3zDe58hqUqjraQOmzZ1LIH9SWQJTlEKCH4kJ2oQvLZk7RbQXvtDM2XEq3fwkV9CCvvH4LA0AV+ogFsBM2Q==", - "dev": true, - "dependencies": { - "is-obj": "^2.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/duplexer2": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/duplexer2/-/duplexer2-0.1.4.tgz", - "integrity": "sha512-asLFVfWWtJ90ZyOUHMqk7/S2w2guQKxUI2itj3d92ADHhxUSbCMGi1f1cBcJ7xM1To+pE/Khbwo1yuNbMEPKeA==", - "dev": true, - "dependencies": { - "readable-stream": "^2.0.2" - } - }, - "node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true - }, - "node_modules/env-ci": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/env-ci/-/env-ci-9.0.0.tgz", - "integrity": "sha512-Q3cjr1tX9xwigprw4G8M3o7PIOO/1LYji6TyGsbD1WfMmD23etZvhgmPXJqkP788yH4dgSSK7oaIMuaayUJIfg==", - "dev": true, - "dependencies": { - "execa": "^7.0.0", - "java-properties": "^1.0.2" - }, - "engines": { - "node": "^16.14 || >=18" - } - }, - "node_modules/env-ci/node_modules/execa": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/execa/-/execa-7.1.1.tgz", - "integrity": "sha512-wH0eMf/UXckdUYnO21+HDztteVv05rq2GXksxT4fCGeHkBhw1DROXh40wcjMcRqDOWE7iPJ4n3M7e2+YFP+76Q==", - "dev": true, - "dependencies": { - "cross-spawn": "^7.0.3", - "get-stream": "^6.0.1", - "human-signals": "^4.3.0", - "is-stream": "^3.0.0", - "merge-stream": "^2.0.0", - "npm-run-path": "^5.1.0", - "onetime": "^6.0.0", - "signal-exit": "^3.0.7", - "strip-final-newline": "^3.0.0" - }, - "engines": { - "node": "^14.18.0 || ^16.14.0 || >=18.0.0" - }, - "funding": { - "url": "https://github.com/sindresorhus/execa?sponsor=1" - } - }, - "node_modules/env-ci/node_modules/human-signals": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-4.3.1.tgz", - "integrity": "sha512-nZXjEF2nbo7lIw3mgYjItAfgQXog3OjJogSbKa2CQIIvSGWcKgeJnQlNXip6NglNzYH45nSRiEVimMvYL8DDqQ==", - "dev": true, - "engines": { - "node": ">=14.18.0" - } - }, - "node_modules/env-ci/node_modules/is-stream": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-3.0.0.tgz", - "integrity": "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==", - "dev": true, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/env-ci/node_modules/mimic-fn": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-4.0.0.tgz", - "integrity": "sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==", - "dev": true, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/env-ci/node_modules/npm-run-path": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.1.0.tgz", - "integrity": "sha512-sJOdmRGrY2sjNTRMbSvluQqg+8X7ZK61yvzBEIDhz4f8z1TZFYABsqjjCBd/0PUNE9M6QDgHJXQkGUEm7Q+l9Q==", - "dev": true, - "dependencies": { - "path-key": "^4.0.0" - }, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/env-ci/node_modules/onetime": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/onetime/-/onetime-6.0.0.tgz", - "integrity": "sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==", - "dev": true, - "dependencies": { - "mimic-fn": "^4.0.0" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/env-ci/node_modules/path-key": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz", - "integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==", - "dev": true, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/env-ci/node_modules/strip-final-newline": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-3.0.0.tgz", - "integrity": "sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==", - "dev": true, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/error-ex": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", - "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", - "dev": true, - "dependencies": { - "is-arrayish": "^0.2.1" - } - }, - "node_modules/escalade": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", - "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", - "dev": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/escape-string-regexp": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz", - "integrity": "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==", - "dev": true, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/esprima": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", - "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", - "dev": true, - "bin": { - "esparse": "bin/esparse.js", - "esvalidate": "bin/esvalidate.js" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/execa": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", - "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", - "dev": true, - "dependencies": { - "cross-spawn": "^7.0.3", - "get-stream": "^6.0.0", - "human-signals": "^2.1.0", - "is-stream": "^2.0.0", - "merge-stream": "^2.0.0", - "npm-run-path": "^4.0.1", - "onetime": "^5.1.2", - "signal-exit": "^3.0.3", - "strip-final-newline": "^2.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sindresorhus/execa?sponsor=1" - } - }, - "node_modules/fast-glob": { - "version": "3.2.12", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.12.tgz", - "integrity": "sha512-DVj4CQIYYow0BlaelwK1pHl5n5cRSJfM60UA0zK891sVInoPri2Ekj7+e1CT3/3qxXenpI+nBBmQAcJPJgaj4w==", - "dev": true, - "dependencies": { - "@nodelib/fs.stat": "^2.0.2", - "@nodelib/fs.walk": "^1.2.3", - "glob-parent": "^5.1.2", - "merge2": "^1.3.0", - "micromatch": "^4.0.4" - }, - "engines": { - "node": ">=8.6.0" - } - }, - "node_modules/fastq": { - "version": "1.15.0", - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.15.0.tgz", - "integrity": "sha512-wBrocU2LCXXa+lWBt8RoIRD89Fi8OdABODa/kEnyeyjS5aZO5/GNvI5sEINADqP/h8M29UHTHUb53sUu5Ihqdw==", - "dev": true, - "dependencies": { - "reusify": "^1.0.4" - } - }, - "node_modules/figures": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/figures/-/figures-5.0.0.tgz", - "integrity": "sha512-ej8ksPF4x6e5wvK9yevct0UCXh8TTFlWGVLlgjZuoBH1HwjIfKE/IdL5mq89sFA7zELi1VhKpmtDnrs7zWyeyg==", - "dev": true, - "dependencies": { - "escape-string-regexp": "^5.0.0", - "is-unicode-supported": "^1.2.0" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/fill-range": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", - "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", - "dev": true, - "dependencies": { - "to-regex-range": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/find-up": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-6.3.0.tgz", - "integrity": "sha512-v2ZsoEuVHYy8ZIlYqwPe/39Cy+cFDzp4dXPaxNvkEuouymu+2Jbz0PxpKarJHYJTmv2HWT3O382qY8l4jMWthw==", - "dev": true, - "dependencies": { - "locate-path": "^7.1.0", - "path-exists": "^5.0.0" - }, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/find-versions": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/find-versions/-/find-versions-5.1.0.tgz", - "integrity": "sha512-+iwzCJ7C5v5KgcBuueqVoNiHVoQpwiUK5XFLjf0affFTep+Wcw93tPvmb8tqujDNmzhBDPddnWV/qgWSXgq+Hg==", - "dev": true, - "dependencies": { - "semver-regex": "^4.0.5" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/from2": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/from2/-/from2-2.3.0.tgz", - "integrity": "sha512-OMcX/4IC/uqEPVgGeyfN22LJk6AZrMkRZHxcHBMBvHScDGgwTm2GT2Wkgtocyd3JfZffjj2kYUDXXII0Fk9W0g==", - "dev": true, - "dependencies": { - "inherits": "^2.0.1", - "readable-stream": "^2.0.0" - } - }, - "node_modules/fs-extra": { - "version": "11.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.1.0.tgz", - "integrity": "sha512-0rcTq621PD5jM/e0a3EJoGC/1TC5ZBCERW82LQuwfGnCa1V8w7dpYH1yNu+SLb6E5dkeCBzKEyLGlFrnr+dUyw==", - "dev": true, - "dependencies": { - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" - }, - "engines": { - "node": ">=14.14" - } - }, - "node_modules/function-bind": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", - "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", - "dev": true - }, - "node_modules/get-caller-file": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", - "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", - "dev": true, - "engines": { - "node": "6.* || 8.* || >= 10.*" - } - }, - "node_modules/get-stream": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", - "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/git-log-parser": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/git-log-parser/-/git-log-parser-1.2.0.tgz", - "integrity": "sha512-rnCVNfkTL8tdNryFuaY0fYiBWEBcgF748O6ZI61rslBvr2o7U65c2/6npCRqH40vuAhtgtDiqLTJjBVdrejCzA==", - "dev": true, - "dependencies": { - "argv-formatter": "~1.0.0", - "spawn-error-forwarder": "~1.0.0", - "split2": "~1.0.0", - "stream-combiner2": "~1.1.1", - "through2": "~2.0.0", - "traverse": "~0.6.6" - } - }, - "node_modules/git-log-parser/node_modules/split2": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/split2/-/split2-1.0.0.tgz", - "integrity": "sha512-NKywug4u4pX/AZBB1FCPzZ6/7O+Xhz1qMVbzTvvKvikjO99oPN87SkK08mEY9P63/5lWjK+wgOOgApnTg5r6qg==", - "dev": true, - "dependencies": { - "through2": "~2.0.0" - } - }, - "node_modules/git-log-parser/node_modules/through2": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz", - "integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==", - "dev": true, - "dependencies": { - "readable-stream": "~2.3.6", - "xtend": "~4.0.1" - } - }, - "node_modules/glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "dev": true, - "dependencies": { - "is-glob": "^4.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/globby": { - "version": "11.1.0", - "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", - "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", - "dev": true, - "dependencies": { - "array-union": "^2.1.0", - "dir-glob": "^3.0.1", - "fast-glob": "^3.2.9", - "ignore": "^5.2.0", - "merge2": "^1.4.1", - "slash": "^3.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/graceful-fs": { - "version": "4.2.11", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", - "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", - "dev": true - }, - "node_modules/handlebars": { - "version": "4.7.7", - "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.7.tgz", - "integrity": "sha512-aAcXm5OAfE/8IXkcZvCepKU3VzW1/39Fb5ZuqMtgI/hT8X2YgoMvBY5dLhq/cpOvw7Lk1nK/UF71aLG/ZnVYRA==", - "dev": true, - "dependencies": { - "minimist": "^1.2.5", - "neo-async": "^2.6.0", - "source-map": "^0.6.1", - "wordwrap": "^1.0.0" - }, - "bin": { - "handlebars": "bin/handlebars" - }, - "engines": { - "node": ">=0.4.7" - }, - "optionalDependencies": { - "uglify-js": "^3.1.4" - } - }, - "node_modules/hard-rejection": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/hard-rejection/-/hard-rejection-2.1.0.tgz", - "integrity": "sha512-VIZB+ibDhx7ObhAe7OVtoEbuP4h/MuOTHJ+J8h/eBXotJYl0fBgR72xDFCKgIh22OJZIOVNxBMWuhAr10r8HdA==", - "dev": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/has": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", - "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", - "dev": true, - "dependencies": { - "function-bind": "^1.1.1" - }, - "engines": { - "node": ">= 0.4.0" - } - }, - "node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/hook-std": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/hook-std/-/hook-std-3.0.0.tgz", - "integrity": "sha512-jHRQzjSDzMtFy34AGj1DN+vq54WVuhSvKgrHf0OMiFQTwDD4L/qqofVEWjLOBMTn5+lCD3fPg32W9yOfnEJTTw==", - "dev": true, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/hosted-git-info": { - "version": "6.1.1", - "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-6.1.1.tgz", - "integrity": "sha512-r0EI+HBMcXadMrugk0GCQ+6BQV39PiWAZVfq7oIckeGiN7sjRGyQxPdft3nQekFTCQbYxLBH+/axZMeH8UX6+w==", - "dev": true, - "dependencies": { - "lru-cache": "^7.5.1" - }, - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" - } - }, - "node_modules/http-proxy-agent": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz", - "integrity": "sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==", - "dev": true, - "dependencies": { - "@tootallnate/once": "2", - "agent-base": "6", - "debug": "4" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/https-proxy-agent": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", - "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", - "dev": true, - "dependencies": { - "agent-base": "6", - "debug": "4" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/human-signals": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", - "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", - "dev": true, - "engines": { - "node": ">=10.17.0" - } - }, - "node_modules/ignore": { - "version": "5.2.4", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.4.tgz", - "integrity": "sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ==", - "dev": true, - "engines": { - "node": ">= 4" - } - }, - "node_modules/import-fresh": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", - "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", - "dev": true, - "dependencies": { - "parent-module": "^1.0.0", - "resolve-from": "^4.0.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/import-fresh/node_modules/resolve-from": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", - "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/import-from": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/import-from/-/import-from-4.0.0.tgz", - "integrity": "sha512-P9J71vT5nLlDeV8FHs5nNxaLbrpfAV5cF5srvbZfpwpcJoM/xZR3hiv+q+SAnuSmuGbXMWud063iIMx/V/EWZQ==", - "dev": true, - "engines": { - "node": ">=12.2" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/indent-string": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", - "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "dev": true - }, - "node_modules/ini": { - "version": "1.3.8", - "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", - "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", - "dev": true - }, - "node_modules/into-stream": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/into-stream/-/into-stream-6.0.0.tgz", - "integrity": "sha512-XHbaOAvP+uFKUFsOgoNPRjLkwB+I22JFPFe5OjTkQ0nwgj6+pSjb4NmB6VMxaPshLiOf+zcpOCBQuLwC1KHhZA==", - "dev": true, - "dependencies": { - "from2": "^2.3.0", - "p-is-promise": "^3.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/is-arrayish": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", - "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", - "dev": true - }, - "node_modules/is-core-module": { - "version": "2.11.0", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.11.0.tgz", - "integrity": "sha512-RRjxlvLDkD1YJwDbroBHMb+cukurkDWNyHx7D3oNB5x9rb5ogcksMC5wHCadcXoo67gVr/+3GFySh3134zi6rw==", - "dev": true, - "dependencies": { - "has": "^1.0.3" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-extglob": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/is-glob": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", - "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", - "dev": true, - "dependencies": { - "is-extglob": "^2.1.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "dev": true, - "engines": { - "node": ">=0.12.0" - } - }, - "node_modules/is-obj": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-2.0.0.tgz", - "integrity": "sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/is-plain-obj": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-1.1.0.tgz", - "integrity": "sha512-yvkRyxmFKEOQ4pNXCmJG5AEQNlXJS5LaONXo5/cLdTZdWvsZ1ioJEonLGAosKlMWE8lwUy/bJzMjcw8az73+Fg==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-plain-object": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz", - "integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-stream": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", - "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", - "dev": true, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/is-text-path": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-text-path/-/is-text-path-1.0.1.tgz", - "integrity": "sha512-xFuJpne9oFz5qDaodwmmG08e3CawH/2ZV8Qqza1Ko7Sk8POWbkRdwIoAWVhqvq0XeUzANEhKo2n0IXUGBm7A/w==", - "dev": true, - "dependencies": { - "text-extensions": "^1.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-unicode-supported": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-1.3.0.tgz", - "integrity": "sha512-43r2mRvz+8JRIKnWJ+3j8JtjRKZ6GmjzfaE/qiBJnikNnYv/6bagRJ1kUhNk8R5EX/GkobD+r+sfxCPJsiKBLQ==", - "dev": true, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", - "dev": true - }, - "node_modules/isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "dev": true - }, - "node_modules/issue-parser": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/issue-parser/-/issue-parser-6.0.0.tgz", - "integrity": "sha512-zKa/Dxq2lGsBIXQ7CUZWTHfvxPC2ej0KfO7fIPqLlHB9J2hJ7rGhZ5rilhuufylr4RXYPzJUeFjKxz305OsNlA==", - "dev": true, - "dependencies": { - "lodash.capitalize": "^4.2.1", - "lodash.escaperegexp": "^4.1.2", - "lodash.isplainobject": "^4.0.6", - "lodash.isstring": "^4.0.1", - "lodash.uniqby": "^4.7.0" - }, - "engines": { - "node": ">=10.13" - } - }, - "node_modules/java-properties": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/java-properties/-/java-properties-1.0.2.tgz", - "integrity": "sha512-qjdpeo2yKlYTH7nFdK0vbZWuTCesk4o63v5iVOlhMQPfuIZQfW/HI35SjfhA+4qpg36rnFSvUK5b1m+ckIblQQ==", - "dev": true, - "engines": { - "node": ">= 0.6.0" - } - }, - "node_modules/js-tokens": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", - "dev": true - }, - "node_modules/js-yaml": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", - "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", - "dev": true, - "dependencies": { - "argparse": "^2.0.1" - }, - "bin": { - "js-yaml": "bin/js-yaml.js" - } - }, - "node_modules/json-parse-better-errors": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz", - "integrity": "sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==", - "dev": true - }, - "node_modules/json-parse-even-better-errors": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", - "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", - "dev": true - }, - "node_modules/json-stringify-safe": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", - "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==", - "dev": true - }, - "node_modules/jsonfile": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", - "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", - "dev": true, - "dependencies": { - "universalify": "^2.0.0" - }, - "optionalDependencies": { - "graceful-fs": "^4.1.6" - } - }, - "node_modules/jsonparse": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/jsonparse/-/jsonparse-1.3.1.tgz", - "integrity": "sha512-POQXvpdL69+CluYsillJ7SUhKvytYjW9vG/GKpnf+xP8UWgYEM/RaMzHHofbALDiKbbP1W8UEYmgGl39WkPZsg==", - "dev": true, - "engines": [ - "node >= 0.2.0" - ] - }, - "node_modules/JSONStream": { - "version": "1.3.5", - "resolved": "https://registry.npmjs.org/JSONStream/-/JSONStream-1.3.5.tgz", - "integrity": "sha512-E+iruNOY8VV9s4JEbe1aNEm6MiszPRr/UfcHMz0TQh1BXSxHK+ASV1R6W4HpjBhSeS+54PIsAMCBmwD06LLsqQ==", - "dev": true, - "dependencies": { - "jsonparse": "^1.2.0", - "through": ">=2.2.7 <3" - }, - "bin": { - "JSONStream": "bin.js" - }, - "engines": { - "node": "*" - } - }, - "node_modules/kind-of": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", - "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/lines-and-columns": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", - "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", - "dev": true - }, - "node_modules/load-json-file": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-4.0.0.tgz", - "integrity": "sha512-Kx8hMakjX03tiGTLAIdJ+lL0htKnXjEZN6hk/tozf/WOuYGdZBJrZ+rCJRbVCugsjB3jMLn9746NsQIf5VjBMw==", - "dev": true, - "dependencies": { - "graceful-fs": "^4.1.2", - "parse-json": "^4.0.0", - "pify": "^3.0.0", - "strip-bom": "^3.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/load-json-file/node_modules/parse-json": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-4.0.0.tgz", - "integrity": "sha512-aOIos8bujGN93/8Ox/jPLh7RwVnPEysynVFE+fQZyg6jKELEHwzgKdLRFHUgXJL6kylijVSBC4BvN9OmsB48Rw==", - "dev": true, - "dependencies": { - "error-ex": "^1.3.1", - "json-parse-better-errors": "^1.0.1" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/locate-path": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-7.2.0.tgz", - "integrity": "sha512-gvVijfZvn7R+2qyPX8mAuKcFGDf6Nc61GdvGafQsHL0sBIxfKzA+usWn4GFC/bk+QdwPUD4kWFJLhElipq+0VA==", - "dev": true, - "dependencies": { - "p-locate": "^6.0.0" - }, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/lodash": { - "version": "4.17.21", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", - "dev": true - }, - "node_modules/lodash-es": { - "version": "4.17.21", - "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz", - "integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==", - "dev": true - }, - "node_modules/lodash.capitalize": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/lodash.capitalize/-/lodash.capitalize-4.2.1.tgz", - "integrity": "sha512-kZzYOKspf8XVX5AvmQF94gQW0lejFVgb80G85bU4ZWzoJ6C03PQg3coYAUpSTpQWelrZELd3XWgHzw4Ck5kaIw==", - "dev": true - }, - "node_modules/lodash.escaperegexp": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/lodash.escaperegexp/-/lodash.escaperegexp-4.1.2.tgz", - "integrity": "sha512-TM9YBvyC84ZxE3rgfefxUWiQKLilstD6k7PTGt6wfbtXF8ixIJLOL3VYyV/z+ZiPLsVxAsKAFVwWlWeb2Y8Yyw==", - "dev": true - }, - "node_modules/lodash.ismatch": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/lodash.ismatch/-/lodash.ismatch-4.4.0.tgz", - "integrity": "sha512-fPMfXjGQEV9Xsq/8MTSgUf255gawYRbjwMyDbcvDhXgV7enSZA0hynz6vMPnpAb5iONEzBHBPsT+0zes5Z301g==", - "dev": true - }, - "node_modules/lodash.isplainobject": { - "version": "4.0.6", - "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", - "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", - "dev": true - }, - "node_modules/lodash.isstring": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", - "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==", - "dev": true - }, - "node_modules/lodash.uniqby": { - "version": "4.7.0", - "resolved": "https://registry.npmjs.org/lodash.uniqby/-/lodash.uniqby-4.7.0.tgz", - "integrity": "sha512-e/zcLx6CSbmaEgFHCA7BnoQKyCtKMxnuWrJygbwPs/AIn+IMKl66L8/s+wBUn5LRw2pZx3bUHibiV1b6aTWIww==", - "dev": true - }, - "node_modules/lru-cache": { - "version": "7.18.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", - "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", - "dev": true, - "engines": { - "node": ">=12" - } - }, - "node_modules/map-obj": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-4.3.0.tgz", - "integrity": "sha512-hdN1wVrZbb29eBGiGjJbeP8JbKjq1urkHJ/LIP/NY48MZ1QVXUsQBV1G1zvYFHn1XE06cwjBsOI2K3Ulnj1YXQ==", - "dev": true, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/marked": { - "version": "4.2.12", - "resolved": "https://registry.npmjs.org/marked/-/marked-4.2.12.tgz", - "integrity": "sha512-yr8hSKa3Fv4D3jdZmtMMPghgVt6TWbk86WQaWhDloQjRSQhMMYCAro7jP7VDJrjjdV8pxVxMssXS8B8Y5DZ5aw==", - "dev": true, - "bin": { - "marked": "bin/marked.js" - }, - "engines": { - "node": ">= 12" - } - }, - "node_modules/marked-terminal": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/marked-terminal/-/marked-terminal-5.1.1.tgz", - "integrity": "sha512-+cKTOx9P4l7HwINYhzbrBSyzgxO2HaHKGZGuB1orZsMIgXYaJyfidT81VXRdpelW/PcHEWxywscePVgI/oUF6g==", - "dev": true, - "dependencies": { - "ansi-escapes": "^5.0.0", - "cardinal": "^2.1.1", - "chalk": "^5.0.0", - "cli-table3": "^0.6.1", - "node-emoji": "^1.11.0", - "supports-hyperlinks": "^2.2.0" - }, - "engines": { - "node": ">=14.13.1 || >=16.0.0" - }, - "peerDependencies": { - "marked": "^1.0.0 || ^2.0.0 || ^3.0.0 || ^4.0.0" - } - }, - "node_modules/meow": { - "version": "8.1.2", - "resolved": "https://registry.npmjs.org/meow/-/meow-8.1.2.tgz", - "integrity": "sha512-r85E3NdZ+mpYk1C6RjPFEMSE+s1iZMuHtsHAqY0DT3jZczl0diWUZ8g6oU7h0M9cD2EL+PzaYghhCLzR0ZNn5Q==", - "dev": true, - "dependencies": { - "@types/minimist": "^1.2.0", - "camelcase-keys": "^6.2.2", - "decamelize-keys": "^1.1.0", - "hard-rejection": "^2.1.0", - "minimist-options": "4.1.0", - "normalize-package-data": "^3.0.0", - "read-pkg-up": "^7.0.1", - "redent": "^3.0.0", - "trim-newlines": "^3.0.0", - "type-fest": "^0.18.0", - "yargs-parser": "^20.2.3" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/meow/node_modules/find-up": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", - "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", - "dev": true, - "dependencies": { - "locate-path": "^5.0.0", - "path-exists": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/meow/node_modules/locate-path": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", - "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", - "dev": true, - "dependencies": { - "p-locate": "^4.1.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/meow/node_modules/p-limit": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", - "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", - "dev": true, - "dependencies": { - "p-try": "^2.0.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/meow/node_modules/p-locate": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", - "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", - "dev": true, - "dependencies": { - "p-limit": "^2.2.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/meow/node_modules/path-exists": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", - "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/meow/node_modules/read-pkg-up": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-7.0.1.tgz", - "integrity": "sha512-zK0TB7Xd6JpCLmlLmufqykGE+/TlOePD6qKClNW7hHDKFh/J7/7gCWGR7joEQEW1bKq3a3yUZSObOoWLFQ4ohg==", - "dev": true, - "dependencies": { - "find-up": "^4.1.0", - "read-pkg": "^5.2.0", - "type-fest": "^0.8.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/meow/node_modules/read-pkg-up/node_modules/type-fest": { - "version": "0.8.1", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz", - "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/meow/node_modules/type-fest": { - "version": "0.18.1", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.18.1.tgz", - "integrity": "sha512-OIAYXk8+ISY+qTOwkHtKqzAuxchoMiD9Udx+FSGQDuiRR+PJKJHc2NJAXlbhkGwTt/4/nKZxELY1w3ReWOL8mw==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/merge-stream": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", - "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", - "dev": true - }, - "node_modules/merge2": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", - "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", - "dev": true, - "engines": { - "node": ">= 8" - } - }, - "node_modules/micromatch": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", - "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", - "dev": true, - "dependencies": { - "braces": "^3.0.2", - "picomatch": "^2.3.1" - }, - "engines": { - "node": ">=8.6" - } - }, - "node_modules/mime": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/mime/-/mime-3.0.0.tgz", - "integrity": "sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==", - "dev": true, - "bin": { - "mime": "cli.js" - }, - "engines": { - "node": ">=10.0.0" - } - }, - "node_modules/mimic-fn": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", - "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", - "dev": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/min-indent": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", - "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/minimist": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", - "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", - "dev": true, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/minimist-options": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/minimist-options/-/minimist-options-4.1.0.tgz", - "integrity": "sha512-Q4r8ghd80yhO/0j1O3B2BjweX3fiHg9cdOwjJd2J76Q135c+NDxGCqdYKQ1SKBuFfgWbAUzBfvYjPUEeNgqN1A==", - "dev": true, - "dependencies": { - "arrify": "^1.0.1", - "is-plain-obj": "^1.1.0", - "kind-of": "^6.0.3" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/modify-values": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/modify-values/-/modify-values-1.0.1.tgz", - "integrity": "sha512-xV2bxeN6F7oYjZWTe/YPAy6MN2M+sL4u/Rlm2AHCIVGfo2p1yGmBHQ6vHehl4bRTZBdHu3TSkWdYgkwpYzAGSw==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true - }, - "node_modules/neo-async": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", - "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", - "dev": true - }, - "node_modules/nerf-dart": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/nerf-dart/-/nerf-dart-1.0.0.tgz", - "integrity": "sha512-EZSPZB70jiVsivaBLYDCyntd5eH8NTSMOn3rB+HxwdmKThGELLdYv8qVIMWvZEFy9w8ZZpW9h9OB32l1rGtj7g==", - "dev": true - }, - "node_modules/node-emoji": { - "version": "1.11.0", - "resolved": "https://registry.npmjs.org/node-emoji/-/node-emoji-1.11.0.tgz", - "integrity": "sha512-wo2DpQkQp7Sjm2A0cq+sN7EHKO6Sl0ctXeBdFZrL9T9+UywORbufTcTZxom8YqpLQt/FqNMUkOpkZrJVYSKD3A==", - "dev": true, - "dependencies": { - "lodash": "^4.17.21" - } - }, - "node_modules/node-fetch": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.9.tgz", - "integrity": "sha512-DJm/CJkZkRjKKj4Zi4BsKVZh3ValV5IR5s7LVZnW+6YMh0W1BfNA8XSs6DLMGYlId5F3KnA70uu2qepcR08Qqg==", - "dev": true, - "dependencies": { - "whatwg-url": "^5.0.0" - }, - "engines": { - "node": "4.x || >=6.0.0" - }, - "peerDependencies": { - "encoding": "^0.1.0" - }, - "peerDependenciesMeta": { - "encoding": { - "optional": true - } - } - }, - "node_modules/normalize-package-data": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-3.0.3.tgz", - "integrity": "sha512-p2W1sgqij3zMMyRC067Dg16bfzVH+w7hyegmpIvZ4JNjqtGOVAIvLmjBx3yP7YTe9vKJgkoNOPjwQGogDoMXFA==", - "dev": true, - "dependencies": { - "hosted-git-info": "^4.0.1", - "is-core-module": "^2.5.0", - "semver": "^7.3.4", - "validate-npm-package-license": "^3.0.1" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/normalize-package-data/node_modules/hosted-git-info": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-4.1.0.tgz", - "integrity": "sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA==", - "dev": true, - "dependencies": { - "lru-cache": "^6.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/normalize-package-data/node_modules/lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "dev": true, - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/npm": { - "version": "9.6.2", - "resolved": "https://registry.npmjs.org/npm/-/npm-9.6.2.tgz", - "integrity": "sha512-TnXoXhlFkH/9wI4+aXSq0aPLwKG7Ge17t1ME4/rQt+0DZWQCRk9PwhBuX/shqdUiHeKicSLSkzWx+QZgTRE+/A==", - "bundleDependencies": [ - "@isaacs/string-locale-compare", - "@npmcli/arborist", - "@npmcli/config", - "@npmcli/map-workspaces", - "@npmcli/package-json", - "@npmcli/run-script", - "abbrev", - "archy", - "cacache", - "chalk", - "ci-info", - "cli-columns", - "cli-table3", - "columnify", - "fastest-levenshtein", - "fs-minipass", - "glob", - "graceful-fs", - "hosted-git-info", - "ini", - "init-package-json", - "is-cidr", - "json-parse-even-better-errors", - "libnpmaccess", - "libnpmdiff", - "libnpmexec", - "libnpmfund", - "libnpmhook", - "libnpmorg", - "libnpmpack", - "libnpmpublish", - "libnpmsearch", - "libnpmteam", - "libnpmversion", - "make-fetch-happen", - "minimatch", - "minipass", - "minipass-pipeline", - "ms", - "node-gyp", - "nopt", - "npm-audit-report", - "npm-install-checks", - "npm-package-arg", - "npm-pick-manifest", - "npm-profile", - "npm-registry-fetch", - "npm-user-validate", - "npmlog", - "p-map", - "pacote", - "parse-conflict-json", - "proc-log", - "qrcode-terminal", - "read", - "read-package-json", - "read-package-json-fast", - "semver", - "ssri", - "tar", - "text-table", - "tiny-relative-date", - "treeverse", - "validate-npm-package-name", - "which", - "write-file-atomic" - ], - "dev": true, - "dependencies": { - "@isaacs/string-locale-compare": "^1.1.0", - "@npmcli/arborist": "^6.2.5", - "@npmcli/config": "^6.1.4", - "@npmcli/map-workspaces": "^3.0.2", - "@npmcli/package-json": "^3.0.0", - "@npmcli/run-script": "^6.0.0", - "abbrev": "^2.0.0", - "archy": "~1.0.0", - "cacache": "^17.0.4", - "chalk": "^4.1.2", - "ci-info": "^3.8.0", - "cli-columns": "^4.0.0", - "cli-table3": "^0.6.3", - "columnify": "^1.6.0", - "fastest-levenshtein": "^1.0.16", - "fs-minipass": "^3.0.1", - "glob": "^8.1.0", - "graceful-fs": "^4.2.10", - "hosted-git-info": "^6.1.1", - "ini": "^3.0.1", - "init-package-json": "^5.0.0", - "is-cidr": "^4.0.2", - "json-parse-even-better-errors": "^3.0.0", - "libnpmaccess": "^7.0.2", - "libnpmdiff": "^5.0.13", - "libnpmexec": "^5.0.13", - "libnpmfund": "^4.0.13", - "libnpmhook": "^9.0.3", - "libnpmorg": "^5.0.3", - "libnpmpack": "^5.0.13", - "libnpmpublish": "^7.1.2", - "libnpmsearch": "^6.0.2", - "libnpmteam": "^5.0.3", - "libnpmversion": "^4.0.2", - "make-fetch-happen": "^11.0.3", - "minimatch": "^6.2.0", - "minipass": "^4.2.4", - "minipass-pipeline": "^1.2.4", - "ms": "^2.1.2", - "node-gyp": "^9.3.1", - "nopt": "^7.0.0", - "npm-audit-report": "^4.0.0", - "npm-install-checks": "^6.0.0", - "npm-package-arg": "^10.1.0", - "npm-pick-manifest": "^8.0.1", - "npm-profile": "^7.0.1", - "npm-registry-fetch": "^14.0.3", - "npm-user-validate": "^2.0.0", - "npmlog": "^7.0.1", - "p-map": "^4.0.0", - "pacote": "^15.1.1", - "parse-conflict-json": "^3.0.0", - "proc-log": "^3.0.0", - "qrcode-terminal": "^0.12.0", - "read": "^2.0.0", - "read-package-json": "^6.0.0", - "read-package-json-fast": "^3.0.2", - "semver": "^7.3.8", - "ssri": "^10.0.1", - "tar": "^6.1.13", - "text-table": "~0.2.0", - "tiny-relative-date": "^1.3.0", - "treeverse": "^3.0.0", - "validate-npm-package-name": "^5.0.0", - "which": "^3.0.0", - "write-file-atomic": "^5.0.0" - }, - "bin": { - "npm": "bin/npm-cli.js", - "npx": "bin/npx-cli.js" - }, - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" - } - }, - "node_modules/npm-run-path": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", - "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", - "dev": true, - "dependencies": { - "path-key": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/npm/node_modules/@colors/colors": { - "version": "1.5.0", - "dev": true, - "inBundle": true, - "license": "MIT", - "optional": true, - "engines": { - "node": ">=0.1.90" - } - }, - "node_modules/npm/node_modules/@gar/promisify": { - "version": "1.1.3", - "dev": true, - "inBundle": true, - "license": "MIT" - }, - "node_modules/npm/node_modules/@isaacs/string-locale-compare": { - "version": "1.1.0", - "dev": true, - "inBundle": true, - "license": "ISC" - }, - "node_modules/npm/node_modules/@npmcli/arborist": { - "version": "6.2.5", - "dev": true, - "inBundle": true, - "license": "ISC", - "dependencies": { - "@isaacs/string-locale-compare": "^1.1.0", - "@npmcli/fs": "^3.1.0", - "@npmcli/installed-package-contents": "^2.0.2", - "@npmcli/map-workspaces": "^3.0.2", - "@npmcli/metavuln-calculator": "^5.0.0", - "@npmcli/name-from-folder": "^2.0.0", - "@npmcli/node-gyp": "^3.0.0", - "@npmcli/package-json": "^3.0.0", - "@npmcli/query": "^3.0.0", - "@npmcli/run-script": "^6.0.0", - "bin-links": "^4.0.1", - "cacache": "^17.0.4", - "common-ancestor-path": "^1.0.1", - "hosted-git-info": "^6.1.1", - "json-parse-even-better-errors": "^3.0.0", - "json-stringify-nice": "^1.1.4", - "minimatch": "^6.1.6", - "nopt": "^7.0.0", - "npm-install-checks": "^6.0.0", - "npm-package-arg": "^10.1.0", - "npm-pick-manifest": "^8.0.1", - "npm-registry-fetch": "^14.0.3", - "npmlog": "^7.0.1", - "pacote": "^15.0.8", - "parse-conflict-json": "^3.0.0", - "proc-log": "^3.0.0", - "promise-all-reject-late": "^1.0.0", - "promise-call-limit": "^1.0.1", - "read-package-json-fast": "^3.0.2", - "semver": "^7.3.7", - "ssri": "^10.0.1", - "treeverse": "^3.0.0", - "walk-up-path": "^1.0.0" - }, - "bin": { - "arborist": "bin/index.js" - }, - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" - } - }, - "node_modules/npm/node_modules/@npmcli/config": { - "version": "6.1.4", - "dev": true, - "inBundle": true, - "license": "ISC", - "dependencies": { - "@npmcli/map-workspaces": "^3.0.2", - "ini": "^3.0.0", - "nopt": "^7.0.0", - "proc-log": "^3.0.0", - "read-package-json-fast": "^3.0.2", - "semver": "^7.3.5", - "walk-up-path": "^1.0.0" - }, - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" - } - }, - "node_modules/npm/node_modules/@npmcli/disparity-colors": { - "version": "3.0.0", - "dev": true, - "inBundle": true, - "license": "ISC", - "dependencies": { - "ansi-styles": "^4.3.0" - }, - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" - } - }, - "node_modules/npm/node_modules/@npmcli/fs": { - "version": "3.1.0", - "dev": true, - "inBundle": true, - "license": "ISC", - "dependencies": { - "semver": "^7.3.5" - }, - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" - } - }, - "node_modules/npm/node_modules/@npmcli/git": { - "version": "4.0.3", - "dev": true, - "inBundle": true, - "license": "ISC", - "dependencies": { - "@npmcli/promise-spawn": "^6.0.0", - "lru-cache": "^7.4.4", - "mkdirp": "^1.0.4", - "npm-pick-manifest": "^8.0.0", - "proc-log": "^3.0.0", - "promise-inflight": "^1.0.1", - "promise-retry": "^2.0.1", - "semver": "^7.3.5", - "which": "^3.0.0" - }, - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" - } - }, - "node_modules/npm/node_modules/@npmcli/installed-package-contents": { - "version": "2.0.2", - "dev": true, - "inBundle": true, - "license": "ISC", - "dependencies": { - "npm-bundled": "^3.0.0", - "npm-normalize-package-bin": "^3.0.0" - }, - "bin": { - "installed-package-contents": "lib/index.js" - }, - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" - } - }, - "node_modules/npm/node_modules/@npmcli/map-workspaces": { - "version": "3.0.2", - "dev": true, - "inBundle": true, - "license": "ISC", - "dependencies": { - "@npmcli/name-from-folder": "^2.0.0", - "glob": "^8.0.1", - "minimatch": "^6.1.6", - "read-package-json-fast": "^3.0.0" - }, - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" - } - }, - "node_modules/npm/node_modules/@npmcli/metavuln-calculator": { - "version": "5.0.0", - "dev": true, - "inBundle": true, - "license": "ISC", - "dependencies": { - "cacache": "^17.0.0", - "json-parse-even-better-errors": "^3.0.0", - "pacote": "^15.0.0", - "semver": "^7.3.5" - }, - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" - } - }, - "node_modules/npm/node_modules/@npmcli/move-file": { - "version": "2.0.1", - "dev": true, - "inBundle": true, - "license": "MIT", - "dependencies": { - "mkdirp": "^1.0.4", - "rimraf": "^3.0.2" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || >=16.0.0" - } - }, - "node_modules/npm/node_modules/@npmcli/name-from-folder": { - "version": "2.0.0", - "dev": true, - "inBundle": true, - "license": "ISC", - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" - } - }, - "node_modules/npm/node_modules/@npmcli/node-gyp": { - "version": "3.0.0", - "dev": true, - "inBundle": true, - "license": "ISC", - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" - } - }, - "node_modules/npm/node_modules/@npmcli/package-json": { - "version": "3.0.0", - "dev": true, - "inBundle": true, - "license": "ISC", - "dependencies": { - "json-parse-even-better-errors": "^3.0.0" - }, - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" - } - }, - "node_modules/npm/node_modules/@npmcli/promise-spawn": { - "version": "6.0.2", - "dev": true, - "inBundle": true, - "license": "ISC", - "dependencies": { - "which": "^3.0.0" - }, - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" - } - }, - "node_modules/npm/node_modules/@npmcli/query": { - "version": "3.0.0", - "dev": true, - "inBundle": true, - "license": "ISC", - "dependencies": { - "postcss-selector-parser": "^6.0.10" - }, - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" - } - }, - "node_modules/npm/node_modules/@npmcli/run-script": { - "version": "6.0.0", - "dev": true, - "inBundle": true, - "license": "ISC", - "dependencies": { - "@npmcli/node-gyp": "^3.0.0", - "@npmcli/promise-spawn": "^6.0.0", - "node-gyp": "^9.0.0", - "read-package-json-fast": "^3.0.0", - "which": "^3.0.0" - }, - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" - } - }, - "node_modules/npm/node_modules/@sigstore/protobuf-specs": { - "version": "0.1.0", - "dev": true, - "inBundle": true, - "license": "Apache-2.0", - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" - } - }, - "node_modules/npm/node_modules/@tootallnate/once": { - "version": "2.0.0", - "dev": true, - "inBundle": true, - "license": "MIT", - "engines": { - "node": ">= 10" - } - }, - "node_modules/npm/node_modules/@tufjs/models": { - "version": "1.0.0", - "dev": true, - "inBundle": true, - "license": "MIT", - "dependencies": { - "minimatch": "^6.1.0" - }, - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" - } - }, - "node_modules/npm/node_modules/abbrev": { - "version": "2.0.0", - "dev": true, - "inBundle": true, - "license": "ISC", - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" - } - }, - "node_modules/npm/node_modules/abort-controller": { - "version": "3.0.0", - "dev": true, - "inBundle": true, - "license": "MIT", - "dependencies": { - "event-target-shim": "^5.0.0" - }, - "engines": { - "node": ">=6.5" - } - }, - "node_modules/npm/node_modules/agent-base": { - "version": "6.0.2", - "dev": true, - "inBundle": true, - "license": "MIT", - "dependencies": { - "debug": "4" - }, - "engines": { - "node": ">= 6.0.0" - } - }, - "node_modules/npm/node_modules/agentkeepalive": { - "version": "4.3.0", - "dev": true, - "inBundle": true, - "license": "MIT", - "dependencies": { - "debug": "^4.1.0", - "depd": "^2.0.0", - "humanize-ms": "^1.2.1" - }, - "engines": { - "node": ">= 8.0.0" - } - }, - "node_modules/npm/node_modules/aggregate-error": { - "version": "3.1.0", - "dev": true, - "inBundle": true, - "license": "MIT", - "dependencies": { - "clean-stack": "^2.0.0", - "indent-string": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/npm/node_modules/ansi-regex": { - "version": "5.0.1", - "dev": true, - "inBundle": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/npm/node_modules/ansi-styles": { - "version": "4.3.0", - "dev": true, - "inBundle": true, - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/npm/node_modules/aproba": { - "version": "2.0.0", - "dev": true, - "inBundle": true, - "license": "ISC" - }, - "node_modules/npm/node_modules/archy": { - "version": "1.0.0", - "dev": true, - "inBundle": true, - "license": "MIT" - }, - "node_modules/npm/node_modules/are-we-there-yet": { - "version": "4.0.0", - "dev": true, - "inBundle": true, - "license": "ISC", - "dependencies": { - "delegates": "^1.0.0", - "readable-stream": "^4.1.0" - }, - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" - } - }, - "node_modules/npm/node_modules/balanced-match": { - "version": "1.0.2", - "dev": true, - "inBundle": true, - "license": "MIT" - }, - "node_modules/npm/node_modules/base64-js": { - "version": "1.5.1", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "inBundle": true, - "license": "MIT" - }, - "node_modules/npm/node_modules/bin-links": { - "version": "4.0.1", - "dev": true, - "inBundle": true, - "license": "ISC", - "dependencies": { - "cmd-shim": "^6.0.0", - "npm-normalize-package-bin": "^3.0.0", - "read-cmd-shim": "^4.0.0", - "write-file-atomic": "^5.0.0" - }, - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" - } - }, - "node_modules/npm/node_modules/binary-extensions": { - "version": "2.2.0", - "dev": true, - "inBundle": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/npm/node_modules/brace-expansion": { - "version": "2.0.1", - "dev": true, - "inBundle": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/npm/node_modules/buffer": { - "version": "6.0.3", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "inBundle": true, - "license": "MIT", - "dependencies": { - "base64-js": "^1.3.1", - "ieee754": "^1.2.1" - } - }, - "node_modules/npm/node_modules/builtins": { - "version": "5.0.1", - "dev": true, - "inBundle": true, - "license": "MIT", - "dependencies": { - "semver": "^7.0.0" - } - }, - "node_modules/npm/node_modules/cacache": { - "version": "17.0.4", - "dev": true, - "inBundle": true, - "license": "ISC", - "dependencies": { - "@npmcli/fs": "^3.1.0", - "fs-minipass": "^3.0.0", - "glob": "^8.0.1", - "lru-cache": "^7.7.1", - "minipass": "^4.0.0", - "minipass-collect": "^1.0.2", - "minipass-flush": "^1.0.5", - "minipass-pipeline": "^1.2.4", - "p-map": "^4.0.0", - "promise-inflight": "^1.0.1", - "ssri": "^10.0.0", - "tar": "^6.1.11", - "unique-filename": "^3.0.0" - }, - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" - } - }, - "node_modules/npm/node_modules/chalk": { - "version": "4.1.2", - "dev": true, - "inBundle": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/npm/node_modules/chownr": { - "version": "2.0.0", - "dev": true, - "inBundle": true, - "license": "ISC", - "engines": { - "node": ">=10" - } - }, - "node_modules/npm/node_modules/ci-info": { - "version": "3.8.0", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/sibiraj-s" - } - ], - "inBundle": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/npm/node_modules/cidr-regex": { - "version": "3.1.1", - "dev": true, - "inBundle": true, - "license": "BSD-2-Clause", - "dependencies": { - "ip-regex": "^4.1.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/npm/node_modules/clean-stack": { - "version": "2.2.0", - "dev": true, - "inBundle": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/npm/node_modules/cli-columns": { - "version": "4.0.0", - "dev": true, - "inBundle": true, - "license": "MIT", - "dependencies": { - "string-width": "^4.2.3", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">= 10" - } - }, - "node_modules/npm/node_modules/cli-table3": { - "version": "0.6.3", - "dev": true, - "inBundle": true, - "license": "MIT", - "dependencies": { - "string-width": "^4.2.0" - }, - "engines": { - "node": "10.* || >= 12.*" - }, - "optionalDependencies": { - "@colors/colors": "1.5.0" - } - }, - "node_modules/npm/node_modules/clone": { - "version": "1.0.4", - "dev": true, - "inBundle": true, - "license": "MIT", - "engines": { - "node": ">=0.8" - } - }, - "node_modules/npm/node_modules/cmd-shim": { - "version": "6.0.1", - "dev": true, - "inBundle": true, - "license": "ISC", - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" - } - }, - "node_modules/npm/node_modules/color-convert": { - "version": "2.0.1", - "dev": true, - "inBundle": true, - "license": "MIT", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/npm/node_modules/color-name": { - "version": "1.1.4", - "dev": true, - "inBundle": true, - "license": "MIT" - }, - "node_modules/npm/node_modules/color-support": { - "version": "1.1.3", - "dev": true, - "inBundle": true, - "license": "ISC", - "bin": { - "color-support": "bin.js" - } - }, - "node_modules/npm/node_modules/columnify": { - "version": "1.6.0", - "dev": true, - "inBundle": true, - "license": "MIT", - "dependencies": { - "strip-ansi": "^6.0.1", - "wcwidth": "^1.0.0" - }, - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/npm/node_modules/common-ancestor-path": { - "version": "1.0.1", - "dev": true, - "inBundle": true, - "license": "ISC" - }, - "node_modules/npm/node_modules/concat-map": { - "version": "0.0.1", - "dev": true, - "inBundle": true, - "license": "MIT" - }, - "node_modules/npm/node_modules/console-control-strings": { - "version": "1.1.0", - "dev": true, - "inBundle": true, - "license": "ISC" - }, - "node_modules/npm/node_modules/cssesc": { - "version": "3.0.0", - "dev": true, - "inBundle": true, - "license": "MIT", - "bin": { - "cssesc": "bin/cssesc" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/npm/node_modules/debug": { - "version": "4.3.4", - "dev": true, - "inBundle": true, - "license": "MIT", - "dependencies": { - "ms": "2.1.2" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/npm/node_modules/debug/node_modules/ms": { - "version": "2.1.2", - "dev": true, - "inBundle": true, - "license": "MIT" - }, - "node_modules/npm/node_modules/defaults": { - "version": "1.0.4", - "dev": true, - "inBundle": true, - "license": "MIT", - "dependencies": { - "clone": "^1.0.2" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/npm/node_modules/delegates": { - "version": "1.0.0", - "dev": true, - "inBundle": true, - "license": "MIT" - }, - "node_modules/npm/node_modules/depd": { - "version": "2.0.0", - "dev": true, - "inBundle": true, - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/npm/node_modules/diff": { - "version": "5.1.0", - "dev": true, - "inBundle": true, - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.3.1" - } - }, - "node_modules/npm/node_modules/emoji-regex": { - "version": "8.0.0", - "dev": true, - "inBundle": true, - "license": "MIT" - }, - "node_modules/npm/node_modules/encoding": { - "version": "0.1.13", - "dev": true, - "inBundle": true, - "license": "MIT", - "optional": true, - "dependencies": { - "iconv-lite": "^0.6.2" - } - }, - "node_modules/npm/node_modules/env-paths": { - "version": "2.2.1", - "dev": true, - "inBundle": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/npm/node_modules/err-code": { - "version": "2.0.3", - "dev": true, - "inBundle": true, - "license": "MIT" - }, - "node_modules/npm/node_modules/event-target-shim": { - "version": "5.0.1", - "dev": true, - "inBundle": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/npm/node_modules/events": { - "version": "3.3.0", - "dev": true, - "inBundle": true, - "license": "MIT", - "engines": { - "node": ">=0.8.x" - } - }, - "node_modules/npm/node_modules/fastest-levenshtein": { - "version": "1.0.16", - "dev": true, - "inBundle": true, - "license": "MIT", - "engines": { - "node": ">= 4.9.1" - } - }, - "node_modules/npm/node_modules/fs-minipass": { - "version": "3.0.1", - "dev": true, - "inBundle": true, - "license": "ISC", - "dependencies": { - "minipass": "^4.0.0" - }, - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" - } - }, - "node_modules/npm/node_modules/fs.realpath": { - "version": "1.0.0", - "dev": true, - "inBundle": true, - "license": "ISC" - }, - "node_modules/npm/node_modules/function-bind": { - "version": "1.1.1", - "dev": true, - "inBundle": true, - "license": "MIT" - }, - "node_modules/npm/node_modules/gauge": { - "version": "5.0.0", - "dev": true, - "inBundle": true, - "license": "ISC", - "dependencies": { - "aproba": "^1.0.3 || ^2.0.0", - "color-support": "^1.1.3", - "console-control-strings": "^1.1.0", - "has-unicode": "^2.0.1", - "signal-exit": "^3.0.7", - "string-width": "^4.2.3", - "strip-ansi": "^6.0.1", - "wide-align": "^1.1.5" - }, - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" - } - }, - "node_modules/npm/node_modules/glob": { - "version": "8.1.0", - "dev": true, - "inBundle": true, - "license": "ISC", - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^5.0.1", - "once": "^1.3.0" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/npm/node_modules/glob/node_modules/minimatch": { - "version": "5.1.6", - "dev": true, - "inBundle": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/npm/node_modules/graceful-fs": { - "version": "4.2.10", - "dev": true, - "inBundle": true, - "license": "ISC" - }, - "node_modules/npm/node_modules/has": { - "version": "1.0.3", - "dev": true, - "inBundle": true, - "license": "MIT", - "dependencies": { - "function-bind": "^1.1.1" - }, - "engines": { - "node": ">= 0.4.0" - } - }, - "node_modules/npm/node_modules/has-flag": { - "version": "4.0.0", - "dev": true, - "inBundle": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/npm/node_modules/has-unicode": { - "version": "2.0.1", - "dev": true, - "inBundle": true, - "license": "ISC" - }, - "node_modules/npm/node_modules/hosted-git-info": { - "version": "6.1.1", - "dev": true, - "inBundle": true, - "license": "ISC", - "dependencies": { - "lru-cache": "^7.5.1" - }, - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" - } - }, - "node_modules/npm/node_modules/http-cache-semantics": { - "version": "4.1.1", - "dev": true, - "inBundle": true, - "license": "BSD-2-Clause" - }, - "node_modules/npm/node_modules/http-proxy-agent": { - "version": "5.0.0", - "dev": true, - "inBundle": true, - "license": "MIT", - "dependencies": { - "@tootallnate/once": "2", - "agent-base": "6", - "debug": "4" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/npm/node_modules/https-proxy-agent": { - "version": "5.0.1", - "dev": true, - "inBundle": true, - "license": "MIT", - "dependencies": { - "agent-base": "6", - "debug": "4" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/npm/node_modules/humanize-ms": { - "version": "1.2.1", - "dev": true, - "inBundle": true, - "license": "MIT", - "dependencies": { - "ms": "^2.0.0" - } - }, - "node_modules/npm/node_modules/iconv-lite": { - "version": "0.6.3", - "dev": true, - "inBundle": true, - "license": "MIT", - "optional": true, - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/npm/node_modules/ieee754": { - "version": "1.2.1", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "inBundle": true, - "license": "BSD-3-Clause" - }, - "node_modules/npm/node_modules/ignore-walk": { - "version": "6.0.1", - "dev": true, - "inBundle": true, - "license": "ISC", - "dependencies": { - "minimatch": "^6.1.6" - }, - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" - } - }, - "node_modules/npm/node_modules/imurmurhash": { - "version": "0.1.4", - "dev": true, - "inBundle": true, - "license": "MIT", - "engines": { - "node": ">=0.8.19" - } - }, - "node_modules/npm/node_modules/indent-string": { - "version": "4.0.0", - "dev": true, - "inBundle": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/npm/node_modules/infer-owner": { - "version": "1.0.4", - "dev": true, - "inBundle": true, - "license": "ISC" - }, - "node_modules/npm/node_modules/inflight": { - "version": "1.0.6", - "dev": true, - "inBundle": true, - "license": "ISC", - "dependencies": { - "once": "^1.3.0", - "wrappy": "1" - } - }, - "node_modules/npm/node_modules/inherits": { - "version": "2.0.4", - "dev": true, - "inBundle": true, - "license": "ISC" - }, - "node_modules/npm/node_modules/ini": { - "version": "3.0.1", - "dev": true, - "inBundle": true, - "license": "ISC", - "engines": { - "node": "^12.13.0 || ^14.15.0 || >=16.0.0" - } - }, - "node_modules/npm/node_modules/init-package-json": { - "version": "5.0.0", - "dev": true, - "inBundle": true, - "license": "ISC", - "dependencies": { - "npm-package-arg": "^10.0.0", - "promzard": "^1.0.0", - "read": "^2.0.0", - "read-package-json": "^6.0.0", - "semver": "^7.3.5", - "validate-npm-package-license": "^3.0.4", - "validate-npm-package-name": "^5.0.0" - }, - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" - } - }, - "node_modules/npm/node_modules/ip": { - "version": "2.0.0", - "dev": true, - "inBundle": true, - "license": "MIT" - }, - "node_modules/npm/node_modules/ip-regex": { - "version": "4.3.0", - "dev": true, - "inBundle": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/npm/node_modules/is-cidr": { - "version": "4.0.2", - "dev": true, - "inBundle": true, - "license": "BSD-2-Clause", - "dependencies": { - "cidr-regex": "^3.1.1" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/npm/node_modules/is-core-module": { - "version": "2.11.0", - "dev": true, - "inBundle": true, - "license": "MIT", - "dependencies": { - "has": "^1.0.3" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/npm/node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "dev": true, - "inBundle": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/npm/node_modules/is-lambda": { - "version": "1.0.1", - "dev": true, - "inBundle": true, - "license": "MIT" - }, - "node_modules/npm/node_modules/isexe": { - "version": "2.0.0", - "dev": true, - "inBundle": true, - "license": "ISC" - }, - "node_modules/npm/node_modules/json-parse-even-better-errors": { - "version": "3.0.0", - "dev": true, - "inBundle": true, - "license": "MIT", - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" - } - }, - "node_modules/npm/node_modules/json-stringify-nice": { - "version": "1.1.4", - "dev": true, - "inBundle": true, - "license": "ISC", - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/npm/node_modules/jsonparse": { - "version": "1.3.1", - "dev": true, - "engines": [ - "node >= 0.2.0" - ], - "inBundle": true, - "license": "MIT" - }, - "node_modules/npm/node_modules/just-diff": { - "version": "5.2.0", - "dev": true, - "inBundle": true, - "license": "MIT" - }, - "node_modules/npm/node_modules/just-diff-apply": { - "version": "5.5.0", - "dev": true, - "inBundle": true, - "license": "MIT" - }, - "node_modules/npm/node_modules/libnpmaccess": { - "version": "7.0.2", - "dev": true, - "inBundle": true, - "license": "ISC", - "dependencies": { - "npm-package-arg": "^10.1.0", - "npm-registry-fetch": "^14.0.3" - }, - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" - } - }, - "node_modules/npm/node_modules/libnpmdiff": { - "version": "5.0.13", - "dev": true, - "inBundle": true, - "license": "ISC", - "dependencies": { - "@npmcli/arborist": "^6.2.5", - "@npmcli/disparity-colors": "^3.0.0", - "@npmcli/installed-package-contents": "^2.0.2", - "binary-extensions": "^2.2.0", - "diff": "^5.1.0", - "minimatch": "^6.1.6", - "npm-package-arg": "^10.1.0", - "pacote": "^15.0.8", - "tar": "^6.1.13" - }, - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" - } - }, - "node_modules/npm/node_modules/libnpmexec": { - "version": "5.0.13", - "dev": true, - "inBundle": true, - "license": "ISC", - "dependencies": { - "@npmcli/arborist": "^6.2.5", - "@npmcli/run-script": "^6.0.0", - "chalk": "^4.1.0", - "ci-info": "^3.7.1", - "npm-package-arg": "^10.1.0", - "npmlog": "^7.0.1", - "pacote": "^15.0.8", - "proc-log": "^3.0.0", - "read": "^2.0.0", - "read-package-json-fast": "^3.0.2", - "semver": "^7.3.7", - "walk-up-path": "^1.0.0" - }, - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" - } - }, - "node_modules/npm/node_modules/libnpmfund": { - "version": "4.0.13", - "dev": true, - "inBundle": true, - "license": "ISC", - "dependencies": { - "@npmcli/arborist": "^6.2.5" - }, - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" - } - }, - "node_modules/npm/node_modules/libnpmhook": { - "version": "9.0.3", - "dev": true, - "inBundle": true, - "license": "ISC", - "dependencies": { - "aproba": "^2.0.0", - "npm-registry-fetch": "^14.0.3" - }, - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" - } - }, - "node_modules/npm/node_modules/libnpmorg": { - "version": "5.0.3", - "dev": true, - "inBundle": true, - "license": "ISC", - "dependencies": { - "aproba": "^2.0.0", - "npm-registry-fetch": "^14.0.3" - }, - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" - } - }, - "node_modules/npm/node_modules/libnpmpack": { - "version": "5.0.13", - "dev": true, - "inBundle": true, - "license": "ISC", - "dependencies": { - "@npmcli/arborist": "^6.2.5", - "@npmcli/run-script": "^6.0.0", - "npm-package-arg": "^10.1.0", - "pacote": "^15.0.8" - }, - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" - } - }, - "node_modules/npm/node_modules/libnpmpublish": { - "version": "7.1.2", - "dev": true, - "inBundle": true, - "license": "ISC", - "dependencies": { - "ci-info": "^3.6.1", - "normalize-package-data": "^5.0.0", - "npm-package-arg": "^10.1.0", - "npm-registry-fetch": "^14.0.3", - "proc-log": "^3.0.0", - "semver": "^7.3.7", - "sigstore": "^1.0.0", - "ssri": "^10.0.1" - }, - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" - } - }, - "node_modules/npm/node_modules/libnpmsearch": { - "version": "6.0.2", - "dev": true, - "inBundle": true, - "license": "ISC", - "dependencies": { - "npm-registry-fetch": "^14.0.3" - }, - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" - } - }, - "node_modules/npm/node_modules/libnpmteam": { - "version": "5.0.3", - "dev": true, - "inBundle": true, - "license": "ISC", - "dependencies": { - "aproba": "^2.0.0", - "npm-registry-fetch": "^14.0.3" - }, - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" - } - }, - "node_modules/npm/node_modules/libnpmversion": { - "version": "4.0.2", - "dev": true, - "inBundle": true, - "license": "ISC", - "dependencies": { - "@npmcli/git": "^4.0.1", - "@npmcli/run-script": "^6.0.0", - "json-parse-even-better-errors": "^3.0.0", - "proc-log": "^3.0.0", - "semver": "^7.3.7" - }, - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" - } - }, - "node_modules/npm/node_modules/lru-cache": { - "version": "7.18.3", - "dev": true, - "inBundle": true, - "license": "ISC", - "engines": { - "node": ">=12" - } - }, - "node_modules/npm/node_modules/make-fetch-happen": { - "version": "11.0.3", - "dev": true, - "inBundle": true, - "license": "ISC", - "dependencies": { - "agentkeepalive": "^4.2.1", - "cacache": "^17.0.0", - "http-cache-semantics": "^4.1.1", - "http-proxy-agent": "^5.0.0", - "https-proxy-agent": "^5.0.0", - "is-lambda": "^1.0.1", - "lru-cache": "^7.7.1", - "minipass": "^4.0.0", - "minipass-fetch": "^3.0.0", - "minipass-flush": "^1.0.5", - "minipass-pipeline": "^1.2.4", - "negotiator": "^0.6.3", - "promise-retry": "^2.0.1", - "socks-proxy-agent": "^7.0.0", - "ssri": "^10.0.0" - }, - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" - } - }, - "node_modules/npm/node_modules/minimatch": { - "version": "6.2.0", - "dev": true, - "inBundle": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/npm/node_modules/minipass": { - "version": "4.2.4", - "dev": true, - "inBundle": true, - "license": "ISC", - "engines": { - "node": ">=8" - } - }, - "node_modules/npm/node_modules/minipass-collect": { - "version": "1.0.2", - "dev": true, - "inBundle": true, - "license": "ISC", - "dependencies": { - "minipass": "^3.0.0" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/npm/node_modules/minipass-collect/node_modules/minipass": { - "version": "3.3.6", - "dev": true, - "inBundle": true, - "license": "ISC", - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/npm/node_modules/minipass-fetch": { - "version": "3.0.1", - "dev": true, - "inBundle": true, - "license": "MIT", - "dependencies": { - "minipass": "^4.0.0", - "minipass-sized": "^1.0.3", - "minizlib": "^2.1.2" - }, - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" - }, - "optionalDependencies": { - "encoding": "^0.1.13" - } - }, - "node_modules/npm/node_modules/minipass-flush": { - "version": "1.0.5", - "dev": true, - "inBundle": true, - "license": "ISC", - "dependencies": { - "minipass": "^3.0.0" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/npm/node_modules/minipass-flush/node_modules/minipass": { - "version": "3.3.6", - "dev": true, - "inBundle": true, - "license": "ISC", - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/npm/node_modules/minipass-json-stream": { - "version": "1.0.1", - "dev": true, - "inBundle": true, - "license": "MIT", - "dependencies": { - "jsonparse": "^1.3.1", - "minipass": "^3.0.0" - } - }, - "node_modules/npm/node_modules/minipass-json-stream/node_modules/minipass": { - "version": "3.3.6", - "dev": true, - "inBundle": true, - "license": "ISC", - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/npm/node_modules/minipass-pipeline": { - "version": "1.2.4", - "dev": true, - "inBundle": true, - "license": "ISC", - "dependencies": { - "minipass": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/npm/node_modules/minipass-pipeline/node_modules/minipass": { - "version": "3.3.6", - "dev": true, - "inBundle": true, - "license": "ISC", - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/npm/node_modules/minipass-sized": { - "version": "1.0.3", - "dev": true, - "inBundle": true, - "license": "ISC", - "dependencies": { - "minipass": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/npm/node_modules/minipass-sized/node_modules/minipass": { - "version": "3.3.6", - "dev": true, - "inBundle": true, - "license": "ISC", - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/npm/node_modules/minizlib": { - "version": "2.1.2", - "dev": true, - "inBundle": true, - "license": "MIT", - "dependencies": { - "minipass": "^3.0.0", - "yallist": "^4.0.0" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/npm/node_modules/minizlib/node_modules/minipass": { - "version": "3.3.6", - "dev": true, - "inBundle": true, - "license": "ISC", - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/npm/node_modules/mkdirp": { - "version": "1.0.4", - "dev": true, - "inBundle": true, - "license": "MIT", - "bin": { - "mkdirp": "bin/cmd.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/npm/node_modules/ms": { - "version": "2.1.3", - "dev": true, - "inBundle": true, - "license": "MIT" - }, - "node_modules/npm/node_modules/mute-stream": { - "version": "1.0.0", - "dev": true, - "inBundle": true, - "license": "ISC", - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" - } - }, - "node_modules/npm/node_modules/negotiator": { - "version": "0.6.3", - "dev": true, - "inBundle": true, - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/npm/node_modules/node-gyp": { - "version": "9.3.1", - "dev": true, - "inBundle": true, - "license": "MIT", - "dependencies": { - "env-paths": "^2.2.0", - "glob": "^7.1.4", - "graceful-fs": "^4.2.6", - "make-fetch-happen": "^10.0.3", - "nopt": "^6.0.0", - "npmlog": "^6.0.0", - "rimraf": "^3.0.2", - "semver": "^7.3.5", - "tar": "^6.1.2", - "which": "^2.0.2" - }, - "bin": { - "node-gyp": "bin/node-gyp.js" - }, - "engines": { - "node": "^12.13 || ^14.13 || >=16" - } - }, - "node_modules/npm/node_modules/node-gyp/node_modules/@npmcli/fs": { - "version": "2.1.2", - "dev": true, - "inBundle": true, - "license": "ISC", - "dependencies": { - "@gar/promisify": "^1.1.3", - "semver": "^7.3.5" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || >=16.0.0" - } - }, - "node_modules/npm/node_modules/node-gyp/node_modules/abbrev": { - "version": "1.1.1", - "dev": true, - "inBundle": true, - "license": "ISC" - }, - "node_modules/npm/node_modules/node-gyp/node_modules/are-we-there-yet": { - "version": "3.0.1", - "dev": true, - "inBundle": true, - "license": "ISC", - "dependencies": { - "delegates": "^1.0.0", - "readable-stream": "^3.6.0" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || >=16.0.0" - } - }, - "node_modules/npm/node_modules/node-gyp/node_modules/brace-expansion": { - "version": "1.1.11", - "dev": true, - "inBundle": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/npm/node_modules/node-gyp/node_modules/cacache": { - "version": "16.1.3", - "dev": true, - "inBundle": true, - "license": "ISC", - "dependencies": { - "@npmcli/fs": "^2.1.0", - "@npmcli/move-file": "^2.0.0", - "chownr": "^2.0.0", - "fs-minipass": "^2.1.0", - "glob": "^8.0.1", - "infer-owner": "^1.0.4", - "lru-cache": "^7.7.1", - "minipass": "^3.1.6", - "minipass-collect": "^1.0.2", - "minipass-flush": "^1.0.5", - "minipass-pipeline": "^1.2.4", - "mkdirp": "^1.0.4", - "p-map": "^4.0.0", - "promise-inflight": "^1.0.1", - "rimraf": "^3.0.2", - "ssri": "^9.0.0", - "tar": "^6.1.11", - "unique-filename": "^2.0.0" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || >=16.0.0" - } - }, - "node_modules/npm/node_modules/node-gyp/node_modules/cacache/node_modules/brace-expansion": { - "version": "2.0.1", - "dev": true, - "inBundle": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/npm/node_modules/node-gyp/node_modules/cacache/node_modules/glob": { - "version": "8.1.0", - "dev": true, - "inBundle": true, - "license": "ISC", - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^5.0.1", - "once": "^1.3.0" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/npm/node_modules/node-gyp/node_modules/cacache/node_modules/minimatch": { - "version": "5.1.6", - "dev": true, - "inBundle": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/npm/node_modules/node-gyp/node_modules/fs-minipass": { - "version": "2.1.0", - "dev": true, - "inBundle": true, - "license": "ISC", - "dependencies": { - "minipass": "^3.0.0" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/npm/node_modules/node-gyp/node_modules/gauge": { - "version": "4.0.4", - "dev": true, - "inBundle": true, - "license": "ISC", - "dependencies": { - "aproba": "^1.0.3 || ^2.0.0", - "color-support": "^1.1.3", - "console-control-strings": "^1.1.0", - "has-unicode": "^2.0.1", - "signal-exit": "^3.0.7", - "string-width": "^4.2.3", - "strip-ansi": "^6.0.1", - "wide-align": "^1.1.5" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || >=16.0.0" - } - }, - "node_modules/npm/node_modules/node-gyp/node_modules/glob": { - "version": "7.2.3", - "dev": true, - "inBundle": true, - "license": "ISC", - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - }, - "engines": { - "node": "*" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/npm/node_modules/node-gyp/node_modules/make-fetch-happen": { - "version": "10.2.1", - "dev": true, - "inBundle": true, - "license": "ISC", - "dependencies": { - "agentkeepalive": "^4.2.1", - "cacache": "^16.1.0", - "http-cache-semantics": "^4.1.0", - "http-proxy-agent": "^5.0.0", - "https-proxy-agent": "^5.0.0", - "is-lambda": "^1.0.1", - "lru-cache": "^7.7.1", - "minipass": "^3.1.6", - "minipass-collect": "^1.0.2", - "minipass-fetch": "^2.0.3", - "minipass-flush": "^1.0.5", - "minipass-pipeline": "^1.2.4", - "negotiator": "^0.6.3", - "promise-retry": "^2.0.1", - "socks-proxy-agent": "^7.0.0", - "ssri": "^9.0.0" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || >=16.0.0" - } - }, - "node_modules/npm/node_modules/node-gyp/node_modules/minimatch": { - "version": "3.1.2", - "dev": true, - "inBundle": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/npm/node_modules/node-gyp/node_modules/minipass": { - "version": "3.3.6", - "dev": true, - "inBundle": true, - "license": "ISC", - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/npm/node_modules/node-gyp/node_modules/minipass-fetch": { - "version": "2.1.2", - "dev": true, - "inBundle": true, - "license": "MIT", - "dependencies": { - "minipass": "^3.1.6", - "minipass-sized": "^1.0.3", - "minizlib": "^2.1.2" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || >=16.0.0" - }, - "optionalDependencies": { - "encoding": "^0.1.13" - } - }, - "node_modules/npm/node_modules/node-gyp/node_modules/nopt": { - "version": "6.0.0", - "dev": true, - "inBundle": true, - "license": "ISC", - "dependencies": { - "abbrev": "^1.0.0" - }, - "bin": { - "nopt": "bin/nopt.js" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || >=16.0.0" - } - }, - "node_modules/npm/node_modules/node-gyp/node_modules/npmlog": { - "version": "6.0.2", - "dev": true, - "inBundle": true, - "license": "ISC", - "dependencies": { - "are-we-there-yet": "^3.0.0", - "console-control-strings": "^1.1.0", - "gauge": "^4.0.3", - "set-blocking": "^2.0.0" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || >=16.0.0" - } - }, - "node_modules/npm/node_modules/node-gyp/node_modules/readable-stream": { - "version": "3.6.1", - "dev": true, - "inBundle": true, - "license": "MIT", - "dependencies": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/npm/node_modules/node-gyp/node_modules/ssri": { - "version": "9.0.1", - "dev": true, - "inBundle": true, - "license": "ISC", - "dependencies": { - "minipass": "^3.1.1" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || >=16.0.0" - } - }, - "node_modules/npm/node_modules/node-gyp/node_modules/unique-filename": { - "version": "2.0.1", - "dev": true, - "inBundle": true, - "license": "ISC", - "dependencies": { - "unique-slug": "^3.0.0" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || >=16.0.0" - } - }, - "node_modules/npm/node_modules/node-gyp/node_modules/unique-slug": { - "version": "3.0.0", - "dev": true, - "inBundle": true, - "license": "ISC", - "dependencies": { - "imurmurhash": "^0.1.4" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || >=16.0.0" - } - }, - "node_modules/npm/node_modules/node-gyp/node_modules/which": { - "version": "2.0.2", - "dev": true, - "inBundle": true, - "license": "ISC", - "dependencies": { - "isexe": "^2.0.0" - }, - "bin": { - "node-which": "bin/node-which" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/npm/node_modules/nopt": { - "version": "7.0.0", - "dev": true, - "inBundle": true, - "license": "ISC", - "dependencies": { - "abbrev": "^2.0.0" - }, - "bin": { - "nopt": "bin/nopt.js" - }, - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" - } - }, - "node_modules/npm/node_modules/normalize-package-data": { - "version": "5.0.0", - "dev": true, - "inBundle": true, - "license": "BSD-2-Clause", - "dependencies": { - "hosted-git-info": "^6.0.0", - "is-core-module": "^2.8.1", - "semver": "^7.3.5", - "validate-npm-package-license": "^3.0.4" - }, - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" - } - }, - "node_modules/npm/node_modules/npm-audit-report": { - "version": "4.0.0", - "dev": true, - "inBundle": true, - "license": "ISC", - "dependencies": { - "chalk": "^4.0.0" - }, - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" - } - }, - "node_modules/npm/node_modules/npm-bundled": { - "version": "3.0.0", - "dev": true, - "inBundle": true, - "license": "ISC", - "dependencies": { - "npm-normalize-package-bin": "^3.0.0" - }, - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" - } - }, - "node_modules/npm/node_modules/npm-install-checks": { - "version": "6.0.0", - "dev": true, - "inBundle": true, - "license": "BSD-2-Clause", - "dependencies": { - "semver": "^7.1.1" - }, - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" - } - }, - "node_modules/npm/node_modules/npm-normalize-package-bin": { - "version": "3.0.0", - "dev": true, - "inBundle": true, - "license": "ISC", - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" - } - }, - "node_modules/npm/node_modules/npm-package-arg": { - "version": "10.1.0", - "dev": true, - "inBundle": true, - "license": "ISC", - "dependencies": { - "hosted-git-info": "^6.0.0", - "proc-log": "^3.0.0", - "semver": "^7.3.5", - "validate-npm-package-name": "^5.0.0" - }, - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" - } - }, - "node_modules/npm/node_modules/npm-packlist": { - "version": "7.0.4", - "dev": true, - "inBundle": true, - "license": "ISC", - "dependencies": { - "ignore-walk": "^6.0.0" - }, - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" - } - }, - "node_modules/npm/node_modules/npm-pick-manifest": { - "version": "8.0.1", - "dev": true, - "inBundle": true, - "license": "ISC", - "dependencies": { - "npm-install-checks": "^6.0.0", - "npm-normalize-package-bin": "^3.0.0", - "npm-package-arg": "^10.0.0", - "semver": "^7.3.5" - }, - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" - } - }, - "node_modules/npm/node_modules/npm-profile": { - "version": "7.0.1", - "dev": true, - "inBundle": true, - "license": "ISC", - "dependencies": { - "npm-registry-fetch": "^14.0.0", - "proc-log": "^3.0.0" - }, - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" - } - }, - "node_modules/npm/node_modules/npm-registry-fetch": { - "version": "14.0.3", - "dev": true, - "inBundle": true, - "license": "ISC", - "dependencies": { - "make-fetch-happen": "^11.0.0", - "minipass": "^4.0.0", - "minipass-fetch": "^3.0.0", - "minipass-json-stream": "^1.0.1", - "minizlib": "^2.1.2", - "npm-package-arg": "^10.0.0", - "proc-log": "^3.0.0" - }, - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" - } - }, - "node_modules/npm/node_modules/npm-user-validate": { - "version": "2.0.0", - "dev": true, - "inBundle": true, - "license": "BSD-2-Clause", - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" - } - }, - "node_modules/npm/node_modules/npmlog": { - "version": "7.0.1", - "dev": true, - "inBundle": true, - "license": "ISC", - "dependencies": { - "are-we-there-yet": "^4.0.0", - "console-control-strings": "^1.1.0", - "gauge": "^5.0.0", - "set-blocking": "^2.0.0" - }, - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" - } - }, - "node_modules/npm/node_modules/once": { - "version": "1.4.0", - "dev": true, - "inBundle": true, - "license": "ISC", - "dependencies": { - "wrappy": "1" - } - }, - "node_modules/npm/node_modules/p-map": { - "version": "4.0.0", - "dev": true, - "inBundle": true, - "license": "MIT", - "dependencies": { - "aggregate-error": "^3.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/npm/node_modules/pacote": { - "version": "15.1.1", - "dev": true, - "inBundle": true, - "license": "ISC", - "dependencies": { - "@npmcli/git": "^4.0.0", - "@npmcli/installed-package-contents": "^2.0.1", - "@npmcli/promise-spawn": "^6.0.1", - "@npmcli/run-script": "^6.0.0", - "cacache": "^17.0.0", - "fs-minipass": "^3.0.0", - "minipass": "^4.0.0", - "npm-package-arg": "^10.0.0", - "npm-packlist": "^7.0.0", - "npm-pick-manifest": "^8.0.0", - "npm-registry-fetch": "^14.0.0", - "proc-log": "^3.0.0", - "promise-retry": "^2.0.1", - "read-package-json": "^6.0.0", - "read-package-json-fast": "^3.0.0", - "sigstore": "^1.0.0", - "ssri": "^10.0.0", - "tar": "^6.1.11" - }, - "bin": { - "pacote": "lib/bin.js" - }, - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" - } - }, - "node_modules/npm/node_modules/parse-conflict-json": { - "version": "3.0.0", - "dev": true, - "inBundle": true, - "license": "ISC", - "dependencies": { - "json-parse-even-better-errors": "^3.0.0", - "just-diff": "^5.0.1", - "just-diff-apply": "^5.2.0" - }, - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" - } - }, - "node_modules/npm/node_modules/path-is-absolute": { - "version": "1.0.1", - "dev": true, - "inBundle": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/npm/node_modules/postcss-selector-parser": { - "version": "6.0.11", - "dev": true, - "inBundle": true, - "license": "MIT", - "dependencies": { - "cssesc": "^3.0.0", - "util-deprecate": "^1.0.2" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/npm/node_modules/proc-log": { - "version": "3.0.0", - "dev": true, - "inBundle": true, - "license": "ISC", - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" - } - }, - "node_modules/npm/node_modules/process": { - "version": "0.11.10", - "dev": true, - "inBundle": true, - "license": "MIT", - "engines": { - "node": ">= 0.6.0" - } - }, - "node_modules/npm/node_modules/promise-all-reject-late": { - "version": "1.0.1", - "dev": true, - "inBundle": true, - "license": "ISC", - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/npm/node_modules/promise-call-limit": { - "version": "1.0.1", - "dev": true, - "inBundle": true, - "license": "ISC", - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/npm/node_modules/promise-inflight": { - "version": "1.0.1", - "dev": true, - "inBundle": true, - "license": "ISC" - }, - "node_modules/npm/node_modules/promise-retry": { - "version": "2.0.1", - "dev": true, - "inBundle": true, - "license": "MIT", - "dependencies": { - "err-code": "^2.0.2", - "retry": "^0.12.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/npm/node_modules/promzard": { - "version": "1.0.0", - "dev": true, - "inBundle": true, - "license": "ISC", - "dependencies": { - "read": "^2.0.0" - }, - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" - } - }, - "node_modules/npm/node_modules/qrcode-terminal": { - "version": "0.12.0", - "dev": true, - "inBundle": true, - "bin": { - "qrcode-terminal": "bin/qrcode-terminal.js" - } - }, - "node_modules/npm/node_modules/read": { - "version": "2.0.0", - "dev": true, - "inBundle": true, - "license": "ISC", - "dependencies": { - "mute-stream": "~1.0.0" - }, - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" - } - }, - "node_modules/npm/node_modules/read-cmd-shim": { - "version": "4.0.0", - "dev": true, - "inBundle": true, - "license": "ISC", - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" - } - }, - "node_modules/npm/node_modules/read-package-json": { - "version": "6.0.0", - "dev": true, - "inBundle": true, - "license": "ISC", - "dependencies": { - "glob": "^8.0.1", - "json-parse-even-better-errors": "^3.0.0", - "normalize-package-data": "^5.0.0", - "npm-normalize-package-bin": "^3.0.0" - }, - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" - } - }, - "node_modules/npm/node_modules/read-package-json-fast": { - "version": "3.0.2", - "dev": true, - "inBundle": true, - "license": "ISC", - "dependencies": { - "json-parse-even-better-errors": "^3.0.0", - "npm-normalize-package-bin": "^3.0.0" - }, - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" - } - }, - "node_modules/npm/node_modules/readable-stream": { - "version": "4.3.0", - "dev": true, - "inBundle": true, - "license": "MIT", - "dependencies": { - "abort-controller": "^3.0.0", - "buffer": "^6.0.3", - "events": "^3.3.0", - "process": "^0.11.10" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - } - }, - "node_modules/npm/node_modules/retry": { - "version": "0.12.0", - "dev": true, - "inBundle": true, - "license": "MIT", - "engines": { - "node": ">= 4" - } - }, - "node_modules/npm/node_modules/rimraf": { - "version": "3.0.2", - "dev": true, - "inBundle": true, - "license": "ISC", - "dependencies": { - "glob": "^7.1.3" - }, - "bin": { - "rimraf": "bin.js" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/npm/node_modules/rimraf/node_modules/brace-expansion": { - "version": "1.1.11", - "dev": true, - "inBundle": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/npm/node_modules/rimraf/node_modules/glob": { - "version": "7.2.3", - "dev": true, - "inBundle": true, - "license": "ISC", - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - }, - "engines": { - "node": "*" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/npm/node_modules/rimraf/node_modules/minimatch": { - "version": "3.1.2", - "dev": true, - "inBundle": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/npm/node_modules/safe-buffer": { - "version": "5.1.2", - "dev": true, - "inBundle": true, - "license": "MIT" - }, - "node_modules/npm/node_modules/safer-buffer": { - "version": "2.1.2", - "dev": true, - "inBundle": true, - "license": "MIT", - "optional": true - }, - "node_modules/npm/node_modules/semver": { - "version": "7.3.8", - "dev": true, - "inBundle": true, - "license": "ISC", - "dependencies": { - "lru-cache": "^6.0.0" - }, - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/npm/node_modules/semver/node_modules/lru-cache": { - "version": "6.0.0", - "dev": true, - "inBundle": true, - "license": "ISC", - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/npm/node_modules/set-blocking": { - "version": "2.0.0", - "dev": true, - "inBundle": true, - "license": "ISC" - }, - "node_modules/npm/node_modules/signal-exit": { - "version": "3.0.7", - "dev": true, - "inBundle": true, - "license": "ISC" - }, - "node_modules/npm/node_modules/sigstore": { - "version": "1.1.1", - "dev": true, - "inBundle": true, - "license": "Apache-2.0", - "dependencies": { - "@sigstore/protobuf-specs": "^0.1.0", - "make-fetch-happen": "^11.0.1", - "tuf-js": "^1.0.0" - }, - "bin": { - "sigstore": "bin/sigstore.js" - }, - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" - } - }, - "node_modules/npm/node_modules/smart-buffer": { - "version": "4.2.0", - "dev": true, - "inBundle": true, - "license": "MIT", - "engines": { - "node": ">= 6.0.0", - "npm": ">= 3.0.0" - } - }, - "node_modules/npm/node_modules/socks": { - "version": "2.7.1", - "dev": true, - "inBundle": true, - "license": "MIT", - "dependencies": { - "ip": "^2.0.0", - "smart-buffer": "^4.2.0" - }, - "engines": { - "node": ">= 10.13.0", - "npm": ">= 3.0.0" - } - }, - "node_modules/npm/node_modules/socks-proxy-agent": { - "version": "7.0.0", - "dev": true, - "inBundle": true, - "license": "MIT", - "dependencies": { - "agent-base": "^6.0.2", - "debug": "^4.3.3", - "socks": "^2.6.2" - }, - "engines": { - "node": ">= 10" - } - }, - "node_modules/npm/node_modules/spdx-correct": { - "version": "3.2.0", - "dev": true, - "inBundle": true, - "license": "Apache-2.0", - "dependencies": { - "spdx-expression-parse": "^3.0.0", - "spdx-license-ids": "^3.0.0" - } - }, - "node_modules/npm/node_modules/spdx-exceptions": { - "version": "2.3.0", - "dev": true, - "inBundle": true, - "license": "CC-BY-3.0" - }, - "node_modules/npm/node_modules/spdx-expression-parse": { - "version": "3.0.1", - "dev": true, - "inBundle": true, - "license": "MIT", - "dependencies": { - "spdx-exceptions": "^2.1.0", - "spdx-license-ids": "^3.0.0" - } - }, - "node_modules/npm/node_modules/spdx-license-ids": { - "version": "3.0.12", - "dev": true, - "inBundle": true, - "license": "CC0-1.0" - }, - "node_modules/npm/node_modules/ssri": { - "version": "10.0.1", - "dev": true, - "inBundle": true, - "license": "ISC", - "dependencies": { - "minipass": "^4.0.0" - }, - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" - } - }, - "node_modules/npm/node_modules/string_decoder": { - "version": "1.1.1", - "dev": true, - "inBundle": true, - "license": "MIT", - "dependencies": { - "safe-buffer": "~5.1.0" - } - }, - "node_modules/npm/node_modules/string-width": { - "version": "4.2.3", - "dev": true, - "inBundle": true, - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/npm/node_modules/strip-ansi": { - "version": "6.0.1", - "dev": true, - "inBundle": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/npm/node_modules/supports-color": { - "version": "7.2.0", - "dev": true, - "inBundle": true, - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/npm/node_modules/tar": { - "version": "6.1.13", - "dev": true, - "inBundle": true, - "license": "ISC", - "dependencies": { - "chownr": "^2.0.0", - "fs-minipass": "^2.0.0", - "minipass": "^4.0.0", - "minizlib": "^2.1.1", - "mkdirp": "^1.0.3", - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/npm/node_modules/tar/node_modules/fs-minipass": { - "version": "2.1.0", - "dev": true, - "inBundle": true, - "license": "ISC", - "dependencies": { - "minipass": "^3.0.0" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/npm/node_modules/tar/node_modules/fs-minipass/node_modules/minipass": { - "version": "3.3.6", - "dev": true, - "inBundle": true, - "license": "ISC", - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/npm/node_modules/text-table": { - "version": "0.2.0", - "dev": true, - "inBundle": true, - "license": "MIT" - }, - "node_modules/npm/node_modules/tiny-relative-date": { - "version": "1.3.0", - "dev": true, - "inBundle": true, - "license": "MIT" - }, - "node_modules/npm/node_modules/treeverse": { - "version": "3.0.0", - "dev": true, - "inBundle": true, - "license": "ISC", - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" - } - }, - "node_modules/npm/node_modules/tuf-js": { - "version": "1.1.1", - "dev": true, - "inBundle": true, - "license": "MIT", - "dependencies": { - "@tufjs/models": "1.0.0", - "make-fetch-happen": "^11.0.1" - }, - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" - } - }, - "node_modules/npm/node_modules/unique-filename": { - "version": "3.0.0", - "dev": true, - "inBundle": true, - "license": "ISC", - "dependencies": { - "unique-slug": "^4.0.0" - }, - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" - } - }, - "node_modules/npm/node_modules/unique-slug": { - "version": "4.0.0", - "dev": true, - "inBundle": true, - "license": "ISC", - "dependencies": { - "imurmurhash": "^0.1.4" - }, - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" - } - }, - "node_modules/npm/node_modules/util-deprecate": { - "version": "1.0.2", - "dev": true, - "inBundle": true, - "license": "MIT" - }, - "node_modules/npm/node_modules/validate-npm-package-license": { - "version": "3.0.4", - "dev": true, - "inBundle": true, - "license": "Apache-2.0", - "dependencies": { - "spdx-correct": "^3.0.0", - "spdx-expression-parse": "^3.0.0" - } - }, - "node_modules/npm/node_modules/validate-npm-package-name": { - "version": "5.0.0", - "dev": true, - "inBundle": true, - "license": "ISC", - "dependencies": { - "builtins": "^5.0.0" - }, - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" - } - }, - "node_modules/npm/node_modules/walk-up-path": { - "version": "1.0.0", - "dev": true, - "inBundle": true, - "license": "ISC" - }, - "node_modules/npm/node_modules/wcwidth": { - "version": "1.0.1", - "dev": true, - "inBundle": true, - "license": "MIT", - "dependencies": { - "defaults": "^1.0.3" - } - }, - "node_modules/npm/node_modules/which": { - "version": "3.0.0", - "dev": true, - "inBundle": true, - "license": "ISC", - "dependencies": { - "isexe": "^2.0.0" - }, - "bin": { - "node-which": "bin/which.js" - }, - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" - } - }, - "node_modules/npm/node_modules/wide-align": { - "version": "1.1.5", - "dev": true, - "inBundle": true, - "license": "ISC", - "dependencies": { - "string-width": "^1.0.2 || 2 || 3 || 4" - } - }, - "node_modules/npm/node_modules/wrappy": { - "version": "1.0.2", - "dev": true, - "inBundle": true, - "license": "ISC" - }, - "node_modules/npm/node_modules/write-file-atomic": { - "version": "5.0.0", - "dev": true, - "inBundle": true, - "license": "ISC", - "dependencies": { - "imurmurhash": "^0.1.4", - "signal-exit": "^3.0.7" - }, - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" - } - }, - "node_modules/npm/node_modules/yallist": { - "version": "4.0.0", - "dev": true, - "inBundle": true, - "license": "ISC" - }, - "node_modules/once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "dev": true, - "dependencies": { - "wrappy": "1" - } - }, - "node_modules/onetime": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", - "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", - "dev": true, - "dependencies": { - "mimic-fn": "^2.1.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/p-each-series": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/p-each-series/-/p-each-series-3.0.0.tgz", - "integrity": "sha512-lastgtAdoH9YaLyDa5i5z64q+kzOcQHsQ5SsZJD3q0VEyI8mq872S3geuNbRUQLVAE9siMfgKrpj7MloKFHruw==", - "dev": true, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/p-filter": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/p-filter/-/p-filter-2.1.0.tgz", - "integrity": "sha512-ZBxxZ5sL2HghephhpGAQdoskxplTwr7ICaehZwLIlfL6acuVgZPm8yBNuRAFBGEqtD/hmUeq9eqLg2ys9Xr/yw==", - "dev": true, - "dependencies": { - "p-map": "^2.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/p-is-promise": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/p-is-promise/-/p-is-promise-3.0.0.tgz", - "integrity": "sha512-Wo8VsW4IRQSKVXsJCn7TomUaVtyfjVDn3nUP7kE967BQk0CwFpdbZs0X0uk5sW9mkBa9eNM7hCMaG93WUAwxYQ==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/p-limit": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-4.0.0.tgz", - "integrity": "sha512-5b0R4txpzjPWVw/cXXUResoD4hb6U/x9BH08L7nw+GN1sezDzPdxeRvpc9c433fZhBan/wusjbCsqwqm4EIBIQ==", - "dev": true, - "dependencies": { - "yocto-queue": "^1.0.0" - }, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/p-locate": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-6.0.0.tgz", - "integrity": "sha512-wPrq66Llhl7/4AGC6I+cqxT07LhXvWL08LNXz1fENOw0Ap4sRZZ/gZpTTJ5jpurzzzfS2W/Ge9BY3LgLjCShcw==", - "dev": true, - "dependencies": { - "p-limit": "^4.0.0" - }, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/p-map": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/p-map/-/p-map-2.1.0.tgz", - "integrity": "sha512-y3b8Kpd8OAN444hxfBbFfj1FY/RjtTd8tzYwhUqNYXx0fXx2iX4maP4Qr6qhIKbQXI02wTLAda4fYUbDagTUFw==", - "dev": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/p-reduce": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/p-reduce/-/p-reduce-2.1.0.tgz", - "integrity": "sha512-2USApvnsutq8uoxZBGbbWM0JIYLiEMJ9RlaN7fAzVNb9OZN0SHjjTTfIcb667XynS5Y1VhwDJVDa72TnPzAYWw==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/p-retry": { - "version": "4.6.2", - "resolved": "https://registry.npmjs.org/p-retry/-/p-retry-4.6.2.tgz", - "integrity": "sha512-312Id396EbJdvRONlngUx0NydfrIQ5lsYu0znKVUzVvArzEIt08V1qhtyESbGVd1FGX7UKtiFp5uwKZdM8wIuQ==", - "dev": true, - "dependencies": { - "@types/retry": "0.12.0", - "retry": "^0.13.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/p-try": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", - "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", - "dev": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/parent-module": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", - "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", - "dev": true, - "dependencies": { - "callsites": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/parse-json": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", - "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", - "dev": true, - "dependencies": { - "@babel/code-frame": "^7.0.0", - "error-ex": "^1.3.1", - "json-parse-even-better-errors": "^2.3.0", - "lines-and-columns": "^1.1.6" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/path-exists": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-5.0.0.tgz", - "integrity": "sha512-RjhtfwJOxzcFmNOi6ltcbcu4Iu+FL3zEj83dk4kAS+fVpTxXLO1b38RvJgT/0QwvV/L3aY9TAnyv0EOqW4GoMQ==", - "dev": true, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - } - }, - "node_modules/path-key": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", - "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/path-parse": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", - "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", - "dev": true - }, - "node_modules/path-type": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", - "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", - "dev": true, - "engines": { - "node": ">=8.6" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/pify": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", - "integrity": "sha512-C3FsVNH1udSEX48gGX1xfvwTWfsYWj5U+8/uK15BGzIGrKoUpghX8hWZwa/OFnakBiiVNmBvemTJR5mcy7iPcg==", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/pkg-conf": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/pkg-conf/-/pkg-conf-2.1.0.tgz", - "integrity": "sha512-C+VUP+8jis7EsQZIhDYmS5qlNtjv2yP4SNtjXK9AP1ZcTRlnSfuumaTnRfYZnYgUUYVIKqL0fRvmUGDV2fmp6g==", - "dev": true, - "dependencies": { - "find-up": "^2.0.0", - "load-json-file": "^4.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/pkg-conf/node_modules/find-up": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-2.1.0.tgz", - "integrity": "sha512-NWzkk0jSJtTt08+FBFMvXoeZnOJD+jTtsRmBYbAIzJdX6l7dLgR7CTubCM5/eDdPUBvLCeVasP1brfVR/9/EZQ==", - "dev": true, - "dependencies": { - "locate-path": "^2.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/pkg-conf/node_modules/locate-path": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-2.0.0.tgz", - "integrity": "sha512-NCI2kiDkyR7VeEKm27Kda/iQHyKJe1Bu0FlTbYp3CqJu+9IFe9bLyAjMxf5ZDDbEg+iMPzB5zYyUTSm8wVTKmA==", - "dev": true, - "dependencies": { - "p-locate": "^2.0.0", - "path-exists": "^3.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/pkg-conf/node_modules/p-limit": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-1.3.0.tgz", - "integrity": "sha512-vvcXsLAJ9Dr5rQOPk7toZQZJApBl2K4J6dANSsEuh6QI41JYcsS/qhTGa9ErIUUgK3WNQoJYvylxvjqmiqEA9Q==", - "dev": true, - "dependencies": { - "p-try": "^1.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/pkg-conf/node_modules/p-locate": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-2.0.0.tgz", - "integrity": "sha512-nQja7m7gSKuewoVRen45CtVfODR3crN3goVQ0DDZ9N3yHxgpkuBhZqsaiotSQRrADUrne346peY7kT3TSACykg==", - "dev": true, - "dependencies": { - "p-limit": "^1.1.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/pkg-conf/node_modules/p-try": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/p-try/-/p-try-1.0.0.tgz", - "integrity": "sha512-U1etNYuMJoIz3ZXSrrySFjsXQTWOx2/jdi86L+2pRvph/qMKL6sbcCYdH23fqsbm8TH2Gn0OybpT4eSFlCVHww==", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/pkg-conf/node_modules/path-exists": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", - "integrity": "sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ==", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/process-nextick-args": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", - "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", - "dev": true - }, - "node_modules/proto-list": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/proto-list/-/proto-list-1.2.4.tgz", - "integrity": "sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA==", - "dev": true - }, - "node_modules/q": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/q/-/q-1.5.1.tgz", - "integrity": "sha512-kV/CThkXo6xyFEZUugw/+pIOywXcDbFYgSct5cT3gqlbkBE1SJdwy6UQoZvodiWF/ckQLZyDE/Bu1M6gVu5lVw==", - "dev": true, - "engines": { - "node": ">=0.6.0", - "teleport": ">=0.2.0" - } - }, - "node_modules/queue-microtask": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", - "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ] - }, - "node_modules/quick-lru": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-4.0.1.tgz", - "integrity": "sha512-ARhCpm70fzdcvNQfPoy49IaanKkTlRWF2JMzqhcJbhSFRZv7nPTvZJdcY7301IPmvW+/p0RgIWnQDLJxifsQ7g==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/rc": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", - "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", - "dev": true, - "dependencies": { - "deep-extend": "^0.6.0", - "ini": "~1.3.0", - "minimist": "^1.2.0", - "strip-json-comments": "~2.0.1" - }, - "bin": { - "rc": "cli.js" - } - }, - "node_modules/read-pkg": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-5.2.0.tgz", - "integrity": "sha512-Ug69mNOpfvKDAc2Q8DRpMjjzdtrnv9HcSMX+4VsZxD1aZ6ZzrIE7rlzXBtWTyhULSMKg076AW6WR5iZpD0JiOg==", - "dev": true, - "dependencies": { - "@types/normalize-package-data": "^2.4.0", - "normalize-package-data": "^2.5.0", - "parse-json": "^5.0.0", - "type-fest": "^0.6.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/read-pkg-up": { - "version": "9.1.0", - "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-9.1.0.tgz", - "integrity": "sha512-vaMRR1AC1nrd5CQM0PhlRsO5oc2AAigqr7cCrZ/MW/Rsaflz4RlgzkpL4qoU/z1F6wrbd85iFv1OQj/y5RdGvg==", - "dev": true, - "dependencies": { - "find-up": "^6.3.0", - "read-pkg": "^7.1.0", - "type-fest": "^2.5.0" - }, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/read-pkg-up/node_modules/read-pkg": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-7.1.0.tgz", - "integrity": "sha512-5iOehe+WF75IccPc30bWTbpdDQLOCc3Uu8bi3Dte3Eueij81yx1Mrufk8qBx/YAbR4uL1FdUr+7BKXDwEtisXg==", - "dev": true, - "dependencies": { - "@types/normalize-package-data": "^2.4.1", - "normalize-package-data": "^3.0.2", - "parse-json": "^5.2.0", - "type-fest": "^2.0.0" - }, - "engines": { - "node": ">=12.20" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/read-pkg-up/node_modules/type-fest": { - "version": "2.19.0", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-2.19.0.tgz", - "integrity": "sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==", - "dev": true, - "engines": { - "node": ">=12.20" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/read-pkg/node_modules/hosted-git-info": { - "version": "2.8.9", - "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz", - "integrity": "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==", - "dev": true - }, - "node_modules/read-pkg/node_modules/normalize-package-data": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", - "integrity": "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==", - "dev": true, - "dependencies": { - "hosted-git-info": "^2.1.4", - "resolve": "^1.10.0", - "semver": "2 || 3 || 4 || 5", - "validate-npm-package-license": "^3.0.1" - } - }, - "node_modules/read-pkg/node_modules/semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", - "dev": true, - "bin": { - "semver": "bin/semver" - } - }, - "node_modules/read-pkg/node_modules/type-fest": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.6.0.tgz", - "integrity": "sha512-q+MB8nYR1KDLrgr4G5yemftpMC7/QLqVndBmEEdqzmNj5dcFOO4Oo8qlwZE3ULT3+Zim1F8Kq4cBnikNhlCMlg==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/readable-stream": { - "version": "2.3.8", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", - "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", - "dev": true, - "dependencies": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "node_modules/redent": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz", - "integrity": "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==", - "dev": true, - "dependencies": { - "indent-string": "^4.0.0", - "strip-indent": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/redeyed": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/redeyed/-/redeyed-2.1.1.tgz", - "integrity": "sha512-FNpGGo1DycYAdnrKFxCMmKYgo/mILAqtRYbkdQD8Ep/Hk2PQ5+aEAEx+IU713RTDmuBaH0c8P5ZozurNu5ObRQ==", - "dev": true, - "dependencies": { - "esprima": "~4.0.0" - } - }, - "node_modules/registry-auth-token": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/registry-auth-token/-/registry-auth-token-5.0.2.tgz", - "integrity": "sha512-o/3ikDxtXaA59BmZuZrJZDJv8NMDGSj+6j6XaeBmHw8eY1i1qd9+6H+LjVvQXx3HN6aRCGa1cUdJ9RaJZUugnQ==", - "dev": true, - "dependencies": { - "@pnpm/npm-conf": "^2.1.0" - }, - "engines": { - "node": ">=14" - } - }, - "node_modules/require-directory": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", - "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/resolve": { - "version": "1.22.1", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.1.tgz", - "integrity": "sha512-nBpuuYuY5jFsli/JIs1oldw6fOQCBioohqWZg/2hiaOybXOft4lonv85uDOKXdf8rhyK159cxU5cDcK/NKk8zw==", - "dev": true, - "dependencies": { - "is-core-module": "^2.9.0", - "path-parse": "^1.0.7", - "supports-preserve-symlinks-flag": "^1.0.0" - }, - "bin": { - "resolve": "bin/resolve" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/resolve-from": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", - "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/retry": { - "version": "0.13.1", - "resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz", - "integrity": "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==", - "dev": true, - "engines": { - "node": ">= 4" - } - }, - "node_modules/reusify": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", - "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", - "dev": true, - "engines": { - "iojs": ">=1.0.0", - "node": ">=0.10.0" - } - }, - "node_modules/run-parallel": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", - "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "dependencies": { - "queue-microtask": "^1.2.2" - } - }, - "node_modules/safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "dev": true - }, - "node_modules/semantic-release": { - "version": "21.0.1", - "resolved": "https://registry.npmjs.org/semantic-release/-/semantic-release-21.0.1.tgz", - "integrity": "sha512-UhGxTUXHJQCBFgEQRZszLOHDpMduDSHGq3Q+30Bu+g0GbXh/EW508+kuFHezP5m0mN8xINW8hooiR3dzSV5ZLA==", - "dev": true, - "dependencies": { - "@semantic-release/commit-analyzer": "^9.0.2", - "@semantic-release/error": "^3.0.0", - "@semantic-release/github": "^8.0.0", - "@semantic-release/npm": "^10.0.2", - "@semantic-release/release-notes-generator": "^10.0.0", - "aggregate-error": "^4.0.1", - "cosmiconfig": "^8.0.0", - "debug": "^4.0.0", - "env-ci": "^9.0.0", - "execa": "^7.0.0", - "figures": "^5.0.0", - "find-versions": "^5.1.0", - "get-stream": "^6.0.0", - "git-log-parser": "^1.2.0", - "hook-std": "^3.0.0", - "hosted-git-info": "^6.0.0", - "lodash-es": "^4.17.21", - "marked": "^4.1.0", - "marked-terminal": "^5.1.1", - "micromatch": "^4.0.2", - "p-each-series": "^3.0.0", - "p-reduce": "^3.0.0", - "read-pkg-up": "^9.1.0", - "resolve-from": "^5.0.0", - "semver": "^7.3.2", - "semver-diff": "^4.0.0", - "signale": "^1.2.1", - "yargs": "^17.5.1" - }, - "bin": { - "semantic-release": "bin/semantic-release.js" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/semantic-release-export-data": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/semantic-release-export-data/-/semantic-release-export-data-1.0.1.tgz", - "integrity": "sha512-6vlgrrzzcMi/REhQd65Bh4dfSKmgwXOJ/Q2RVlT9WsU4Ya1T2qGpkSrMfG/n6oFRrqBdbDlyZgxNd94ziW+vSg==", - "dev": true, - "dependencies": { - "@actions/core": "^1.10.0" - }, - "peerDependencies": { - "semantic-release": ">=18" - } - }, - "node_modules/semantic-release-flutter-plugin": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/semantic-release-flutter-plugin/-/semantic-release-flutter-plugin-1.1.2.tgz", - "integrity": "sha512-z0TcuNwaF9kzPXHIxGNFm/aasRJr5th1e6mf33xxAMnD2mBT55JIohguP7o01mGJEVFoc2ftbfvdtcE1+esSEA==", - "dev": true, - "dependencies": { - "semantic-release": "^21.0.1", - "semver": "^7.5.0", - "yaml": "^2.2.1" - } - }, - "node_modules/semantic-release-flutter-plugin/node_modules/yaml": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.2.2.tgz", - "integrity": "sha512-CBKFWExMn46Foo4cldiChEzn7S7SRV+wqiluAb6xmueD/fGyRHIhX8m14vVGgeFWjN540nKCNVj6P21eQjgTuA==", - "dev": true, - "engines": { - "node": ">= 14" - } - }, - "node_modules/semantic-release/node_modules/aggregate-error": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-4.0.1.tgz", - "integrity": "sha512-0poP0T7el6Vq3rstR8Mn4V/IQrpBLO6POkUSrN7RhyY+GF/InCFShQzsQ39T25gkHhLgSLByyAz+Kjb+c2L98w==", - "dev": true, - "dependencies": { - "clean-stack": "^4.0.0", - "indent-string": "^5.0.0" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/semantic-release/node_modules/clean-stack": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-4.2.0.tgz", - "integrity": "sha512-LYv6XPxoyODi36Dp976riBtSY27VmFo+MKqEU9QCCWyTrdEPDog+RWA7xQWHi6Vbp61j5c4cdzzX1NidnwtUWg==", - "dev": true, - "dependencies": { - "escape-string-regexp": "5.0.0" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/semantic-release/node_modules/execa": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/execa/-/execa-7.1.1.tgz", - "integrity": "sha512-wH0eMf/UXckdUYnO21+HDztteVv05rq2GXksxT4fCGeHkBhw1DROXh40wcjMcRqDOWE7iPJ4n3M7e2+YFP+76Q==", - "dev": true, - "dependencies": { - "cross-spawn": "^7.0.3", - "get-stream": "^6.0.1", - "human-signals": "^4.3.0", - "is-stream": "^3.0.0", - "merge-stream": "^2.0.0", - "npm-run-path": "^5.1.0", - "onetime": "^6.0.0", - "signal-exit": "^3.0.7", - "strip-final-newline": "^3.0.0" - }, - "engines": { - "node": "^14.18.0 || ^16.14.0 || >=18.0.0" - }, - "funding": { - "url": "https://github.com/sindresorhus/execa?sponsor=1" - } - }, - "node_modules/semantic-release/node_modules/human-signals": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-4.3.0.tgz", - "integrity": "sha512-zyzVyMjpGBX2+6cDVZeFPCdtOtdsxOeseRhB9tkQ6xXmGUNrcnBzdEKPy3VPNYz+4gy1oukVOXcrJCunSyc6QQ==", - "dev": true, - "engines": { - "node": ">=14.18.0" - } - }, - "node_modules/semantic-release/node_modules/indent-string": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-5.0.0.tgz", - "integrity": "sha512-m6FAo/spmsW2Ab2fU35JTYwtOKa2yAwXSwgjSv1TJzh4Mh7mC3lzAOVLBprb72XsTrgkEIsl7YrFNAiDiRhIGg==", - "dev": true, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/semantic-release/node_modules/is-stream": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-3.0.0.tgz", - "integrity": "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==", - "dev": true, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/semantic-release/node_modules/mimic-fn": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-4.0.0.tgz", - "integrity": "sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==", - "dev": true, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/semantic-release/node_modules/npm-run-path": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.1.0.tgz", - "integrity": "sha512-sJOdmRGrY2sjNTRMbSvluQqg+8X7ZK61yvzBEIDhz4f8z1TZFYABsqjjCBd/0PUNE9M6QDgHJXQkGUEm7Q+l9Q==", - "dev": true, - "dependencies": { - "path-key": "^4.0.0" - }, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/semantic-release/node_modules/onetime": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/onetime/-/onetime-6.0.0.tgz", - "integrity": "sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==", - "dev": true, - "dependencies": { - "mimic-fn": "^4.0.0" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/semantic-release/node_modules/p-reduce": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/p-reduce/-/p-reduce-3.0.0.tgz", - "integrity": "sha512-xsrIUgI0Kn6iyDYm9StOpOeK29XM1aboGji26+QEortiFST1hGZaUQOLhtEbqHErPpGW/aSz6allwK2qcptp0Q==", - "dev": true, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/semantic-release/node_modules/path-key": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz", - "integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==", - "dev": true, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/semantic-release/node_modules/strip-final-newline": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-3.0.0.tgz", - "integrity": "sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==", - "dev": true, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/semver": { - "version": "7.5.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.0.tgz", - "integrity": "sha512-+XC0AD/R7Q2mPSRuy2Id0+CGTZ98+8f+KvwirxOKIEyid+XSx6HbC63p+O4IndTHuX5Z+JxQ0TghCkO5Cg/2HA==", - "dev": true, - "dependencies": { - "lru-cache": "^6.0.0" - }, - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/semver-diff": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/semver-diff/-/semver-diff-4.0.0.tgz", - "integrity": "sha512-0Ju4+6A8iOnpL/Thra7dZsSlOHYAHIeMxfhWQRI1/VLcT3WDBZKKtQt/QkBOsiIN9ZpuvHE6cGZ0x4glCMmfiA==", - "dev": true, - "dependencies": { - "semver": "^7.3.5" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/semver-regex": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/semver-regex/-/semver-regex-4.0.5.tgz", - "integrity": "sha512-hunMQrEy1T6Jr2uEVjrAIqjwWcQTgOAcIM52C8MY1EZSD3DDNft04XzvYKPqjED65bNVVko0YI38nYeEHCX3yw==", - "dev": true, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/semver/node_modules/lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "dev": true, - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/shebang-command": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", - "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "dev": true, - "dependencies": { - "shebang-regex": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/shebang-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", - "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/signal-exit": { - "version": "3.0.7", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", - "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", - "dev": true - }, - "node_modules/signale": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/signale/-/signale-1.4.0.tgz", - "integrity": "sha512-iuh+gPf28RkltuJC7W5MRi6XAjTDCAPC/prJUpQoG4vIP3MJZ+GTydVnodXA7pwvTKb2cA0m9OFZW/cdWy/I/w==", - "dev": true, - "dependencies": { - "chalk": "^2.3.2", - "figures": "^2.0.0", - "pkg-conf": "^2.1.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/signale/node_modules/ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dev": true, - "dependencies": { - "color-convert": "^1.9.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/signale/node_modules/chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, - "dependencies": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/signale/node_modules/color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "dev": true, - "dependencies": { - "color-name": "1.1.3" - } - }, - "node_modules/signale/node_modules/color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", - "dev": true - }, - "node_modules/signale/node_modules/escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", - "dev": true, - "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/signale/node_modules/figures": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/figures/-/figures-2.0.0.tgz", - "integrity": "sha512-Oa2M9atig69ZkfwiApY8F2Yy+tzMbazyvqv21R0NsSC8floSOC09BbT1ITWAdoMGQvJ/aZnR1KMwdx9tvHnTNA==", - "dev": true, - "dependencies": { - "escape-string-regexp": "^1.0.5" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/signale/node_modules/has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/signale/node_modules/supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, - "dependencies": { - "has-flag": "^3.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/slash": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", - "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/spawn-error-forwarder": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/spawn-error-forwarder/-/spawn-error-forwarder-1.0.0.tgz", - "integrity": "sha512-gRjMgK5uFjbCvdibeGJuy3I5OYz6VLoVdsOJdA6wV0WlfQVLFueoqMxwwYD9RODdgb6oUIvlRlsyFSiQkMKu0g==", - "dev": true - }, - "node_modules/spdx-correct": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.2.0.tgz", - "integrity": "sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==", - "dev": true, - "dependencies": { - "spdx-expression-parse": "^3.0.0", - "spdx-license-ids": "^3.0.0" - } - }, - "node_modules/spdx-exceptions": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.3.0.tgz", - "integrity": "sha512-/tTrYOC7PPI1nUAgx34hUpqXuyJG+DTHJTnIULG4rDygi4xu/tfgmq1e1cIRwRzwZgo4NLySi+ricLkZkw4i5A==", - "dev": true - }, - "node_modules/spdx-expression-parse": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz", - "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==", - "dev": true, - "dependencies": { - "spdx-exceptions": "^2.1.0", - "spdx-license-ids": "^3.0.0" - } - }, - "node_modules/spdx-license-ids": { - "version": "3.0.13", - "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.13.tgz", - "integrity": "sha512-XkD+zwiqXHikFZm4AX/7JSCXA98U5Db4AFd5XUg/+9UNtnH75+Z9KxtpYiJZx36mUDVOwH83pl7yvCer6ewM3w==", - "dev": true - }, - "node_modules/split": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/split/-/split-1.0.1.tgz", - "integrity": "sha512-mTyOoPbrivtXnwnIxZRFYRrPNtEFKlpB2fvjSnCQUiAA6qAZzqwna5envK4uk6OIeP17CsdF3rSBGYVBsU0Tkg==", - "dev": true, - "dependencies": { - "through": "2" - }, - "engines": { - "node": "*" - } - }, - "node_modules/split2": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/split2/-/split2-3.2.2.tgz", - "integrity": "sha512-9NThjpgZnifTkJpzTZ7Eue85S49QwpNhZTq6GRJwObb6jnLFNGB7Qm73V5HewTROPyxD0C29xqmaI68bQtV+hg==", - "dev": true, - "dependencies": { - "readable-stream": "^3.0.0" - } - }, - "node_modules/split2/node_modules/readable-stream": { - "version": "3.6.2", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", - "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", - "dev": true, - "dependencies": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/stream-combiner2": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/stream-combiner2/-/stream-combiner2-1.1.1.tgz", - "integrity": "sha512-3PnJbYgS56AeWgtKF5jtJRT6uFJe56Z0Hc5Ngg/6sI6rIt8iiMBTa9cvdyFfpMQjaVHr8dusbNeFGIIonxOvKw==", - "dev": true, - "dependencies": { - "duplexer2": "~0.1.0", - "readable-stream": "^2.0.2" - } - }, - "node_modules/string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "dev": true, - "dependencies": { - "safe-buffer": "~5.1.0" - } - }, - "node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/strip-bom": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", - "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/strip-final-newline": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", - "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", - "dev": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/strip-indent": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz", - "integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==", - "dev": true, - "dependencies": { - "min-indent": "^1.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/strip-json-comments": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", - "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/supports-hyperlinks": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/supports-hyperlinks/-/supports-hyperlinks-2.3.0.tgz", - "integrity": "sha512-RpsAZlpWcDwOPQA22aCH4J0t7L8JmAvsCxfOSEwm7cQs3LshN36QaTkwd70DnBOXDWGssw2eUoc8CaRWT0XunA==", - "dev": true, - "dependencies": { - "has-flag": "^4.0.0", - "supports-color": "^7.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/supports-preserve-symlinks-flag": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", - "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", - "dev": true, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/temp-dir": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/temp-dir/-/temp-dir-2.0.0.tgz", - "integrity": "sha512-aoBAniQmmwtcKp/7BzsH8Cxzv8OL736p7v1ihGb5e9DJ9kTwGWHrQrVB5+lfVDzfGrdRzXch+ig7LHaY1JTOrg==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/text-extensions": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/text-extensions/-/text-extensions-1.9.0.tgz", - "integrity": "sha512-wiBrwC1EhBelW12Zy26JeOUkQ5mRu+5o8rpsJk5+2t+Y5vE7e842qtZDQ2g1NpX/29HdyFeJ4nSIhI47ENSxlQ==", - "dev": true, - "engines": { - "node": ">=0.10" - } - }, - "node_modules/through": { - "version": "2.3.8", - "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", - "integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==", - "dev": true - }, - "node_modules/through2": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/through2/-/through2-4.0.2.tgz", - "integrity": "sha512-iOqSav00cVxEEICeD7TjLB1sueEL+81Wpzp2bY17uZjZN0pWZPuo4suZ/61VujxmqSGFfgOcNuTZ85QJwNZQpw==", - "dev": true, - "dependencies": { - "readable-stream": "3" - } - }, - "node_modules/through2/node_modules/readable-stream": { - "version": "3.6.2", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", - "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", - "dev": true, - "dependencies": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/to-regex-range": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "dev": true, - "dependencies": { - "is-number": "^7.0.0" - }, - "engines": { - "node": ">=8.0" - } - }, - "node_modules/tr46": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", - "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", - "dev": true - }, - "node_modules/traverse": { - "version": "0.6.7", - "resolved": "https://registry.npmjs.org/traverse/-/traverse-0.6.7.tgz", - "integrity": "sha512-/y956gpUo9ZNCb99YjxG7OaslxZWHfCHAUUfshwqOXmxUIvqLjVO581BT+gM59+QV9tFe6/CGG53tsA1Y7RSdg==", - "dev": true, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/trim-newlines": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/trim-newlines/-/trim-newlines-3.0.1.tgz", - "integrity": "sha512-c1PTsA3tYrIsLGkJkzHF+w9F2EyxfXGo4UyJc4pFL++FMjnq0HJS69T3M7d//gKrFKwy429bouPescbjecU+Zw==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/tunnel": { - "version": "0.0.6", - "resolved": "https://registry.npmjs.org/tunnel/-/tunnel-0.0.6.tgz", - "integrity": "sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg==", - "dev": true, - "engines": { - "node": ">=0.6.11 <=0.7.0 || >=0.7.3" - } - }, - "node_modules/type-fest": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-1.4.0.tgz", - "integrity": "sha512-yGSza74xk0UG8k+pLh5oeoYirvIiWo5t0/o3zHHAO2tRDiZcxWP7fywNlXhqb6/r6sWvwi+RsyQMWhVLe4BVuA==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/uglify-js": { - "version": "3.17.4", - "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.17.4.tgz", - "integrity": "sha512-T9q82TJI9e/C1TAxYvfb16xO120tMVFZrGA3f9/P4424DNu6ypK103y0GPFVa17yotwSyZW5iYXgjYHkGrJW/g==", - "dev": true, - "optional": true, - "bin": { - "uglifyjs": "bin/uglifyjs" - }, - "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/universal-user-agent": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-6.0.0.tgz", - "integrity": "sha512-isyNax3wXoKaulPDZWHQqbmIx1k2tb9fb3GGDBRxCscfYV2Ch7WxPArBsFEG8s/safwXTT7H4QGhaIkTp9447w==", - "dev": true - }, - "node_modules/universalify": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz", - "integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==", - "dev": true, - "engines": { - "node": ">= 10.0.0" - } - }, - "node_modules/url-join": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/url-join/-/url-join-4.0.1.tgz", - "integrity": "sha512-jk1+QP6ZJqyOiuEI9AEWQfju/nB2Pw466kbA0LEZljHwKeMgd9WrAEgEGxjPDD2+TNbbb37rTyhEfrCXfuKXnA==", - "dev": true - }, - "node_modules/util-deprecate": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", - "dev": true - }, - "node_modules/uuid": { - "version": "8.3.2", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", - "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", - "dev": true, - "bin": { - "uuid": "dist/bin/uuid" - } - }, - "node_modules/validate-npm-package-license": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", - "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", - "dev": true, - "dependencies": { - "spdx-correct": "^3.0.0", - "spdx-expression-parse": "^3.0.0" - } - }, - "node_modules/webidl-conversions": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", - "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", - "dev": true - }, - "node_modules/whatwg-url": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", - "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", - "dev": true, - "dependencies": { - "tr46": "~0.0.3", - "webidl-conversions": "^3.0.0" - } - }, - "node_modules/which": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dev": true, - "dependencies": { - "isexe": "^2.0.0" - }, - "bin": { - "node-which": "bin/node-which" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/wordwrap": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", - "integrity": "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==", - "dev": true - }, - "node_modules/wrap-ansi": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "dev": true, - "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", - "dev": true - }, - "node_modules/xtend": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", - "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", - "dev": true, - "engines": { - "node": ">=0.4" - } - }, - "node_modules/y18n": { - "version": "5.0.8", - "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", - "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", - "dev": true, - "engines": { - "node": ">=10" - } - }, - "node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true - }, - "node_modules/yargs": { - "version": "17.7.1", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.1.tgz", - "integrity": "sha512-cwiTb08Xuv5fqF4AovYacTFNxk62th7LKJ6BL9IGUpTJrWoU7/7WdQGTP2SjKf1dUNBGzDd28p/Yfs/GI6JrLw==", - "dev": true, - "dependencies": { - "cliui": "^8.0.1", - "escalade": "^3.1.1", - "get-caller-file": "^2.0.5", - "require-directory": "^2.1.1", - "string-width": "^4.2.3", - "y18n": "^5.0.5", - "yargs-parser": "^21.1.1" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/yargs-parser": { - "version": "20.2.9", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", - "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", - "dev": true, - "engines": { - "node": ">=10" - } - }, - "node_modules/yargs/node_modules/yargs-parser": { - "version": "21.1.1", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", - "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", - "dev": true, - "engines": { - "node": ">=12" - } - }, - "node_modules/yocto-queue": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.0.0.tgz", - "integrity": "sha512-9bnSc/HEW2uRy67wc+T8UwauLuPJVn28jb+GtJY16iiKWyvmYJRXVT4UamsAEGQfPohgr2q4Tq0sQbQlxTfi1g==", - "dev": true, - "engines": { - "node": ">=12.20" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - } - } -} diff --git a/package.json b/package.json deleted file mode 100644 index 9641fca8e1..0000000000 --- a/package.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "devDependencies": { - "@saithodev/semantic-release-backmerge": "^3.1.0", - "@semantic-release/changelog": "^6.0.3", - "@semantic-release/git": "^10.0.1", - "semantic-release": "^21.0.1", - "semantic-release-export-data": "^1.0.1", - "semantic-release-flutter-plugin": "^1.1.2" - } -} diff --git a/pubspec.yaml b/pubspec.yaml deleted file mode 100644 index d902b20658..0000000000 --- a/pubspec.yaml +++ /dev/null @@ -1,93 +0,0 @@ -name: revanced_manager -description: Patch your favorite apps, right on your device. -homepage: https://github.com/revanced/revanced-manager - -publish_to: 'none' - -version: 1.8.0+100800000 - -environment: - sdk: '>=3.0.0 <4.0.0' - -dependencies: - animations: ^2.0.7 - collection: ^1.17.0 - cross_connectivity: ^3.0.5 - cr_file_saver: - git: - url: https://github.com/dhruvanbhalara/cr_file_saver - ref: "fix/incorrect_file_name" - device_apps: - git: - url: https://github.com/ponces/flutter_plugin_device_apps - ref: revanced-manager - device_info_plus: ^8.1.0 - dynamic_color: ^1.6.3 - dio: ^5.0.0 - dynamic_themes: ^1.1.0 - expandable: ^5.0.1 - file_picker: - git: - url: https://github.com/alexmercerind/flutter_file_picker - ref: master - flex_color_scheme: ^7.0.1 - flutter: - sdk: flutter - flutter_background: ^1.2.0 - flutter_cache_manager: ^3.3.0 - flutter_i18n: ^0.33.0 - flutter_local_notifications: ^13.0.0 - flutter_localizations: - sdk: flutter - flutter_svg: ^2.0.4 - fluttertoast: ^8.2.1 - font_awesome_flutter: ^10.4.0 - get_it: 7.2.0 - google_fonts: ^4.0.3 - http: ^0.13.5 - injectable: ^2.1.1 - intl: ^0.18.0 - json_annotation: ^4.8.0 - logcat: - git: - url: https://github.com/SuaMusica/logcat - ref: feature/nullSafe - package_info_plus: ^3.0.3 - path_provider: ^2.0.14 - permission_handler: ^10.2.0 - pull_to_refresh: ^2.0.0 - root: - git: - url: https://github.com/EvadeMaster/root - ref: 82803aa40f63cddff81c3e4d27ce8ce3e7c83f60 - share_extend: ^2.0.0 - shared_preferences: ^2.1.0 - skeletons: ^0.0.3 - stacked: ^3.2.0 - stacked_generator: ^1.1.0 - stacked_services: ^1.0.0 - stacked_themes: ^0.3.10 - timeago: ^3.3.0 - timezone: ^0.9.0 - url_launcher: ^6.1.10 - wakelock: ^0.6.2 - flutter_dotenv: ^5.0.2 - flutter_markdown: ^0.6.14 - dio_cache_interceptor: ^3.4.0 - install_plugin: ^2.1.0 - -dev_dependencies: - json_serializable: ^6.6.1 - build_runner: any - flutter_launcher_icons: ^0.13.0 - flutter_lints: ^2.0.1 - flutter_test: - sdk: flutter - injectable_generator: ^2.1.5 - - - -flutter: - uses-material-design: true - assets: - - assets/i18n/ diff --git a/settings.gradle.kts b/settings.gradle.kts new file mode 100644 index 0000000000..18b2f45638 --- /dev/null +++ b/settings.gradle.kts @@ -0,0 +1,30 @@ +pluginManagement { + repositories { + gradlePluginPortal() + google() + mavenCentral() + maven("https://jitpack.io") + mavenLocal() + } +} +dependencyResolutionManagement { + repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) + repositories { + google() + mavenCentral() + maven("https://jitpack.io") + mavenLocal() + maven { + // A repository must be specified for some reason. "registry" is a dummy. + url = uri("https://maven.pkg.github.com/revanced/registry") + credentials { + username = System.getenv("GITHUB_ACTOR") ?: extra["gpr.user"] as String? + password = System.getenv("GITHUB_TOKEN") ?: extra["gpr.key"] as String? + } + } + } +} +rootProject.name = "ReVanced Manager" +include(":app") +include(":downloader-plugin") +include(":example-downloader-plugin")