Repository: bunny-mod/BunnyManager Branch: main Commit: 2a3b6a637b9e Files: 183 Total size: 669.3 KB Directory structure: gitextract_m7wwd48o/ ├── .github/ │ ├── FUNDING.yml │ └── workflows/ │ ├── build-debug.yml │ └── build-release.yml ├── .gitignore ├── LICENSE ├── README.md ├── app/ │ ├── .gitignore │ ├── build.gradle.kts │ ├── libs/ │ │ ├── convert-lspatch.sh │ │ └── lspatch.aar │ ├── proguard-rules.pro │ └── src/ │ └── main/ │ ├── AndroidManifest.xml │ ├── java/ │ │ └── dev/ │ │ └── beefers/ │ │ └── vendetta/ │ │ └── manager/ │ │ ├── ManagerApplication.kt │ │ ├── di/ │ │ │ ├── HttpModule.kt │ │ │ ├── ManagerModule.kt │ │ │ ├── RepositoryModule.kt │ │ │ └── ViewModelModule.kt │ │ ├── domain/ │ │ │ ├── manager/ │ │ │ │ ├── DownloadManager.kt │ │ │ │ ├── InstallManager.kt │ │ │ │ ├── PreferenceManager.kt │ │ │ │ └── base/ │ │ │ │ └── BasePreferenceManager.kt │ │ │ ├── receiver/ │ │ │ │ └── InstallReceiver.kt │ │ │ └── repository/ │ │ │ └── RestRepository.kt │ │ ├── installer/ │ │ │ ├── Installer.kt │ │ │ ├── session/ │ │ │ │ ├── SessionInstaller.kt │ │ │ │ └── SessionInstallerService.kt │ │ │ ├── shizuku/ │ │ │ │ ├── ShizukuInstaller.kt │ │ │ │ └── ShizukuPermissions.kt │ │ │ ├── step/ │ │ │ │ ├── Step.kt │ │ │ │ ├── StepGroup.kt │ │ │ │ ├── StepRunner.kt │ │ │ │ ├── StepStatus.kt │ │ │ │ ├── download/ │ │ │ │ │ ├── DownloadBaseStep.kt │ │ │ │ │ ├── DownloadLangStep.kt │ │ │ │ │ ├── DownloadLibsStep.kt │ │ │ │ │ ├── DownloadResourcesStep.kt │ │ │ │ │ ├── DownloadVendettaStep.kt │ │ │ │ │ └── base/ │ │ │ │ │ └── DownloadStep.kt │ │ │ │ ├── installing/ │ │ │ │ │ └── InstallStep.kt │ │ │ │ └── patching/ │ │ │ │ ├── AddVendettaStep.kt │ │ │ │ ├── PatchManifestsStep.kt │ │ │ │ ├── PresignApksStep.kt │ │ │ │ └── ReplaceIconStep.kt │ │ │ └── util/ │ │ │ ├── ArscUtil.kt │ │ │ ├── AxmlUtil.kt │ │ │ ├── LogEntry.kt │ │ │ ├── Logger.kt │ │ │ ├── ManifestPatcher.kt │ │ │ ├── Patcher.kt │ │ │ └── Signer.kt │ │ ├── network/ │ │ │ ├── dto/ │ │ │ │ ├── Commit.kt │ │ │ │ ├── Index.kt │ │ │ │ ├── Release.kt │ │ │ │ └── User.kt │ │ │ ├── service/ │ │ │ │ ├── HttpService.kt │ │ │ │ └── RestService.kt │ │ │ └── utils/ │ │ │ ├── ApiResponse.kt │ │ │ └── CommitsPagingSource.kt │ │ ├── ui/ │ │ │ ├── activity/ │ │ │ │ └── MainActivity.kt │ │ │ ├── components/ │ │ │ │ ├── Label.kt │ │ │ │ ├── NavBarSpacer.kt │ │ │ │ ├── RadioController.kt │ │ │ │ ├── SegmentedButton.kt │ │ │ │ ├── ThinDivider.kt │ │ │ │ └── settings/ │ │ │ │ ├── SettingsButton.kt │ │ │ │ ├── SettingsCategory.kt │ │ │ │ ├── SettingsChoiceDialog.kt │ │ │ │ ├── SettingsHeader.kt │ │ │ │ ├── SettingsItem.kt │ │ │ │ ├── SettingsItemChoice.kt │ │ │ │ ├── SettingsSwitch.kt │ │ │ │ └── SettingsTextField.kt │ │ │ ├── screen/ │ │ │ │ ├── about/ │ │ │ │ │ └── AboutScreen.kt │ │ │ │ ├── home/ │ │ │ │ │ └── HomeScreen.kt │ │ │ │ ├── installer/ │ │ │ │ │ ├── InstallerScreen.kt │ │ │ │ │ └── LogViewerScreen.kt │ │ │ │ ├── libraries/ │ │ │ │ │ └── LibrariesScreen.kt │ │ │ │ └── settings/ │ │ │ │ ├── AdvancedSettings.kt │ │ │ │ ├── AppearanceSettings.kt │ │ │ │ ├── CustomizationSettings.kt │ │ │ │ ├── DeveloperSettings.kt │ │ │ │ └── SettingsScreen.kt │ │ │ ├── theme/ │ │ │ │ ├── Theme.kt │ │ │ │ └── Type.kt │ │ │ ├── viewmodel/ │ │ │ │ ├── home/ │ │ │ │ │ └── HomeViewModel.kt │ │ │ │ ├── installer/ │ │ │ │ │ ├── InstallerViewModel.kt │ │ │ │ │ └── LogViewerViewModel.kt │ │ │ │ ├── libraries/ │ │ │ │ │ └── LibrariesViewModel.kt │ │ │ │ └── settings/ │ │ │ │ └── AdvancedSettingsViewModel.kt │ │ │ └── widgets/ │ │ │ ├── AppIcon.kt │ │ │ ├── about/ │ │ │ │ ├── LinkItem.kt │ │ │ │ ├── ListItem.kt │ │ │ │ └── UserEntry.kt │ │ │ ├── dialog/ │ │ │ │ ├── BackWarningDialog.kt │ │ │ │ ├── DownloadFailedDialog.kt │ │ │ │ ├── EndOfLifeDialog.kt │ │ │ │ └── StoragePermissionsDialog.kt │ │ │ ├── home/ │ │ │ │ ├── Commit.kt │ │ │ │ └── CommitList.kt │ │ │ ├── installer/ │ │ │ │ ├── LogLine.kt │ │ │ │ ├── StepGroupCard.kt │ │ │ │ ├── StepIcon.kt │ │ │ │ └── StepRow.kt │ │ │ ├── libraries/ │ │ │ │ ├── LibraryItem.kt │ │ │ │ └── LicenseBottomSheet.kt │ │ │ ├── settings/ │ │ │ │ ├── ThemePicker.kt │ │ │ │ ├── ThemePickerOption.kt │ │ │ │ └── ThemePreview.kt │ │ │ └── updater/ │ │ │ └── UpdateDialog.kt │ │ ├── updatechecker/ │ │ │ ├── reciever/ │ │ │ │ └── UpdateBroadcastReceiver.kt │ │ │ └── worker/ │ │ │ └── UpdateWorker.kt │ │ └── utils/ │ │ ├── Constants.kt │ │ ├── Context.kt │ │ ├── DimenUtils.kt │ │ ├── LazyUtils.kt │ │ ├── ModifierUtils.kt │ │ ├── NavUtils.kt │ │ ├── Utils.kt │ │ └── VersionUtils.kt │ └── res/ │ ├── drawable/ │ │ ├── bunny_logo.xml │ │ ├── ic_discord.xml │ │ ├── ic_discord_icon.xml │ │ ├── ic_github.xml │ │ ├── ic_update.xml │ │ ├── ts_avatars.xml │ │ ├── ts_bg.xml │ │ ├── ts_content.xml │ │ ├── ts_content_50.xml │ │ ├── ts_icon.xml │ │ ├── ts_outline.xml │ │ ├── ts_primary.xml │ │ ├── ts_status.xml │ │ ├── ts_surface_l1.xml │ │ └── ts_surface_l2.xml │ ├── drawable-v26/ │ │ └── ic_launcher.xml │ ├── values/ │ │ ├── colors.xml │ │ ├── strings.xml │ │ └── themes.xml │ ├── values-af-rZA/ │ │ └── strings.xml │ ├── values-ar-rSA/ │ │ └── strings.xml │ ├── values-ca-rES/ │ │ └── strings.xml │ ├── values-cs-rCZ/ │ │ └── strings.xml │ ├── values-da-rDK/ │ │ └── strings.xml │ ├── values-de-rDE/ │ │ └── strings.xml │ ├── values-el-rGR/ │ │ └── strings.xml │ ├── values-en-rUS/ │ │ └── strings.xml │ ├── values-es-rES/ │ │ └── strings.xml │ ├── values-fi-rFI/ │ │ └── strings.xml │ ├── values-fil-rPH/ │ │ └── strings.xml │ ├── values-fr-rFR/ │ │ └── strings.xml │ ├── values-hu-rHU/ │ │ └── strings.xml │ ├── values-in-rID/ │ │ └── strings.xml │ ├── values-it-rIT/ │ │ └── strings.xml │ ├── values-iw-rIL/ │ │ └── strings.xml │ ├── values-ja-rJP/ │ │ └── strings.xml │ ├── values-ko-rKR/ │ │ └── strings.xml │ ├── values-nl-rNL/ │ │ └── strings.xml │ ├── values-no-rNO/ │ │ └── strings.xml │ ├── values-or-rIN/ │ │ └── strings.xml │ ├── values-pl-rPL/ │ │ └── strings.xml │ ├── values-pt-rBR/ │ │ └── strings.xml │ ├── values-pt-rPT/ │ │ └── strings.xml │ ├── values-ro-rRO/ │ │ └── strings.xml │ ├── values-ru-rRU/ │ │ └── strings.xml │ ├── values-sr-rSP/ │ │ └── strings.xml │ ├── values-sv-rSE/ │ │ └── strings.xml │ ├── values-tr-rTR/ │ │ └── strings.xml │ ├── values-uk-rUA/ │ │ └── strings.xml │ ├── values-vi-rVN/ │ │ └── strings.xml │ ├── values-zh-rCN/ │ │ └── strings.xml │ ├── values-zh-rTW/ │ │ └── strings.xml │ └── xml/ │ ├── backup_rules.xml │ ├── data_extraction_rules.xml │ ├── locales_config.xml │ └── provider_paths.xml ├── build.gradle.kts ├── gradle/ │ ├── libs.versions.toml │ └── wrapper/ │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradle.properties ├── gradlew ├── gradlew.bat └── settings.gradle.kts ================================================ FILE CONTENTS ================================================ ================================================ FILE: .github/FUNDING.yml ================================================ github: wingio ================================================ FILE: .github/workflows/build-debug.yml ================================================ name: Build debug APK on: push: branches: - '*' paths-ignore: - '**.md' - '.idea/*' - 'LICENSE' pull_request: branches: - '*' paths-ignore: - '**.md' - '.idea/*' - 'LICENSE' workflow_dispatch: jobs: build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - name: Setup JDK 17 uses: actions/setup-java@v2 with: java-version: 17 distribution: 'temurin' - name: Setup Android SDK uses: android-actions/setup-android@v2.0.10 - name: Build APK run: chmod +x ./gradlew && ./gradlew assembleDebug - name: Upload APK uses: actions/upload-artifact@v4 with: name: manager-debug path: app/build/outputs/apk/debug/app-debug.apk ================================================ FILE: .github/workflows/build-release.yml ================================================ name: Build Release on: workflow_dispatch: inputs: versionName: required: true description: This releases version name default: "1.0.0" versionCode: required: true description: This releases version code default: "1000" jobs: build: runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@master - name: Setup JDK 17 uses: actions/setup-java@v2 with: java-version: 17 distribution: 'temurin' - name: Setup Android SDK uses: android-actions/setup-android@v2.0.10 - name: Set Version uses: chkfung/android-version-actions@v1.1 with: gradlePath: app/build.gradle.kts versionCode: ${{github.event.inputs.versionCode}} versionName: ${{github.event.inputs.versionName}} - name: Build Signed APK run: | echo "${{ secrets.keystore }}" | base64 -d > $GITHUB_WORKSPACE/signing-key.jks chmod +x ./gradlew ./gradlew packageReleaseUniversalApk -Pandroid.injected.signing.store.file=$GITHUB_WORKSPACE/signing-key.jks -Pandroid.injected.signing.store.password=${{ secrets.keystore_password }} -Pandroid.injected.signing.key.alias=${{ secrets.key_alias }} -Pandroid.injected.signing.key.password=${{ secrets.key_password }} - name: Release run: | mv app/build/outputs/apk_from_bundle/release/app-release-universal.apk ./Manager.apk git config --local user.email "actions@github.com" git config --local user.name "GitHub Actions" tag="${{ github.event.inputs.versionCode }}" git tag "$tag" git push origin "$tag" gh release create "$tag" \ --title "${{ github.event.inputs.versionName }}" \ --generate-notes \ ./Manager.apk env: GITHUB_TOKEN: '${{ secrets.GITHUB_TOKEN }}' ================================================ FILE: .gitignore ================================================ # Built application files *.apk *.ap_ *.aab # Files for the ART/Dalvik VM *.dex # Java class files *.class # Generated files bin/ gen/ out/ .kotlin/ # Uncomment the following line in case you need and you don't have the release build type files in your app # release/ # Gradle files .gradle/ build/ # Local configuration file (sdk path, etc) local.properties # Proguard folder generated by Eclipse proguard/ # Log Files *.log # Android Studio Navigation editor temp files .navigation/ # Android Studio captures folder captures/ # IntelliJ *.iml .idea .idea/workspace.xml .idea/tasks.xml .idea/gradle.xml .idea/assetWizardSettings.xml .idea/dictionaries .idea/libraries # Android Studio 3 in .gitignore file. .idea/caches .idea/modules.xml # Comment next line if keeping position of elements in Navigation Editor is relevant for you .idea/navEditor.xml # Keystore files # Uncomment the following lines if you do not want to check your keystore files in. #*.jks #*.keystore # External native build folder generated in Android Studio 2.2 and later .externalNativeBuild .cxx/ # Google Services (e.g. APIs or Firebase) # google-services.json # Freeline freeline.py freeline/ freeline_project_description.json # fastlane fastlane/report.xml fastlane/Preview.html fastlane/screenshots fastlane/test_output fastlane/readme.md # Version control vcs.xml # lint lint/intermediates/ lint/generated/ lint/outputs/ lint/tmp/ # lint/reports/ ================================================ FILE: LICENSE ================================================ Open Software License ("OSL") v. 3.0 This Open Software License (the "License") applies to any original work of authorship (the "Original Work") whose owner (the "Licensor") has placed the following licensing notice adjacent to the copyright notice for the Original Work: Licensed under the Open Software License version 3.0 1) Grant of Copyright License. Licensor grants You a worldwide, royalty-free, non-exclusive, sublicensable license, for the duration of the copyright, to do the following: a) to reproduce the Original Work in copies, either alone or as part of a collective work; b) to translate, adapt, alter, transform, modify, or arrange the Original Work, thereby creating derivative works ("Derivative Works") based upon the Original Work; c) to distribute or communicate copies of the Original Work and Derivative Works to the public, with the proviso that copies of Original Work or Derivative Works that You distribute or communicate shall be licensed under this Open Software License; d) to perform the Original Work publicly; and e) to display the Original Work publicly. 2) Grant of Patent License. Licensor grants You a worldwide, royalty-free, non-exclusive, sublicensable license, under patent claims owned or controlled by the Licensor that are embodied in the Original Work as furnished by the Licensor, for the duration of the patents, to make, use, sell, offer for sale, have made, and import the Original Work and Derivative Works. 3) Grant of Source Code License. The term "Source Code" means the preferred form of the Original Work for making modifications to it and all available documentation describing how to modify the Original Work. Licensor agrees to provide a machine-readable copy of the Source Code of the Original Work along with each copy of the Original Work that Licensor distributes. Licensor reserves the right to satisfy this obligation by placing a machine-readable copy of the Source Code in an information repository reasonably calculated to permit inexpensive and convenient access by You for as long as Licensor continues to distribute the Original Work. 4) Exclusions From License Grant. Neither the names of Licensor, nor the names of any contributors to the Original Work, nor any of their trademarks or service marks, may be used to endorse or promote products derived from this Original Work without express prior permission of the Licensor. Except as expressly stated herein, nothing in this License grants any license to Licensor's trademarks, copyrights, patents, trade secrets or any other intellectual property. No patent license is granted to make, use, sell, offer for sale, have made, or import embodiments of any patent claims other than the licensed claims defined in Section 2. No license is granted to the trademarks of Licensor even if such marks are included in the Original Work. Nothing in this License shall be interpreted to prohibit Licensor from licensing under terms different from this License any Original Work that Licensor otherwise would have a right to license. 5) External Deployment. The term "External Deployment" means the use, distribution, or communication of the Original Work or Derivative Works in any way such that the Original Work or Derivative Works may be used by anyone other than You, whether those works are distributed or communicated to those persons or made available as an application intended for use over a network. As an express condition for the grants of license hereunder, You must treat any External Deployment by You of the Original Work or a Derivative Work as a distribution under section 1(c). 6) Attribution Rights. You must retain, in the Source Code of any Derivative Works that You create, all copyright, patent, or trademark notices from the Source Code of the Original Work, as well as any notices of licensing and any descriptive text identified therein as an "Attribution Notice." You must cause the Source Code for any Derivative Works that You create to carry a prominent Attribution Notice reasonably calculated to inform recipients that You have modified the Original Work. 7) Warranty of Provenance and Disclaimer of Warranty. Licensor warrants that the copyright in and to the Original Work and the patent rights granted herein by Licensor are owned by the Licensor or are sublicensed to You under the terms of this License with the permission of the contributor(s) of those copyrights and patent rights. Except as expressly stated in the immediately preceding sentence, the Original Work is provided under this License on an "AS IS" BASIS and WITHOUT WARRANTY, either express or implied, including, without limitation, the warranties of non-infringement, merchantability or fitness for a particular purpose. THE ENTIRE RISK AS TO THE QUALITY OF THE ORIGINAL WORK IS WITH YOU. This DISCLAIMER OF WARRANTY constitutes an essential part of this License. No license to the Original Work is granted by this License except under this disclaimer. 8) Limitation of Liability. Under no circumstances and under no legal theory, whether in tort (including negligence), contract, or otherwise, shall the Licensor be liable to anyone for any indirect, special, incidental, or consequential damages of any character arising as a result of this License or the use of the Original Work including, without limitation, damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses. This limitation of liability shall not apply to the extent applicable law prohibits such limitation. 9) Acceptance and Termination. If, at any time, You expressly assented to this License, that assent indicates your clear and irrevocable acceptance of this License and all of its terms and conditions. If You distribute or communicate copies of the Original Work or a Derivative Work, You must make a reasonable effort under the circumstances to obtain the express assent of recipients to the terms of this License. This License conditions your rights to undertake the activities listed in Section 1, including your right to create Derivative Works based upon the Original Work, and doing so without honoring these terms and conditions is prohibited by copyright law and international treaty. Nothing in this License is intended to affect copyright exceptions and limitations (including "fair use" or "fair dealing"). This License shall terminate immediately and You may no longer exercise any of the rights granted to You by this License upon your failure to honor the conditions in Section 1(c). 10) Termination for Patent Action. This License shall terminate automatically and You may no longer exercise any of the rights granted to You by this License as of the date You commence an action, including a cross-claim or counterclaim, against Licensor or any licensee alleging that the Original Work infringes a patent. This termination provision shall not apply for an action alleging patent infringement by combinations of the Original Work with other software or hardware. 11) Jurisdiction, Venue and Governing Law. Any action or suit relating to this License may be brought only in the courts of a jurisdiction wherein the Licensor resides or in which Licensor conducts its primary business, and under the laws of that jurisdiction excluding its conflict-of-law provisions. The application of the United Nations Convention on Contracts for the International Sale of Goods is expressly excluded. Any use of the Original Work outside the scope of this License or after its termination shall be subject to the requirements and penalties of copyright or patent law in the appropriate jurisdiction. This section shall survive the termination of this License. 12) Attorneys' Fees. In any action to enforce the terms of this License or seeking damages relating thereto, the prevailing party shall be entitled to recover its costs and expenses, including, without limitation, reasonable attorneys' fees and costs incurred in connection with such action, including any appeal of such action. This section shall survive the termination of this License. 13) Miscellaneous. If any provision of this License is held to be unenforceable, such provision shall be reformed only to the extent necessary to make it enforceable. 14) Definition of "You" in This License. "You" throughout this License, whether in upper or lower case, means an individual or a legal entity exercising rights under, and complying with all of the terms of, this License. For legal entities, "You" includes any entity that controls, is controlled by, or is under common control with you. For purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. 15) Right to Use. You may use the Original Work in all ways not otherwise restricted or conditioned by this License or by law, and Licensor promises not to interfere with or be responsible for such uses by You. 16) Modification of This License. This License is Copyright © 2005 Lawrence Rosen. Permission is granted to copy, distribute, or communicate this License without modification. Nothing in this License permits You to modify this License as applied to the Original Work or to Derivative Works. However, You may modify the text of this License and copy, distribute or communicate your modified version (the "Modified License") and apply it to other original works of authorship subject to the following conditions: (i) You may not indicate in any way that your Modified License is the "Open Software License" or "OSL" and you may not use those names in the name of your Modified License; (ii) You must replace the notice specified in the first paragraph above with the notice "Licensed under " or with a notice of your own that is not confusingly similar to the notice in this License; and (iii) You may not claim that your original works are open source software unless your Modified License has been approved by Open Source Initiative (OSI) and You comply with its license review and certification process. ================================================ FILE: README.md ================================================ > [!IMPORTANT] > ## Project Archival > > **This project is no longer actively maintained and this repository has been archived.** > > We want to thank all contributors and users for their support over the project's lifetime. While development has ceased, the code remains available for those who may find it useful.
Bunny logo # Bunny Manager Easily install Bunny on Android [![Latest release](https://img.shields.io/github/v/release/pyoncord/BunnyManager?color=3AB8BA&display_name=release&label=Latest&style=for-the-badge)](https://github.com/pyoncord/BunnyManager/releases/latest) ---
![Debug build status](https://img.shields.io/github/actions/workflow/status/pyoncord/BunnyManager/build-debug.yml?label=Debug%20Build&logo=github&style=for-the-badge&branch=main) [![Stars](https://img.shields.io/github/stars/pyoncord/BunnyManager?logo=github&style=for-the-badge)](https://github.com/pyoncord/BunnyManager/stargazers) [![Discord](https://img.shields.io/discord/1196075698301968455?logo=discord&logoColor=white&style=for-the-badge)](https://discord.gg/XjYgWXHb9Q)
![GitHub code size in bytes](https://img.shields.io/github/languages/code-size/pyoncord/BunnyManager?logo=github&logoColor=%23fff&style=for-the-badge) ![Downloads (latest)](https://img.shields.io/github/downloads/pyoncord/BunnyManager/latest/total?style=for-the-badge&logo=github&label=Downloads%20(Latest)&color=blue) ![Total downloads](https://img.shields.io/github/downloads/pyoncord/BunnyManager/total?style=for-the-badge&logo=github&label=Downloads%20(Total)&color=blue) ![GitHub top language](https://img.shields.io/github/languages/top/pyoncord/BunnyManager?style=for-the-badge)
Build --- #### Prerequisites - [Git](https://git-scm.com/downloads) - [JDK 17](https://www.oracle.com/java/technologies/javase/jdk11-archive-downloads.html) - [Android SDK](https://developer.android.com/studio) #### Instructions 1. Clone the repo - `git clone https://github.com/pyoncord/BunnyManager.git && cd BunnyManager` 2. Build the project - Linux: `chmod +x ./gradlew && gradlew assembleDebug` - Windows: `./gradlew assembleDebug` 3. Install on device - [Enable USB debugging](https://developer.android.com/studio/debug/dev-options) and plug in your phone - Run `adb install app/build/outputs/apk/debug/app-debug.apk` ## Contributing This is an open-source project, you can do so without any programming. Here are a few things you can do: - [Test and report issues](https://github.com/pyoncord/BunnyManager/issues/new/choose) License --- Bunny Manager is licensed under the Open Software License version 3.0 [![License: OSL v3](https://img.shields.io/badge/License-OSL%20v3-blue.svg?style=for-the-badge)](https://github.com/pyoncord/BunnyManager/blob/main/LICENSE) ================================================ FILE: app/.gitignore ================================================ /build /release ================================================ FILE: app/build.gradle.kts ================================================ import java.io.ByteArrayOutputStream plugins { alias(libs.plugins.aboutlibraries) alias(libs.plugins.android.application) alias(libs.plugins.kotlin.android) alias(libs.plugins.kotlin.serialization) alias(libs.plugins.compose.compiler) } android { namespace = "dev.beefers.vendetta.manager" compileSdk = 35 defaultConfig { applicationId = "io.github.pyoncord.manager" minSdk = 28 targetSdk = 35 versionCode = 1100 versionName = "1.1.0" buildConfigField("String", "GIT_BRANCH", "\"${getCurrentBranch()}\"") buildConfigField("String", "GIT_COMMIT", "\"${getLatestCommit()}\"") buildConfigField("boolean", "GIT_LOCAL_COMMITS", "${hasLocalCommits()}") buildConfigField("boolean", "GIT_LOCAL_CHANGES", "${hasLocalChanges()}") testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" vectorDrawables { useSupportLibrary = true } } buildTypes { named("release") { isCrunchPngs = true isMinifyEnabled = true isShrinkResources = true proguardFiles( getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro", ) } } compileOptions { sourceCompatibility = JavaVersion.VERSION_11 targetCompatibility = JavaVersion.VERSION_11 } kotlinOptions { jvmTarget = "11" freeCompilerArgs += listOf( "-Xcontext-receivers", "-P", "plugin:androidx.compose.compiler.plugins.kotlin:reportsDestination=" + layout.buildDirectory.get().asFile.resolve("report").absolutePath, ) } buildFeatures { compose = true buildConfig = true } composeOptions { kotlinCompilerExtensionVersion = "1.5.6" } androidComponents { onVariants(selector().withBuildType("release")) { it.packaging.resources.excludes.apply { // Debug metadata add("/**/*.version") add("/kotlin-tooling-metadata.json") // Kotlin debugging (https://github.com/Kotlin/kotlinx.coroutines/issues/2274) add("/DebugProbesKt.bin") } } } packaging { resources { // Reflection symbol list (https://stackoverflow.com/a/41073782/13964629) excludes += "/**/*.kotlin_builtins" } } configurations { all { exclude(module = "listenablefuture") exclude(module = "error_prone_annotations") } } } dependencies { implementation(platform(libs.compose.bom)) implementation(libs.bundles.accompanist) implementation(libs.bundles.androidx) implementation(libs.bundles.coil) implementation(libs.bundles.compose) implementation(libs.bundles.koin) implementation(libs.bundles.ktor) implementation(libs.bundles.shizuku) implementation(libs.bundles.voyager) implementation(files("libs/lspatch.aar")) implementation(libs.aboutlibraries.core) implementation(libs.binaryResources) { exclude(module = "checker-qual") exclude(module = "jsr305") exclude(module = "guava") } implementation(libs.kotlinx.datetime) implementation(libs.kotlinx.collections) implementation(libs.zip.android) { artifact { type = "aar" } } } fun getCurrentBranch(): String? = exec("git", "symbolic-ref", "--short", "HEAD") fun getLatestCommit(): String? = exec("git", "rev-parse", "--short", "HEAD") fun hasLocalCommits(): Boolean { val branch = getCurrentBranch() ?: return false return exec("git", "log", "origin/$branch..HEAD")?.isNotBlank() ?: false } fun hasLocalChanges(): Boolean = exec("git", "status", "-s")?.isNotEmpty() ?: false fun exec(vararg command: String): String? { return try { val stdout = ByteArrayOutputStream() val errout = ByteArrayOutputStream() exec { commandLine = command.toList() standardOutput = stdout errorOutput = errout isIgnoreExitValue = true } if(errout.size() > 0) throw Error(errout.toString(Charsets.UTF_8)) stdout.toString(Charsets.UTF_8).trim() } catch (e: Throwable) { e.printStackTrace() null } } ================================================ FILE: app/libs/convert-lspatch.sh ================================================ # Download LSPatch jar and convert it to an aar # Jars cannot be used as a dependency without breaking R8 optimization # TODO: update to latest LSPatch #LSPATCH_URL="https://github.com/LSPosed/LSPatch/releases/download/v0.6/jar-v0.6-398-release.jar" LSPATCH_URL="https://github.com/vendetta-mod/VendettaManager/raw/5764b16a14c42d8449722f3656b2cb42019b82a8/app/libs/lspatch.jar" LSPATCH_FILE_NAME="lspatch" function cleanup { rm -rf $LSPATCH_FILE_NAME.jar META-INF AndroidManifest.xml classes.jar R.txt } cleanup # download lspatch curl -sSL -o $LSPATCH_FILE_NAME.jar $LSPATCH_URL # prepare aar contents unzip -q $LSPATCH_FILE_NAME.jar "META-INF/**" mv $LSPATCH_FILE_NAME.jar classes.jar touch R.txt cat >AndroidManifest.xml < EOF # add everything to aar rm -f $LSPATCH_FILE_NAME.aar zip -rq $LSPATCH_FILE_NAME.aar R.txt AndroidManifest.xml META-INF classes.jar cleanup ================================================ FILE: app/proguard-rules.pro ================================================ # Keep `Companion` object fields of serializable classes. # This avoids serializer lookup through `getDeclaredClasses` as done for named companion objects. -if @kotlinx.serialization.Serializable class ** -keepclassmembers class <1> { static <1>$Companion Companion; } # Keep `serializer()` on companion objects (both default and named) of serializable classes. -if @kotlinx.serialization.Serializable class ** { static **$* *; } -keepclassmembers class <2>$<3> { kotlinx.serialization.KSerializer serializer(...); } # Keep `INSTANCE.serializer()` of serializable objects. -if @kotlinx.serialization.Serializable class ** { public static ** INSTANCE; } -keepclassmembers class <1> { public static <1> INSTANCE; kotlinx.serialization.KSerializer serializer(...); } # @Serializable and @Polymorphic are used at runtime for polymorphic serialization. -keepattributes RuntimeVisibleAnnotations,AnnotationDefault -keepattributes InnerClasses # Needed for `getDeclaredClasses`. -keepnames class <1>$$serializer { # -keepnames suffices; class is kept when serializer() is kept. static <1>$$serializer INSTANCE; } # Fix missing apksig annotations and fields -keep class com.android.apksig.internal.** { *; } # Fix LSPatch breaking # ref: https://github.com/LSPosed/LSPatch/blob/bbe8d93fb9230f7b04babaf1c4a11642110f55a6/manager/proguard-rules.pro#L12-L18 -keep class com.beust.jcommander.** { *; } -keep class org.lsposed.lspatch.Patcher$Options { *; } -keep class org.lsposed.lspatch.share.LSPConfig { *; } -keep class org.lsposed.lspatch.share.PatchConfig { *; } -keepclassmembers class org.lsposed.patch.LSPatch { private ; } # Uncomment this to preserve the line number information for # debugging stack traces. -keepattributes SourceFile,LineNumberTable # Keep all names -dontobfuscate # Repackage classes into the top-level. -repackageclasses # Amount of optimization iterations, taken from an SO post -optimizationpasses 5 # Broaden access modifiers to increase results during optimization -allowaccessmodification # Ignore missing classes (automatically generated by AGP) -dontwarn com.google.auto.value.AutoValue$Builder -dontwarn com.google.auto.value.AutoValue -dontwarn org.bouncycastle.jsse.BCSSLParameters -dontwarn org.bouncycastle.jsse.BCSSLSocket -dontwarn org.bouncycastle.jsse.provider.BouncyCastleJsseProvider -dontwarn org.conscrypt.Conscrypt$Version -dontwarn org.conscrypt.Conscrypt -dontwarn org.conscrypt.ConscryptHostnameVerifier -dontwarn org.jetbrains.annotations.ApiStatus$Internal -dontwarn org.openjsse.javax.net.ssl.SSLParameters -dontwarn org.openjsse.javax.net.ssl.SSLSocket -dontwarn org.openjsse.net.ssl.OpenJSSE -dontwarn org.slf4j.impl.StaticLoggerBinder # Can be removed once compose ui artifact is updated to 1.4.0 # ref: https://issuetracker.google.com/issues/265188224?pli=1 -keep,allowshrinking class * extends androidx.compose.ui.node.ModifierNodeElement {} ================================================ FILE: app/src/main/AndroidManifest.xml ================================================ ================================================ FILE: app/src/main/java/dev/beefers/vendetta/manager/ManagerApplication.kt ================================================ package dev.beefers.vendetta.manager import android.app.Application import android.app.NotificationChannel import android.app.NotificationManager import android.content.Context import androidx.work.ExistingPeriodicWorkPolicy import androidx.work.PeriodicWorkRequestBuilder import androidx.work.WorkManager import dev.beefers.vendetta.manager.di.httpModule import dev.beefers.vendetta.manager.di.managerModule import dev.beefers.vendetta.manager.di.repositoryModule import dev.beefers.vendetta.manager.di.viewModelModule import dev.beefers.vendetta.manager.domain.manager.PreferenceManager import dev.beefers.vendetta.manager.domain.manager.UpdateCheckerDuration import dev.beefers.vendetta.manager.updatechecker.worker.UpdateWorker import org.koin.android.ext.android.get import org.koin.android.ext.koin.androidContext import org.koin.core.context.startKoin class ManagerApplication : Application() { override fun onCreate() { super.onCreate() initNotificationChannels() startKoin { androidContext(this@ManagerApplication) modules( httpModule, managerModule, viewModelModule, repositoryModule ) } val prefs: PreferenceManager = get() if (prefs.updateDuration != UpdateCheckerDuration.DISABLED) { val duration = prefs.updateDuration WorkManager.getInstance(applicationContext).enqueueUniquePeriodicWork( "dev.beefers.vendetta.manager.UPDATE_CHECK", ExistingPeriodicWorkPolicy.KEEP, PeriodicWorkRequestBuilder(duration.time, duration.unit).build() ) } } private fun initNotificationChannels() { val nm = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager val updates = NotificationChannel( "${BuildConfig.APPLICATION_ID}.notifications.UPDATE", "Discord updates", NotificationManager.IMPORTANCE_DEFAULT ) nm.createNotificationChannel(updates) } } ================================================ FILE: app/src/main/java/dev/beefers/vendetta/manager/di/HttpModule.kt ================================================ package dev.beefers.vendetta.manager.di import dev.beefers.vendetta.manager.network.service.HttpService import dev.beefers.vendetta.manager.network.service.RestService import io.ktor.client.HttpClient import io.ktor.client.engine.cio.CIO import io.ktor.client.plugins.contentnegotiation.ContentNegotiation import io.ktor.serialization.kotlinx.json.json import kotlinx.serialization.json.Json import org.koin.core.module.dsl.singleOf import org.koin.dsl.module val httpModule = module { fun provideJson() = Json { ignoreUnknownKeys = true isLenient = true } fun provideHttpClient(json: Json) = HttpClient(CIO) { install(ContentNegotiation) { json(json) } } singleOf(::provideJson) singleOf(::provideHttpClient) singleOf(::HttpService) singleOf(::RestService) } ================================================ FILE: app/src/main/java/dev/beefers/vendetta/manager/di/ManagerModule.kt ================================================ package dev.beefers.vendetta.manager.di import dev.beefers.vendetta.manager.domain.manager.DownloadManager import dev.beefers.vendetta.manager.domain.manager.InstallManager import dev.beefers.vendetta.manager.domain.manager.PreferenceManager import org.koin.core.module.dsl.singleOf import org.koin.dsl.module val managerModule = module { singleOf(::DownloadManager) singleOf(::PreferenceManager) singleOf(::InstallManager) } ================================================ FILE: app/src/main/java/dev/beefers/vendetta/manager/di/RepositoryModule.kt ================================================ package dev.beefers.vendetta.manager.di import dev.beefers.vendetta.manager.domain.repository.RestRepository import org.koin.core.module.dsl.singleOf import org.koin.dsl.module val repositoryModule = module { singleOf(::RestRepository) } ================================================ FILE: app/src/main/java/dev/beefers/vendetta/manager/di/ViewModelModule.kt ================================================ package dev.beefers.vendetta.manager.di import dev.beefers.vendetta.manager.ui.viewmodel.home.HomeViewModel import dev.beefers.vendetta.manager.ui.viewmodel.installer.InstallerViewModel import dev.beefers.vendetta.manager.ui.viewmodel.installer.LogViewerViewModel import dev.beefers.vendetta.manager.ui.viewmodel.libraries.LibrariesViewModel import dev.beefers.vendetta.manager.ui.viewmodel.settings.AdvancedSettingsViewModel import org.koin.core.module.dsl.factoryOf import org.koin.dsl.module val viewModelModule = module { factoryOf(::InstallerViewModel) factoryOf(::AdvancedSettingsViewModel) factoryOf(::HomeViewModel) factoryOf(::LogViewerViewModel) factoryOf(::LibrariesViewModel) } ================================================ FILE: app/src/main/java/dev/beefers/vendetta/manager/domain/manager/DownloadManager.kt ================================================ package dev.beefers.vendetta.manager.domain.manager import android.app.DownloadManager import android.content.Context import android.database.Cursor import android.net.Uri import androidx.core.content.getSystemService import kotlinx.coroutines.CancellationException import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.delay import java.io.File class DownloadManager( private val context: Context, private val prefs: PreferenceManager ) { suspend fun downloadDiscordApk(version: String, out: File, onProgressUpdate: (Float?) -> Unit): DownloadResult = download("${prefs.mirror.baseUrl}/tracker/download/$version/base", out, onProgressUpdate) suspend fun downloadSplit(version: String, split: String, out: File, onProgressUpdate: (Float?) -> Unit): DownloadResult = download("${prefs.mirror.baseUrl}/tracker/download/$version/$split", out, onProgressUpdate) suspend fun downloadVendetta(out: File, onProgressUpdate: (Float?) -> Unit) = download( "https://github.com/pyoncord/BunnyXposed/releases/latest/download/app-release.apk", out, onProgressUpdate ) suspend fun downloadUpdate(out: File) = download( "https://github.com/pyoncord/BunnyManager/releases/latest/download/Manager.apk", out ) { /* TODO: Update a progress bar in the update dialog */ } /** * Start a cancellable download with the system [DownloadManager]. * If the current [CoroutineScope] is cancelled, then the system download will be cancelled * almost immediately. * @param url Remote src url * @param out Target path to download to * @param onProgressUpdate Download progress update in a `[0,1]` range, and if null then the * download is currently in a pending state. This is called every 100ms. */ suspend fun download( url: String, out: File, onProgressUpdate: (Float?) -> Unit ): DownloadResult { val downloadManager = context.getSystemService() ?: throw IllegalStateException("DownloadManager service is not available") val downloadId = DownloadManager.Request(Uri.parse(url)) .setTitle("Bunny Manager") .setDescription("Downloading ${out.name}...") .setDestinationUri(Uri.fromFile(out)) .setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE) .setAllowedOverMetered(true) .setAllowedOverRoaming(true) .let(downloadManager::enqueue) // Repeatedly request download state until it is finished while (true) { try { // Hand over control to a suspend function to check for cancellation delay(100) } catch (_: CancellationException) { // If the running CoroutineScope has been cancelled, then gracefully cancel download downloadManager.remove(downloadId) return DownloadResult.Cancelled(systemTriggered = false) } // Request download status val cursor = DownloadManager.Query() .setFilterById(downloadId) .let(downloadManager::query) // No results in cursor, download was cancelled if (!cursor.moveToFirst()) { cursor.close() return DownloadResult.Cancelled(systemTriggered = true) } val statusColumn = cursor.getColumnIndex(DownloadManager.COLUMN_STATUS) val status = cursor.getInt(statusColumn) cursor.use { when (status) { DownloadManager.STATUS_PENDING, DownloadManager.STATUS_PAUSED -> onProgressUpdate(null) DownloadManager.STATUS_RUNNING -> onProgressUpdate(getDownloadProgress(cursor)) DownloadManager.STATUS_SUCCESSFUL -> return DownloadResult.Success DownloadManager.STATUS_FAILED -> { val reasonColumn = cursor.getColumnIndex(DownloadManager.COLUMN_REASON) val reason = cursor.getInt(reasonColumn) return DownloadResult.Error(debugReason = convertErrorCode(reason)) } } } } } private fun getDownloadProgress(queryCursor: Cursor): Float? { val bytesColumn = queryCursor.getColumnIndex(DownloadManager.COLUMN_BYTES_DOWNLOADED_SO_FAR) val bytes = queryCursor.getLong(bytesColumn) val totalBytesColumn = queryCursor.getColumnIndex(DownloadManager.COLUMN_TOTAL_SIZE_BYTES) val totalBytes = queryCursor.getLong(totalBytesColumn) if (totalBytes <= 0) return null return bytes.toFloat() / totalBytes } private fun convertErrorCode(code: Int) = when (code) { DownloadManager.ERROR_UNKNOWN -> "UNKNOWN" DownloadManager.ERROR_FILE_ERROR -> "FILE_ERROR" DownloadManager.ERROR_UNHANDLED_HTTP_CODE -> "UNHANDLED_HTTP_CODE" DownloadManager.ERROR_HTTP_DATA_ERROR -> "HTTP_DATA_ERROR" DownloadManager.ERROR_TOO_MANY_REDIRECTS -> "TOO_MANY_REDIRECTS" DownloadManager.ERROR_INSUFFICIENT_SPACE -> "INSUFFICIENT_SPACE" DownloadManager.ERROR_DEVICE_NOT_FOUND -> "DEVICE_NOT_FOUND" DownloadManager.ERROR_CANNOT_RESUME -> "CANNOT_RESUME" DownloadManager.ERROR_FILE_ALREADY_EXISTS -> "FILE_ALREADY_EXISTS" /* DownloadManager.ERROR_BLOCKED */ 1010 -> "NETWORK_BLOCKED" else -> "UNKNOWN_CODE" } } sealed interface DownloadResult { data object Success : DownloadResult data class Cancelled(val systemTriggered: Boolean) : DownloadResult data class Error(val debugReason: String) : DownloadResult } ================================================ FILE: app/src/main/java/dev/beefers/vendetta/manager/domain/manager/InstallManager.kt ================================================ package dev.beefers.vendetta.manager.domain.manager import android.annotation.SuppressLint import android.app.PendingIntent import android.content.Context import android.content.Intent import android.content.pm.PackageInfo import android.content.pm.PackageManager import android.os.Build import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue import dev.beefers.vendetta.manager.installer.session.InstallService class InstallManager( private val context: Context, private val prefs: PreferenceManager, ) { var current by mutableStateOf(null) init { getInstalled() } fun getInstalled() { current = try { when { Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU -> { context.packageManager.getPackageInfo( prefs.packageName.ifBlank { "io.github.pyoncord.app" }, PackageManager.PackageInfoFlags.of( 0L ) ) } else -> { context.packageManager.getPackageInfo( prefs.packageName.ifBlank { "io.github.pyoncord.app" }, 0 ) } } } catch (e: PackageManager.NameNotFoundException) { null } } fun uninstall() { current?.let { val callbackIntent = Intent(context, InstallService::class.java).apply { action = "vendetta.actions.ACTION_UNINSTALL" } @SuppressLint("UnspecifiedImmutableFlag") val contentIntent = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { PendingIntent.getService(context, 0, callbackIntent, PendingIntent.FLAG_MUTABLE) } else { PendingIntent.getService(context, 0, callbackIntent, 0) } context.packageManager.packageInstaller.uninstall( it.packageName, contentIntent.intentSender ) } } } ================================================ FILE: app/src/main/java/dev/beefers/vendetta/manager/domain/manager/PreferenceManager.kt ================================================ package dev.beefers.vendetta.manager.domain.manager import android.content.Context import android.os.Build import android.os.Environment import androidx.annotation.StringRes import dev.beefers.vendetta.manager.R import dev.beefers.vendetta.manager.domain.manager.base.BasePreferenceManager import dev.beefers.vendetta.manager.utils.DiscordVersion import java.io.File import java.util.concurrent.TimeUnit class PreferenceManager(context: Context) : BasePreferenceManager(context.getSharedPreferences("prefs", Context.MODE_PRIVATE)) { val DEFAULT_MODULE_LOCATION = (context.externalCacheDir ?: File(Environment.getExternalStorageDirectory(), Environment.DIRECTORY_DOWNLOADS).resolve("BunnyManager").also { it.mkdirs() }).resolve("xposed.apk") var packageName by stringPreference("package_name", "io.github.pyoncord.app") var appName by stringPreference("app_name", "Bunny") var discordVersion by stringPreference("discord_version", "") var moduleVersion by stringPreference("module_version", "") var patchIcon by booleanPreference("patch_icon", true) var debuggable by booleanPreference("debuggable", false) var mirror by enumPreference("mirror", Mirror.DEFAULT) var monet by booleanPreference("monet", Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) var isDeveloper by booleanPreference("is_developer", false) var autoClearCache by booleanPreference("auto_clear_cache", true) var theme by enumPreference("theme", Theme.SYSTEM) var channel by enumPreference("channel", DiscordVersion.Type.STABLE) var updateDuration by enumPreference("update_duration", UpdateCheckerDuration.HOURLY) var moduleLocation by filePreference("module_location", DEFAULT_MODULE_LOCATION) var installMethod by enumPreference("install_method", InstallMethod.DEFAULT) var logsAlternateBackground by booleanPreference("logs_alternate_bg", true) var logsLineWrap by booleanPreference("logs_line_wrap", false) var allowDowngrade by booleanPreference("allow_downgrade", false) init { // Will be removed next update if(mirror == Mirror.VENDETTA_ROCKS) mirror = Mirror.VENDETTA_ROCKS_ALT } } enum class Theme(@StringRes val labelRes: Int) { SYSTEM(R.string.theme_system), LIGHT(R.string.theme_light), DARK(R.string.theme_dark) } enum class UpdateCheckerDuration(@StringRes val labelRes: Int, val time: Long, val unit: TimeUnit) { DISABLED(R.string.duration_disabled, 0, TimeUnit.SECONDS), QUARTERLY(R.string.duration_fifteen_min, 15, TimeUnit.MINUTES), HALF_HOUR(R.string.duration_half_hour, 30, TimeUnit.MINUTES), HOURLY(R.string.duration_hourly, 1, TimeUnit.HOURS), BIHOURLY(R.string.duration_bihourly, 2, TimeUnit.HOURS), TWICE_DAILY(R.string.duration_twice_daily, 12, TimeUnit.HOURS), DAILY(R.string.duration_daily, 1, TimeUnit.DAYS), WEEKLY(R.string.duration_weekly, 7, TimeUnit.DAYS) } enum class Mirror(val baseUrl: String) { DEFAULT("https://tracker.vendetta.rocks"), VENDETTA_ROCKS("https://proxy.vendetta.rocks"), // Temporarily added for compatibility VENDETTA_ROCKS_ALT("https://proxy.vendetta.rocks"), K6("https://vd.k6.tf"), NEXPID("https://tracker.vd.nexpid.xyz") } enum class InstallMethod(@StringRes val labelRes: Int) { DEFAULT(R.string.default_installer), SHIZUKU(R.string.shizuku_installer) } ================================================ FILE: app/src/main/java/dev/beefers/vendetta/manager/domain/manager/base/BasePreferenceManager.kt ================================================ package dev.beefers.vendetta.manager.domain.manager.base import android.content.SharedPreferences import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue import androidx.compose.ui.graphics.Color import androidx.core.content.edit import java.io.File import kotlin.reflect.KProperty abstract class BasePreferenceManager( private val prefs: SharedPreferences ) { protected fun getString(key: String, defaultValue: String?) = prefs.getString(key, defaultValue)!! private fun getBoolean(key: String, defaultValue: Boolean) = prefs.getBoolean(key, defaultValue) private fun getInt(key: String, defaultValue: Int) = prefs.getInt(key, defaultValue) private fun getFloat(key: String, defaultValue: Float) = prefs.getFloat(key, defaultValue) private fun getColor(key: String, defaultValue: Color): Color { val c = prefs.getString(key, null) return if (c == null) defaultValue else Color(c.toULong()) } private fun getFile(key: String, defaultValue: File) = File(getString(key, defaultValue.absolutePath)) protected inline fun > getEnum(key: String, defaultValue: E) = enumValueOf(getString(key, defaultValue.name)) protected fun putString(key: String, value: String?) = prefs.edit { putString(key, value) } private fun putBoolean(key: String, value: Boolean) = prefs.edit { putBoolean(key, value) } private fun putInt(key: String, value: Int) = prefs.edit { putInt(key, value) } private fun putFloat(key: String, value: Float) = prefs.edit { putFloat(key, value) } private fun putColor(key: String, value: Color) = prefs.edit { putString(key, value.value.toString()) } private fun putFile(key: String, value: File) = putString(key, value.absolutePath) protected inline fun > putEnum(key: String, value: E) = putString(key, value.name) protected class Preference( private val key: String, defaultValue: T, getter: (key: String, defaultValue: T) -> T, private val setter: (key: String, newValue: T) -> Unit ) { @Suppress("RedundantSetter") var value by mutableStateOf(getter(key, defaultValue)) private set operator fun getValue(thisRef: Any?, property: KProperty<*>) = value operator fun setValue(thisRef: Any?, property: KProperty<*>, newValue: T) { value = newValue setter(key, newValue) } } protected fun stringPreference( key: String, defaultValue: String = "" ) = Preference( key = key, defaultValue = defaultValue, getter = ::getString, setter = ::putString ) protected fun booleanPreference( key: String, defaultValue: Boolean ) = Preference( key = key, defaultValue = defaultValue, getter = ::getBoolean, setter = ::putBoolean ) protected fun intPreference( key: String, defaultValue: Int ) = Preference( key = key, defaultValue = defaultValue, getter = ::getInt, setter = ::putInt ) protected fun floatPreference( key: String, defaultValue: Float ) = Preference( key = key, defaultValue = defaultValue, getter = ::getFloat, setter = ::putFloat ) protected fun colorPreference( key: String, defaultValue: Color ) = Preference( key = key, defaultValue = defaultValue, getter = ::getColor, setter = ::putColor ) protected fun filePreference( key: String, defaultValue: File ) = Preference( key = key, defaultValue = defaultValue, getter = ::getFile, setter = ::putFile ) protected inline fun > enumPreference( key: String, defaultValue: E ) = Preference( key = key, defaultValue = defaultValue, getter = ::getEnum, setter = ::putEnum ) } ================================================ FILE: app/src/main/java/dev/beefers/vendetta/manager/domain/receiver/InstallReceiver.kt ================================================ package dev.beefers.vendetta.manager.domain.receiver import android.annotation.SuppressLint import android.content.BroadcastReceiver import android.content.Context import android.content.Intent import dev.beefers.vendetta.manager.domain.manager.InstallManager import org.koin.core.component.KoinComponent import org.koin.core.component.inject class InstallReceiver : BroadcastReceiver(), KoinComponent { private val installManager: InstallManager by inject() @SuppressLint("UnsafeProtectedBroadcastReceiver") override fun onReceive(context: Context?, intent: Intent?) { installManager.getInstalled() } } ================================================ FILE: app/src/main/java/dev/beefers/vendetta/manager/domain/repository/RestRepository.kt ================================================ package dev.beefers.vendetta.manager.domain.repository import dev.beefers.vendetta.manager.network.service.RestService import dev.beefers.vendetta.manager.network.utils.transform import dev.beefers.vendetta.manager.utils.DiscordVersion class RestRepository( private val service: RestService ) { suspend fun getLatestRelease(repo: String) = service.getLatestRelease(repo) suspend fun getLatestDiscordVersions() = service.getLatestDiscordVersions().transform { mapOf( DiscordVersion.Type.ALPHA to DiscordVersion.fromVersionCode(it.latest.alpha), DiscordVersion.Type.BETA to DiscordVersion.fromVersionCode(it.latest.beta), DiscordVersion.Type.STABLE to DiscordVersion.fromVersionCode(it.latest.stable) ) } suspend fun getCommits(repo: String, page: Int = 1) = service.getCommits(repo, page) } ================================================ FILE: app/src/main/java/dev/beefers/vendetta/manager/installer/Installer.kt ================================================ package dev.beefers.vendetta.manager.installer import java.io.File interface Installer { suspend fun installApks(silent: Boolean = false, vararg apks: File) } ================================================ FILE: app/src/main/java/dev/beefers/vendetta/manager/installer/session/SessionInstaller.kt ================================================ package dev.beefers.vendetta.manager.installer.session import android.annotation.SuppressLint import android.app.PendingIntent import android.content.Context import android.content.Intent import android.content.pm.PackageInstaller.SessionParams import android.content.pm.PackageManager import android.os.Build import dev.beefers.vendetta.manager.installer.Installer import java.io.File internal class SessionInstaller(private val context: Context) : Installer { private val packageManager: PackageManager = context.packageManager override suspend fun installApks(silent: Boolean, vararg apks: File) { val params = SessionParams(SessionParams.MODE_FULL_INSTALL).apply { if (Build.VERSION.SDK_INT >= 31) { setInstallScenario(PackageManager.INSTALL_SCENARIO_FAST) if (silent) { setRequireUserAction(SessionParams.USER_ACTION_NOT_REQUIRED) } } } val packageInstaller = packageManager.packageInstaller val sessionId = packageInstaller.createSession(params) val session = packageInstaller.openSession(sessionId) apks.forEach { apk -> session.openWrite(apk.name, 0, apk.length()).use { it.write(apk.readBytes()) session.fsync(it) } } val callbackIntent = Intent(context, InstallService::class.java).apply { action = "vendetta.actions.ACTION_INSTALL" } @SuppressLint("UnspecifiedImmutableFlag") val contentIntent = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { PendingIntent.getService(context, 0, callbackIntent, PendingIntent.FLAG_MUTABLE) } else { PendingIntent.getService(context, 0, callbackIntent, 0) } session.commit(contentIntent.intentSender) session.close() } } ================================================ FILE: app/src/main/java/dev/beefers/vendetta/manager/installer/session/SessionInstallerService.kt ================================================ package dev.beefers.vendetta.manager.installer.session import android.app.Service import android.content.Intent import android.content.pm.PackageInstaller import android.os.IBinder import dev.beefers.vendetta.manager.R import dev.beefers.vendetta.manager.ui.activity.MainActivity import dev.beefers.vendetta.manager.utils.showToast class InstallService : Service() { private val messages = mapOf( PackageInstaller.STATUS_FAILURE to R.string.install_fail_generic, PackageInstaller.STATUS_FAILURE_BLOCKED to R.string.install_fail_blocked, PackageInstaller.STATUS_FAILURE_INVALID to R.string.install_fail_invalid, PackageInstaller.STATUS_FAILURE_CONFLICT to R.string.install_fail_conflict, PackageInstaller.STATUS_FAILURE_STORAGE to R.string.install_fail_storage, PackageInstaller.STATUS_FAILURE_INCOMPATIBLE to R.string.install_fail_incompatible, 8 /* STATUS_FAILURE_TIMEOUT (Added in Android 14) */ to R.string.install_fail_timeout ) override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int { val isInstall = intent.action == "vendetta.actions.ACTION_INSTALL" when (val statusCode = intent.getIntExtra(PackageInstaller.EXTRA_STATUS, -999)) { PackageInstaller.STATUS_PENDING_USER_ACTION -> { @Suppress("DEPRECATION") // No. val confirmationIntent = intent.getParcelableExtra(Intent.EXTRA_INTENT)!! .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) startActivity(confirmationIntent) } PackageInstaller.STATUS_SUCCESS -> if (isInstall) showToast(R.string.installer_success) PackageInstaller.STATUS_FAILURE_ABORTED -> if (isInstall) showToast(R.string.installer_aborted) else -> { if (isInstall) { messages[statusCode]?.let(::showToast) // Send error messages back to the activity for debugging (to be received by InstallerScreen) startActivity( Intent("vendetta.actions.ACTION_INSTALL_FINISHED").apply { setClass(this@InstallService, MainActivity::class.java) addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) putExtra("vendetta.extras.EXTRA_MESSAGE", intent.getStringExtra(PackageInstaller.EXTRA_STATUS_MESSAGE)) } ) } } } stopSelf() return START_NOT_STICKY } override fun onBind(intent: Intent): IBinder? = null } ================================================ FILE: app/src/main/java/dev/beefers/vendetta/manager/installer/shizuku/ShizukuInstaller.kt ================================================ package dev.beefers.vendetta.manager.installer.shizuku import android.content.Context import dev.beefers.vendetta.manager.R import dev.beefers.vendetta.manager.domain.manager.InstallManager import dev.beefers.vendetta.manager.installer.Installer import dev.beefers.vendetta.manager.utils.showToast import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import org.koin.core.component.KoinComponent import org.koin.core.component.inject import rikka.shizuku.Shizuku import java.io.File class ShizukuInstaller(private val context: Context) : Installer, KoinComponent { val installManager: InstallManager by inject() override suspend fun installApks(silent: Boolean, vararg apks: File) { if (!ShizukuPermissions.waitShizukuPermissions()) { withContext(Dispatchers.Main) { context.showToast(R.string.msg_shizuku_denied, short = false) } throw Error("Failed to install due to missing Shizuku permissions") } val tempDir = File("/data/local/tmp") val movedApks = mutableListOf() // Copy each split to tmp apks.forEach { val moveCommand = "cp ${it.absolutePath} ${tempDir.absolutePath}" val moveResult = executeShellCommand(moveCommand) if(moveResult.isBlank()) movedApks.add(File(tempDir.absolutePath, it.name)) else throw RuntimeException("Failed to move ${it.absolutePath} to temp dir") } val installCommand = "pm install ${movedApks.joinToString(" ") { it.absolutePath }}" executeShellCommand(installCommand) installManager.getInstalled() movedApks.forEach { it.delete() } } private fun executeShellCommand(command: String): String { @Suppress("DEPRECATION") val process = Shizuku.newProcess(arrayOf("sh", "-c", command), null, null) val errorStr = process.errorStream.bufferedReader().use { it.readText().trim() } if(errorStr.isNotBlank()) throw RuntimeException("Failed to execute $command:\n\n$errorStr") return process.inputStream.bufferedReader().use { it.readText().trim() } } } ================================================ FILE: app/src/main/java/dev/beefers/vendetta/manager/installer/shizuku/ShizukuPermissions.kt ================================================ package dev.beefers.vendetta.manager.installer.shizuku import android.content.pm.PackageManager import kotlinx.coroutines.DelicateCoroutinesApi import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.first import kotlinx.coroutines.launch import rikka.shizuku.Shizuku import rikka.shizuku.Shizuku.OnRequestPermissionResultListener @OptIn(DelicateCoroutinesApi::class) object ShizukuPermissions { private const val REQUEST_CODE = 1 private val _permissionsGranted = MutableSharedFlow(replay = 0) private lateinit var permissionResultListener: OnRequestPermissionResultListener fun requestShizukuPermissions() { if (!Shizuku.pingBinder()) { GlobalScope.launch { _permissionsGranted.emit(false) } return } if (Shizuku.checkSelfPermission() == PackageManager.PERMISSION_GRANTED) { GlobalScope.launch { _permissionsGranted.emit(true) } return } Shizuku.addRequestPermissionResultListener(permissionResultListener) Shizuku.requestPermission(REQUEST_CODE) } suspend fun waitShizukuPermissions(): Boolean { requestShizukuPermissions() return _permissionsGranted.first() } init { permissionResultListener = OnRequestPermissionResultListener { requestCode, grantResult -> if (requestCode != REQUEST_CODE) return@OnRequestPermissionResultListener Shizuku.removeRequestPermissionResultListener(permissionResultListener) GlobalScope.launch { _permissionsGranted.emit(grantResult == PackageManager.PERMISSION_GRANTED) } } } } ================================================ FILE: app/src/main/java/dev/beefers/vendetta/manager/installer/step/Step.kt ================================================ package dev.beefers.vendetta.manager.installer.step import androidx.annotation.StringRes import androidx.compose.runtime.Stable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue import org.koin.core.component.KoinComponent import kotlin.time.measureTimedValue /** * A distinct step to be ran while patching */ @Stable abstract class Step: KoinComponent { /** * Group this step belongs to */ abstract val group: StepGroup /** * Label used in the installer ui */ @get:StringRes abstract val nameRes: Int /** * Current status for this step */ var status by mutableStateOf(StepStatus.QUEUED) protected set /** * How much progress this step has made, use null if unknown */ var progress by mutableStateOf(null) protected set /** * How long this step took to run, in milliseconds */ var durationMs by mutableIntStateOf(0) private set /** * Runs this step * * @param runner The host runner, used to share information between steps */ protected abstract suspend fun run(runner: StepRunner) /** * Safely runs this step, catching any errors and timing how long it runs. * * @param runner The host runner, used to share information between steps */ suspend fun runCatching(runner: StepRunner): Throwable? { if (status != StepStatus.QUEUED) throw IllegalStateException("Cannot execute a step that has already started") status = StepStatus.ONGOING val (error, time) = measureTimedValue { try { run(runner) status = StepStatus.SUCCESSFUL null } catch (t: Throwable) { status = StepStatus.UNSUCCESSFUL t } } durationMs = time.inWholeMilliseconds.toInt() return error } } ================================================ FILE: app/src/main/java/dev/beefers/vendetta/manager/installer/step/StepGroup.kt ================================================ package dev.beefers.vendetta.manager.installer.step import androidx.annotation.StringRes import dev.beefers.vendetta.manager.R /** * Represents a group of [Step]s */ enum class StepGroup(@StringRes val nameRes: Int) { /** * All steps deal with downloading files remotely */ DL(R.string.group_download), /** * Steps that modify the APKs */ PATCHING(R.string.group_patch), /** * Only contains the [install step][dev.beefers.vendetta.manager.installer.step.installing.InstallStep] */ INSTALLING(R.string.group_installing) } ================================================ FILE: app/src/main/java/dev/beefers/vendetta/manager/installer/step/StepRunner.kt ================================================ package dev.beefers.vendetta.manager.installer.step import android.content.Context import android.os.Build import android.os.Environment import androidx.compose.runtime.Stable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue import dev.beefers.vendetta.manager.BuildConfig import dev.beefers.vendetta.manager.domain.manager.PreferenceManager import dev.beefers.vendetta.manager.installer.step.download.DownloadBaseStep import dev.beefers.vendetta.manager.installer.step.download.DownloadLangStep import dev.beefers.vendetta.manager.installer.step.download.DownloadLibsStep import dev.beefers.vendetta.manager.installer.step.download.DownloadResourcesStep import dev.beefers.vendetta.manager.installer.step.download.DownloadVendettaStep import dev.beefers.vendetta.manager.installer.step.installing.InstallStep import dev.beefers.vendetta.manager.installer.step.patching.AddVendettaStep import dev.beefers.vendetta.manager.installer.step.patching.PatchManifestsStep import dev.beefers.vendetta.manager.installer.step.patching.PresignApksStep import dev.beefers.vendetta.manager.installer.step.patching.ReplaceIconStep import dev.beefers.vendetta.manager.installer.util.LogEntry import dev.beefers.vendetta.manager.installer.util.Logger import dev.beefers.vendetta.manager.utils.DiscordVersion import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.delay import org.koin.core.component.KoinComponent import org.koin.core.component.inject import java.io.File /** * Runs all installation steps in order * * Credit to rushii (github.com/rushiiMachine) * * @param discordVersion Version of Discord to inject Vendetta into */ @Stable class StepRunner( private val discordVersion: DiscordVersion ): KoinComponent { private val preferenceManager: PreferenceManager by inject() private val context: Context by inject() private val debugInfo = """ Bunny Manager v${BuildConfig.VERSION_NAME} Built from commit ${BuildConfig.GIT_COMMIT} on ${BuildConfig.GIT_BRANCH} ${if (BuildConfig.GIT_LOCAL_CHANGES || BuildConfig.GIT_LOCAL_COMMITS) "(Changes Present)" else ""} Running Android ${Build.VERSION.RELEASE}, API level ${Build.VERSION.SDK_INT} Supported ABIs: ${Build.SUPPORTED_ABIS.joinToString()} Device: ${Build.MANUFACTURER} - ${Build.MODEL} (${Build.DEVICE}) ${if(Build.VERSION.SDK_INT > Build.VERSION_CODES.S) "SOC: ${Build.SOC_MANUFACTURER} ${Build.SOC_MODEL}\n" else "\n\n"} Adding Bunny to Discord v$discordVersion """.trimIndent() /** * Logger associated with this runner */ val logger = Logger("StepRunner").also { logger -> debugInfo.split("\n").forEach { logger.logs += LogEntry(it, LogEntry.Level.INFO) // Add debug information to logs but don't print to logcat } } /** * Root directory for all downloaded files */ private val cacheDir = context.externalCacheDir ?: File(Environment.getExternalStorageDirectory(), Environment.DIRECTORY_DOWNLOADS) .resolve("BunnyManager") .also { it.mkdirs() } /** * Where version specific downloads are persisted */ private val discordCacheDir = cacheDir.resolve(discordVersion.toVersionCode()) /** * Working directory where apks are directly modified (i.e. replacing the app icon) */ private val patchedDir = discordCacheDir.resolve("patched").also { it.deleteRecursively() } /** * Where apks are moved to once signed */ private val signedDir = discordCacheDir.resolve("signed").also { it.deleteRecursively() } /** * Output directory for LSPatch */ private val lspatchedDir = patchedDir.resolve("lspatched").also { it.deleteRecursively() } var currentStep by mutableStateOf(null) private set /** * Whether or not the patching/installation process has completed. * Note that this does not mean all steps were finished successfully */ var completed by mutableStateOf(false) private set /** * Whether or not a download step failed, this is only for errors related to network conditions and not cancellations */ var downloadErrored by mutableStateOf(false) /** * List of steps to go through for this install * * ORDER MATTERS */ val steps: ImmutableList = buildList { // Downloading add(DownloadBaseStep(discordCacheDir, patchedDir, discordVersion.toVersionCode())) add(DownloadLibsStep(discordCacheDir, patchedDir, discordVersion.toVersionCode())) add(DownloadLangStep(discordCacheDir, patchedDir, discordVersion.toVersionCode())) add(DownloadResourcesStep(discordCacheDir, patchedDir, discordVersion.toVersionCode())) add(DownloadVendettaStep(patchedDir)) // Patching if (preferenceManager.patchIcon) add(ReplaceIconStep()) add(PatchManifestsStep()) add(PresignApksStep(signedDir)) add(AddVendettaStep(signedDir, lspatchedDir)) // Installing add(InstallStep(lspatchedDir)) }.toImmutableList() /** * Get a step that has already been successfully executed. * This is used to retrieve previously executed dependency steps from a later step. */ inline fun getCompletedStep(): T { val step = steps.asSequence() .filterIsInstance() .filter { it.status == StepStatus.SUCCESSFUL } .firstOrNull() if (step == null) { throw IllegalArgumentException("No completed step ${T::class.simpleName} exists in container") } return step } /** * Clears all cached files */ fun clearCache() { cacheDir.deleteRecursively() } /** * Run all the [steps] in order */ suspend fun runAll(): Throwable? { for (step in steps) { if (completed) return null // Failsafe in case runner is incorrectly marked as not completed too early currentStep = step val error = step.runCatching(this) if (error != null) { logger.e("Failed on ${step::class.simpleName}", error) completed = true return error } // Add delay for human psychology and // better group visibility in UI (the active group can change way too fast) if (!preferenceManager.isDeveloper && step.durationMs < 1000) { delay(1000L - step.durationMs) } } completed = true return null } } ================================================ FILE: app/src/main/java/dev/beefers/vendetta/manager/installer/step/StepStatus.kt ================================================ package dev.beefers.vendetta.manager.installer.step enum class StepStatus { /** * Currently in progress */ ONGOING, /** * Completed with no errors */ SUCCESSFUL, /** * Completed with an error */ UNSUCCESSFUL, /** * Has not yet been ran */ QUEUED } ================================================ FILE: app/src/main/java/dev/beefers/vendetta/manager/installer/step/download/DownloadBaseStep.kt ================================================ package dev.beefers.vendetta.manager.installer.step.download import androidx.compose.runtime.Stable import dev.beefers.vendetta.manager.R import dev.beefers.vendetta.manager.installer.step.download.base.DownloadStep import java.io.File /** * Downloads the base Discord APK */ @Stable class DownloadBaseStep( dir: File, workingDir: File, version: String ): DownloadStep() { override val nameRes = R.string.step_dl_base override val url: String = "$baseUrl/tracker/download/$version/base" override val destination = dir.resolve("base-$version.apk") override val workingCopy = workingDir.resolve("base-$version.apk") } ================================================ FILE: app/src/main/java/dev/beefers/vendetta/manager/installer/step/download/DownloadLangStep.kt ================================================ package dev.beefers.vendetta.manager.installer.step.download import androidx.compose.runtime.Stable import dev.beefers.vendetta.manager.R import dev.beefers.vendetta.manager.installer.step.download.base.DownloadStep import java.io.File /** * Downloads the languages split, will always be English because Discord doesn't store their strings in this split */ @Stable class DownloadLangStep( dir: File, workingDir: File, version: String ): DownloadStep() { override val nameRes = R.string.step_dl_lang override val url: String = "$baseUrl/tracker/download/$version/config.en" override val destination = dir.resolve("config.en-$version.apk") override val workingCopy = workingDir.resolve("config.en-$version.apk") } ================================================ FILE: app/src/main/java/dev/beefers/vendetta/manager/installer/step/download/DownloadLibsStep.kt ================================================ package dev.beefers.vendetta.manager.installer.step.download import android.os.Build import androidx.compose.runtime.Stable import dev.beefers.vendetta.manager.R import dev.beefers.vendetta.manager.installer.step.download.base.DownloadStep import java.io.File /** * Downloads the split containing the native libraries for the current devices architecture */ @Stable class DownloadLibsStep( dir: File, workingDir: File, version: String ): DownloadStep() { /** * Supported CPU architecture for this device, used to download the correct library split */ private val arch = Build.SUPPORTED_ABIS.first().replace("-v", "_v") override val nameRes = R.string.step_dl_lib override val url: String = "$baseUrl/tracker/download/$version/config.$arch" override val destination = dir.resolve("config.$arch-$version.apk") override val workingCopy = workingDir.resolve("config.$arch-$version.apk") } ================================================ FILE: app/src/main/java/dev/beefers/vendetta/manager/installer/step/download/DownloadResourcesStep.kt ================================================ package dev.beefers.vendetta.manager.installer.step.download import androidx.compose.runtime.Stable import dev.beefers.vendetta.manager.R import dev.beefers.vendetta.manager.installer.step.download.base.DownloadStep import java.io.File /** * Downloads the split containing all images, fonts, and other assets */ @Stable class DownloadResourcesStep( dir: File, workingDir: File, version: String ): DownloadStep() { override val nameRes = R.string.step_dl_res override val url: String = "$baseUrl/tracker/download/$version/config.xxhdpi" override val destination = dir.resolve("config.xxhdpi-$version.apk") override val workingCopy = workingDir.resolve("config.xxhdpi-$version.apk") } ================================================ FILE: app/src/main/java/dev/beefers/vendetta/manager/installer/step/download/DownloadVendettaStep.kt ================================================ package dev.beefers.vendetta.manager.installer.step.download import androidx.compose.runtime.Stable import dev.beefers.vendetta.manager.R import dev.beefers.vendetta.manager.installer.step.download.base.DownloadStep import java.io.File /** * Downloads the Vendetta XPosed module * * https://github.com/pyoncord/BunnyXposed */ @Stable class DownloadVendettaStep( workingDir: File ): DownloadStep() { override val nameRes = R.string.step_dl_vd override val url: String = "https://github.com/pyoncord/BunnyXposed/releases/latest/download/app-release.apk" override val destination = preferenceManager.moduleLocation override val workingCopy = workingDir.resolve("xposed.apk") } ================================================ FILE: app/src/main/java/dev/beefers/vendetta/manager/installer/step/download/base/DownloadStep.kt ================================================ package dev.beefers.vendetta.manager.installer.step.download.base import android.content.Context import androidx.compose.runtime.Stable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue import dev.beefers.vendetta.manager.R import dev.beefers.vendetta.manager.domain.manager.DownloadManager import dev.beefers.vendetta.manager.domain.manager.DownloadResult import dev.beefers.vendetta.manager.domain.manager.PreferenceManager import dev.beefers.vendetta.manager.installer.step.Step import dev.beefers.vendetta.manager.installer.step.StepGroup import dev.beefers.vendetta.manager.installer.step.StepRunner import dev.beefers.vendetta.manager.installer.step.StepStatus import dev.beefers.vendetta.manager.utils.mainThread import dev.beefers.vendetta.manager.utils.showToast import kotlinx.coroutines.CancellationException import org.koin.core.component.inject import java.io.File import kotlin.math.roundToInt /** * Specialized step used to download a file * * Files are downloaded to [destination] then copied to [workingCopy] for safe patching */ @Stable abstract class DownloadStep: Step() { protected val preferenceManager: PreferenceManager by inject() protected val baseUrl = preferenceManager.mirror.baseUrl private val downloadManager: DownloadManager by inject() private val context: Context by inject() /** * Url of the desired file to download */ abstract val url: String /** * Where to download the file to */ abstract val destination: File /** * Where the downloaded file should be copied to so that it can be used for patching */ abstract val workingCopy: File override val group: StepGroup = StepGroup.DL var cached by mutableStateOf(false) private set /** * Verifies that a file was properly downloaded */ open suspend fun verify() { if (!destination.exists()) error("Downloaded file is missing: ${destination.absolutePath}") if (destination.length() <= 0) error("Downloaded file is empty: ${destination.absolutePath}") } override suspend fun run(runner: StepRunner) { val fileName = destination.name runner.logger.i("Checking if $fileName is cached") if (destination.exists()) { runner.logger.i("Checking if $fileName isn't empty") if (destination.length() > 0) { runner.logger.i("$fileName is cached") cached = true runner.logger.i("Moving $fileName to working directory") destination.copyTo(workingCopy, true) status = StepStatus.SUCCESSFUL return } runner.logger.i("Deleting empty file: $fileName") destination.delete() } runner.logger.i("$fileName was not properly cached, downloading now") var lastProgress: Float? = null val result = downloadManager.download(url, destination) { newProgress -> progress = newProgress if (newProgress != lastProgress && newProgress != null) { lastProgress = newProgress runner.logger.d("$fileName download progress: ${(lastProgress!! * 100f).roundToInt()}%") } } when (result) { is DownloadResult.Success -> { try { runner.logger.i("Verifying downloaded file") verify() runner.logger.i("$fileName downloaded successfully") } catch (t: Throwable) { mainThread { context.showToast(R.string.msg_download_verify_failed) } throw t } runner.logger.i("Moving $fileName to working directory") destination.copyTo(workingCopy, true) } is DownloadResult.Error -> { mainThread { context.showToast(R.string.msg_download_failed) runner.downloadErrored = true } throw Error("Failed to download: ${result.debugReason}") } is DownloadResult.Cancelled -> { status = StepStatus.UNSUCCESSFUL throw CancellationException("$fileName download cancelled") } } } } ================================================ FILE: app/src/main/java/dev/beefers/vendetta/manager/installer/step/installing/InstallStep.kt ================================================ package dev.beefers.vendetta.manager.installer.step.installing import android.content.Context import dev.beefers.vendetta.manager.R import dev.beefers.vendetta.manager.domain.manager.InstallMethod import dev.beefers.vendetta.manager.domain.manager.PreferenceManager import dev.beefers.vendetta.manager.installer.Installer import dev.beefers.vendetta.manager.installer.session.SessionInstaller import dev.beefers.vendetta.manager.installer.shizuku.ShizukuInstaller import dev.beefers.vendetta.manager.installer.step.Step import dev.beefers.vendetta.manager.installer.step.StepGroup import dev.beefers.vendetta.manager.installer.step.StepRunner import dev.beefers.vendetta.manager.utils.isMiui import org.koin.core.component.inject import java.io.File /** * Installs all the modified splits with the users desired [Installer] * * @see SessionInstaller * @see ShizukuInstaller * * @param lspatchedDir Where all the patched APKs are */ class InstallStep( private val lspatchedDir: File ): Step() { private val preferences: PreferenceManager by inject() private val context: Context by inject() override val group = StepGroup.INSTALLING override val nameRes = R.string.step_installing override suspend fun run(runner: StepRunner) { runner.logger.i("Installing apks") val files = lspatchedDir.listFiles() ?.takeIf { it.isNotEmpty() } ?: throw Error("Missing APKs from LSPatch step; failure likely") val installer: Installer = when (preferences.installMethod) { InstallMethod.DEFAULT -> SessionInstaller(context) InstallMethod.SHIZUKU -> ShizukuInstaller(context) } installer.installApks(silent = !isMiui, *files) } } ================================================ FILE: app/src/main/java/dev/beefers/vendetta/manager/installer/step/patching/AddVendettaStep.kt ================================================ package dev.beefers.vendetta.manager.installer.step.patching import dev.beefers.vendetta.manager.R import dev.beefers.vendetta.manager.installer.step.Step import dev.beefers.vendetta.manager.installer.step.StepGroup import dev.beefers.vendetta.manager.installer.step.StepRunner import dev.beefers.vendetta.manager.installer.step.download.DownloadVendettaStep import dev.beefers.vendetta.manager.installer.util.Patcher import java.io.File /** * Uses LSPatch to inject the Vendetta XPosed module into Discord * * @param signedDir The signed apks to patch * @param lspatchedDir Output directory for LSPatch */ class AddVendettaStep( private val signedDir: File, private val lspatchedDir: File ) : Step() { override val group = StepGroup.PATCHING override val nameRes = R.string.step_add_vd override suspend fun run(runner: StepRunner) { val vendetta = runner.getCompletedStep().workingCopy runner.logger.i("Adding BunnyXposed module with LSPatch") val files = signedDir.listFiles() ?.takeIf { it.isNotEmpty() } ?: throw Error("Missing APKs from signing step") Patcher.patch( runner.logger, outputDir = lspatchedDir, apkPaths = files.map { it.absolutePath }, embeddedModules = listOf(vendetta.absolutePath) ) } } ================================================ FILE: app/src/main/java/dev/beefers/vendetta/manager/installer/step/patching/PatchManifestsStep.kt ================================================ package dev.beefers.vendetta.manager.installer.step.patching import com.github.diamondminer88.zip.ZipReader import com.github.diamondminer88.zip.ZipWriter import dev.beefers.vendetta.manager.R import dev.beefers.vendetta.manager.domain.manager.PreferenceManager import dev.beefers.vendetta.manager.installer.step.Step import dev.beefers.vendetta.manager.installer.step.StepGroup import dev.beefers.vendetta.manager.installer.step.StepRunner import dev.beefers.vendetta.manager.installer.step.download.DownloadBaseStep import dev.beefers.vendetta.manager.installer.step.download.DownloadLangStep import dev.beefers.vendetta.manager.installer.step.download.DownloadLibsStep import dev.beefers.vendetta.manager.installer.step.download.DownloadResourcesStep import dev.beefers.vendetta.manager.installer.util.ManifestPatcher import org.koin.core.component.inject /** * Modifies each APKs manifest in order to change the package and app name as well as whether or not its debuggable */ class PatchManifestsStep : Step() { private val preferences: PreferenceManager by inject() override val group = StepGroup.PATCHING override val nameRes = R.string.step_patch_manifests override suspend fun run(runner: StepRunner) { val baseApk = runner.getCompletedStep().workingCopy val libsApk = runner.getCompletedStep().workingCopy val langApk = runner.getCompletedStep().workingCopy val resApk = runner.getCompletedStep().workingCopy arrayOf(baseApk, libsApk, langApk, resApk).forEach { apk -> runner.logger.i("Reading AndroidManifest.xml from ${apk.name}") val manifest = ZipReader(apk) .use { zip -> zip.openEntry("AndroidManifest.xml")?.read() } ?: throw IllegalStateException("No manifest in ${apk.name}") ZipWriter(apk, true).use { zip -> runner.logger.i("Changing package and app name in ${apk.name}") val patchedManifestBytes = if (apk == baseApk) { ManifestPatcher.patchManifest( manifestBytes = manifest, packageName = preferences.packageName, appName = preferences.appName, debuggable = preferences.debuggable, ) } else { runner.logger.i("Changing package name in ${apk.name}") ManifestPatcher.renamePackage(manifest, preferences.packageName) } runner.logger.i("Deleting old AndroidManifest.xml in ${apk.name}") zip.deleteEntry( "AndroidManifest.xml", /* fillVoid = */ apk == libsApk || apk == baseApk ) // Preserve alignment in libs apk runner.logger.i("Adding patched AndroidManifest.xml in ${apk.name}") zip.writeEntry("AndroidManifest.xml", patchedManifestBytes) } } } } ================================================ FILE: app/src/main/java/dev/beefers/vendetta/manager/installer/step/patching/PresignApksStep.kt ================================================ package dev.beefers.vendetta.manager.installer.step.patching import android.os.Build import com.github.diamondminer88.zip.ZipCompression import com.github.diamondminer88.zip.ZipReader import com.github.diamondminer88.zip.ZipWriter import dev.beefers.vendetta.manager.R import dev.beefers.vendetta.manager.installer.step.Step import dev.beefers.vendetta.manager.installer.step.StepGroup import dev.beefers.vendetta.manager.installer.step.StepRunner import dev.beefers.vendetta.manager.installer.step.download.DownloadBaseStep import dev.beefers.vendetta.manager.installer.step.download.DownloadLangStep import dev.beefers.vendetta.manager.installer.step.download.DownloadLibsStep import dev.beefers.vendetta.manager.installer.step.download.DownloadResourcesStep import dev.beefers.vendetta.manager.installer.util.Signer import java.io.File /** * Sign all patched apks before being ran through LSPatch, this is required due to LSPatch not liking unsigned apks. * * @param signedDir Where to output all signed apks */ class PresignApksStep( private val signedDir: File ) : Step() { override val group = StepGroup.PATCHING override val nameRes = R.string.step_signing override suspend fun run(runner: StepRunner) { val baseApk = runner.getCompletedStep().workingCopy val libsApk = runner.getCompletedStep().workingCopy val langApk = runner.getCompletedStep().workingCopy val resApk = runner.getCompletedStep().workingCopy runner.logger.i("Creating dir for signed apks: ${signedDir.absolutePath}") signedDir.mkdirs() val apks = listOf(baseApk, libsApk, langApk, resApk) // Align resources.arsc due to targeting api 30 for silent install if(Build.VERSION.SDK_INT >= 30) { for (file in apks) { runner.logger.i("Byte aligning ${file.name}") val bytes = ZipReader(file).use { if (it.entryNames.contains("resources.arsc")) { it.openEntry("resources.arsc")?.read() } else { null } } ?: continue ZipWriter(file, true).use { runner.logger.i("Removing old resources.arsc") it.deleteEntry("resources.arsc", true) runner.logger.i("Adding aligned resources.arsc") it.writeEntry("resources.arsc", bytes, ZipCompression.NONE, 4096) } } } apks.forEach { runner.logger.i("Signing ${it.name}") Signer.signApk(it, File(signedDir, it.name)) } } } ================================================ FILE: app/src/main/java/dev/beefers/vendetta/manager/installer/step/patching/ReplaceIconStep.kt ================================================ package dev.beefers.vendetta.manager.installer.step.patching import android.content.Context import androidx.compose.ui.graphics.Color import com.github.diamondminer88.zip.ZipWriter import dev.beefers.vendetta.manager.R import dev.beefers.vendetta.manager.domain.manager.PreferenceManager import dev.beefers.vendetta.manager.installer.step.Step import dev.beefers.vendetta.manager.installer.step.StepGroup import dev.beefers.vendetta.manager.installer.step.StepRunner import dev.beefers.vendetta.manager.installer.step.download.DownloadBaseStep import dev.beefers.vendetta.manager.installer.util.ArscUtil import dev.beefers.vendetta.manager.installer.util.ArscUtil.addColorResource import dev.beefers.vendetta.manager.installer.util.ArscUtil.getMainArscChunk import dev.beefers.vendetta.manager.installer.util.ArscUtil.getPackageChunk import dev.beefers.vendetta.manager.installer.util.ArscUtil.getResourceFileName import dev.beefers.vendetta.manager.installer.util.AxmlUtil import dev.beefers.vendetta.manager.utils.DiscordVersion import org.koin.core.component.inject /** * Replaces the existing app icons with Vendetta tinted ones */ class ReplaceIconStep : Step() { private val preferences: PreferenceManager by inject() val context: Context by inject() override val group = StepGroup.PATCHING override val nameRes = R.string.step_change_icon override suspend fun run(runner: StepRunner) { val baseApk = runner.getCompletedStep().workingCopy runner.logger.i("Reading resources.arsc") val arsc = ArscUtil.readArsc(baseApk) val iconRscIds = AxmlUtil.readManifestIconInfo(baseApk) val squareIconFile = arsc.getMainArscChunk().getResourceFileName(iconRscIds.squareIcon, "anydpi-v26") val roundIconFile = arsc.getMainArscChunk().getResourceFileName(iconRscIds.roundIcon, "anydpi-v26") runner.logger.i("Patching icon assets (squareIcon=$squareIconFile, roundIcon=$roundIconFile)") val backgroundColor = arsc.getPackageChunk().addColorResource("bunny_color", Color(0xFF48488B)) val postfix = when (preferences.channel) { DiscordVersion.Type.BETA -> "beta" DiscordVersion.Type.ALPHA -> "canary" else -> null } for (rscFile in setOf(squareIconFile, roundIconFile)) { // setOf to not possibly patch same file twice val referencePath = if (postfix == null) rscFile else { rscFile.replace("_$postfix.xml", ".xml") } runner.logger.i("Patching adaptive icon ($rscFile <- $referencePath)") AxmlUtil.patchAdaptiveIcon( apk = baseApk, resourcePath = rscFile, referencePath = referencePath, backgroundColor = backgroundColor, ) } runner.logger.i("Writing and compiling resources.arsc") ZipWriter(baseApk, /* append = */ true).use { it.deleteEntry("resources.arsc") it.writeEntry("resources.arsc", arsc.toByteArray()) } } } ================================================ FILE: app/src/main/java/dev/beefers/vendetta/manager/installer/util/ArscUtil.kt ================================================ // https://github.com/Aliucord/Manager/blob/main/app/src/main/kotlin/com/aliucord/manager/installer/util/ArscUtil.kt package dev.beefers.vendetta.manager.installer.util import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.toArgb import com.github.diamondminer88.zip.ZipReader import com.google.devrel.gmscore.tools.apk.arsc.* import dev.beefers.vendetta.manager.BuildConfig import java.io.File object ArscUtil { /** * Read and parse `resources.arsc` from an APK. */ fun readArsc(apk: File): BinaryResourceFile { val bytes = ZipReader(apk).use { it.openEntry("resources.arsc")?.read() } ?: error("APK missing resources.arsc") return try { BinaryResourceFile(bytes) } catch (t: Throwable) { throw Error("Failed to parse resources.arsc", t) } } /** * Get the only top-level chunk in an arsc file. */ fun BinaryResourceFile.getMainArscChunk(): ResourceTableChunk { if (this.chunks.size > 1) error("More than 1 top level chunk in resources.arsc") return this.chunks.first() as? ResourceTableChunk ?: error("Invalid top-level resources.arsc chunk") } /** * Get a singular package chunk in an arsc file. */ fun BinaryResourceFile.getPackageChunk(): PackageChunk { return this.getMainArscChunk().packages.singleOrNull() ?: error("resources.arsc must contain exactly 1 package chunk") } /** * Adds a new color resource to all configuration variants in an arsc package. * * @param name The new resource name. * @param color The value of the new color resource. * @return The resource ID of the newly added resource. */ fun PackageChunk.addColorResource( name: String, color: Color, ): BinaryResourceIdentifier { return this.addResource( typeName = "color", resourceName = name, configurations = { true }, valueType = BinaryResourceValue.Type.INT_COLOR_ARGB8, valueData = color.toArgb(), ) } /** * Adds a new color resource to the matching configuration variants in an arsc package. * * @param typeName The type of the resource (ex: `mipmap`, `drawable`, etc.) * @param resourceName The new resource name. * @param configurations A predicate whether to add the value into a matching type chunk. * @param valueType The type of the resource value. * @param valueData The raw data of the resource value. * @return The resource ID of the newly added resource. */ fun PackageChunk.addResource( typeName: String, resourceName: String, configurations: (BinaryResourceConfiguration) -> Boolean, valueType: BinaryResourceValue.Type, valueData: Int, ): BinaryResourceIdentifier { // Add a new resource entry to the "type spec chunk" and, // a new resource entry to all matching "type chunks" val specChunk = this.getTypeSpecChunk(typeName) val typeChunks = this.getTypeChunks(typeName) // Add a new string to the pool to be used as a key val resourceNameIdx = this.keyStringPool.addString(resourceName, /* deduplicate = */ true) // Add a new resource entry to the type spec chunk // HACK: Resource index returned by addResource is off by 1 in release builds due to optimizations or something val resourceIdx = specChunk.addResource(/* flags = */ 0) + if (BuildConfig.DEBUG) 0 else 1 for (typeChunk in typeChunks) { // If no matching config, add a null entry and try next chunk if (!configurations(typeChunk.configuration)) { typeChunk.addEntry(null) continue } val entry = TypeChunk.Entry( /* headerSize = */ 8, /* flags = */ 0, /* keyIndex = */ resourceNameIdx, /* value = */ BinaryResourceValue( /* type = */ valueType, /* data = */ valueData, ), /* values = */ null, // not a complex resource /* parentEntry = */ 0, // not a complex resource /* parent = */ typeChunk, ) typeChunk.addEntry(entry) } return BinaryResourceIdentifier.create( /* packageId = */ this.id, /* typeId = */ specChunk.id, /* entryId = */ resourceIdx, ) } /** * In an arsc file, for a specific resource in a configuration, get it's value. * * @param resourceId The target resource id. * @param configurationName The target configuration variant of the resource. (ex: `anydpi-v26`, `xxhdpi`, `ldtrl-mpi`, etc.) * @return The string value of the resource, which should be a file path inside the apk. */ fun ResourceTableChunk.getResourceFileName( resourceId: BinaryResourceIdentifier, configurationName: String, ): String { val packageChunk = this.packages.find { it.id == resourceId.packageId() } ?: error("Unable to find target resource") val typeChunk = packageChunk.getTypeChunks(resourceId.typeId()) .find { it.configuration.toString() == configurationName } ?: error("Unable to find target resource") val entry = try { typeChunk.getEntry(resourceId.entryId())!! } catch (_: Throwable) { error("Unable to find target resource") } if (entry.isComplex || entry.value().type() != BinaryResourceValue.Type.STRING) error("Target resource value type is not STRING") val valueIdx = entry.value().data() val value = this.stringPool.getString(valueIdx) return value } } ================================================ FILE: app/src/main/java/dev/beefers/vendetta/manager/installer/util/AxmlUtil.kt ================================================ // https://github.com/Aliucord/Manager/blob/main/app/src/main/kotlin/com/aliucord/manager/installer/util/AxmlUtil.kt package dev.beefers.vendetta.manager.installer.util import dev.beefers.vendetta.manager.utils.find import com.github.diamondminer88.zip.ZipReader import com.github.diamondminer88.zip.ZipWriter import com.google.devrel.gmscore.tools.apk.arsc.* import java.io.File object AxmlUtil { /** * Read and parse a specific axml resource inside an APK * @param apk The source apk * @param resourcePath The full path to the axml file inside the apk, which may be flattened. */ private fun readAxml(apk: File, resourcePath: String): BinaryResourceFile { val bytes = ZipReader(apk).use { it.openEntry(resourcePath)?.read() } ?: error("APK missing resource file at $resourcePath") return try { BinaryResourceFile(bytes) } catch (t: Throwable) { throw Error("Failed to parse axml at $resourcePath", t) } } /** * Get the only top-level chunk in an axml file. */ private fun BinaryResourceFile.getMainAxmlChunk(): XmlChunk { if (this.chunks.size > 1) error("More than 1 top level chunk in axml") return this.chunks.first() as? XmlChunk ?: error("Invalid top-level axml chunk") } /** * Finds the first chunk with a matching [name] in a flattened chunk list. * @receiver The top level XmlChunk ([getMainAxmlChunk]) */ private fun XmlChunk.getStartElementChunk(name: String): XmlStartElementChunk? { val nameIdx = this.stringPool.indexOf(name) return this.chunks .find { it is XmlStartElementChunk && it.nameIndex == nameIdx } as? XmlStartElementChunk } /** * Finds the first attribute with a matching name (ignoring namespace) * in a starting element chunk. */ private fun XmlStartElementChunk.getAttribute(name: String): XmlAttribute { val nameIdx = (this.parent as XmlChunk).stringPool.indexOf(name) return this.attributes .find { it.nameIndex() == nameIdx } ?: error("Failed to find $name attribute in an axml chunk") } /** * Patches an axml file to change the `background`, `foreground`, and `monochrome` resource references. * If any of the following are not null, then they will be patched. * @param backgroundColor A color resource id to replace with. * * @param foregroundIcon A drawable resource id to replace with. * @param monochromeIcon A drawable resource id to add or replace with. */ fun patchAdaptiveIcon( apk: File, resourcePath: String, referencePath: String, backgroundColor: BinaryResourceIdentifier? = null, foregroundIcon: BinaryResourceIdentifier? = null, monochromeIcon: BinaryResourceIdentifier? = null, ) { val xml = readAxml(apk, referencePath) val xmlChunk = xml.getMainAxmlChunk() // Patch the background color resource reference if (backgroundColor != null) { val chunk = xmlChunk.getStartElementChunk("background")!! val attribute = chunk.getAttribute("drawable") attribute.typedValue().setValue( /* type = */ BinaryResourceValue.Type.REFERENCE, /* data = */ backgroundColor.resourceId(), ) } // Patch the foreground drawable reference if (foregroundIcon != null) { val chunk = xmlChunk.getStartElementChunk("foreground")!! val attribute = chunk.getAttribute("drawable") attribute.typedValue().setValue( /* type = */ BinaryResourceValue.Type.REFERENCE, /* data = */ foregroundIcon.resourceId(), ) } // Add or replace the monochrome drawable reference if (monochromeIcon != null) { // already exists, patch existing chunk val existingChunk = xmlChunk.getStartElementChunk("monochrome") if (existingChunk != null) { val attribute = existingChunk.getAttribute("drawable") attribute.typedValue().setValue( /* type = */ BinaryResourceValue.Type.REFERENCE, /* data = */ monochromeIcon.resourceId(), ) } // Add a new start & end chunk since they don't exist // ` else { val iconEndChunkIdx = xmlChunk.chunks .indexOfLast { it is XmlEndElementChunk && it.name == "adaptive-icon" } val namespaceIdx = xmlChunk.stringPool.indexOf("http://schemas.android.com/apk/res/android") val drawableIdx = xmlChunk.stringPool.indexOf("drawable") val monochromeIdx = xmlChunk.stringPool.addString("monochrome") val startChunk = XmlStartElementChunk( /* namespaceIndex = */ -1, /* nameIndex = */ monochromeIdx, /* idIndex = */ -1, /* classIndex = */ -1, /* styleIndex = */ -1, /* attributes = */ listOf( XmlAttribute( /* namespaceIndex = */ namespaceIdx, /* nameIndex = */ drawableIdx, /* rawValueIndex = */ -1, /* typedValue = */ BinaryResourceValue( /* type = */ BinaryResourceValue.Type.REFERENCE, /* data = */ monochromeIcon.resourceId(), ), // This is wrong but it doesn't matter here as long as this attribute isn't stringified /* parent = */ null, ) ), /* parent = */ xmlChunk, ) val endChunk = XmlEndElementChunk( /* namespaceIndex = */ namespaceIdx, /* nameIndex = */ monochromeIdx, /* parent = */ xmlChunk, ) xmlChunk.addChunk(iconEndChunkIdx, startChunk) xmlChunk.addChunk(iconEndChunkIdx + 1, endChunk) } } ZipWriter(apk, /* append = */ true).use { zip -> zip.deleteEntry(resourcePath) zip.writeEntry(resourcePath, xml.toByteArray()) } } /** * From an APK, read the manifest's `icon` and `roundIcon` references to a resource. * This is then used to get the filename of the resource from `resources.arsc`. */ fun readManifestIconInfo(apk: File): ManifestIconInfo { val manifestBytes = ZipReader(apk).use { it.openEntry("AndroidManifest.xml")?.read() } ?: error("APK missing manifest") val manifest = BinaryResourceFile(manifestBytes) val mainChunk = manifest.getMainAxmlChunk() // Prefetch string indexes to avoid parsing the entire string pool val iconStringIdx = mainChunk.stringPool.indexOf("icon") val roundIconStringIdx = mainChunk.stringPool.indexOf("roundIcon") val applicationStringIdx = mainChunk.stringPool.indexOf("application") val applicationChunk = mainChunk.chunks .find { it is XmlStartElementChunk && it.nameIndex == applicationStringIdx } as? XmlStartElementChunk ?: error("Unable to find in manifest") val squareIcon = applicationChunk.attributes .find { it.nameIndex() == iconStringIdx } ?: error("Unable to find android:icon in manifest") val roundIcon = applicationChunk.attributes .find { it.nameIndex() == roundIconStringIdx } ?: error("Unable to find android:roundIcon in manifest") assert(squareIcon.typedValue().type() == BinaryResourceValue.Type.REFERENCE) assert(roundIcon.typedValue().type() == BinaryResourceValue.Type.REFERENCE) return ManifestIconInfo( // Resource IDs into resources.arsc squareIcon = BinaryResourceIdentifier.create(squareIcon.typedValue().data()), roundIcon = BinaryResourceIdentifier.create(roundIcon.typedValue().data()), ) } data class ManifestIconInfo( val squareIcon: BinaryResourceIdentifier, val roundIcon: BinaryResourceIdentifier, ) } ================================================ FILE: app/src/main/java/dev/beefers/vendetta/manager/installer/util/LogEntry.kt ================================================ package dev.beefers.vendetta.manager.installer.util import android.annotation.SuppressLint import android.os.Parcel import android.os.Parcelable import androidx.compose.runtime.Stable import kotlinx.datetime.Clock import java.io.Serializable import java.text.SimpleDateFormat import java.util.Date @Stable data class LogEntry( val message: String, val level: Level, private val timestampMillis: Long = Clock.System.now().toEpochMilliseconds() ): Parcelable, Serializable { override fun describeContents() = 0 override fun writeToParcel(dest: Parcel, flags: Int) { dest.writeString(message) dest.writeInt(level.ordinal) dest.writeLong(timestampMillis) } override fun toString(): String { return "${formatTimestamp()} [${level.name[0]}] $message" } @SuppressLint("SimpleDateFormat") fun formatTimestamp(): String { return SimpleDateFormat("MM-dd-yyyy h:mm:ssa").format(Date(timestampMillis)) } enum class Level { DEBUG, INFO, ERROR } companion object CREATOR: Parcelable.Creator { override fun createFromParcel(source: Parcel): LogEntry { val message = source.readString()!! val level = Level.entries[source.readInt()] val timestamp = source.readLong() return LogEntry(message, level, timestamp) } override fun newArray(size: Int): Array { return arrayOfNulls(size) } } } ================================================ FILE: app/src/main/java/dev/beefers/vendetta/manager/installer/util/Logger.kt ================================================ package dev.beefers.vendetta.manager.installer.util import android.util.Log import androidx.compose.runtime.Stable import org.lsposed.patch.util.Logger /** * Used to log events done during the patching process * * @param tag The tag to use for logcat */ @Stable class Logger( private val tag: String ): Logger() { /** * All logs made with this [Logger] */ val logs = mutableListOf() /** * Prints a debug message to logcat and stores it in [logs] */ override fun d(msg: String?) { log(msg, LogEntry.Level.DEBUG) Log.d(tag, msg.toString()) } /** * Prints an info message to logcat and stores it in [logs] */ override fun i(msg: String?) { log(msg, LogEntry.Level.INFO) Log.i(tag, msg.toString()) } /** * Prints an error message to logcat and stores it in [logs] */ override fun e(msg: String?) { log(msg, LogEntry.Level.ERROR) Log.e(tag, msg.toString()) } /** * Prints an error message and stacktrace with a preceding empty line. */ fun e(msg: String?, th: Throwable?) { newline() e(msg) if (th != null) e(th.stackTraceToString().trim()) } /** * Stores a log entry */ private fun log(msg: String?, level: LogEntry.Level) { msg?.let { msg.split("\n").forEach { logs += LogEntry(it, level) } } } /** * Prints an empty line */ private fun newline() { i("\n") } } ================================================ FILE: app/src/main/java/dev/beefers/vendetta/manager/installer/util/ManifestPatcher.kt ================================================ package dev.beefers.vendetta.manager.installer.util import android.Manifest import pxb.android.axml.AxmlReader import pxb.android.axml.AxmlVisitor import pxb.android.axml.AxmlWriter import pxb.android.axml.NodeVisitor object ManifestPatcher { private const val ANDROID_NAMESPACE = "http://schemas.android.com/apk/res/android" private const val USES_CLEARTEXT_TRAFFIC = "usesCleartextTraffic" private const val DEBUGGABLE = "debuggable" private const val REQUEST_LEGACY_EXTERNAL_STORAGE = "requestLegacyExternalStorage" private const val NETWORK_SECURITY_CONFIG = "networkSecurityConfig" private const val LABEL = "label" private const val PACKAGE = "package" private const val COMPILE_SDK_VERSION = "compileSdkVersion" private const val COMPILE_SDK_VERSION_CODENAME = "compileSdkVersionCodename" private const val DRNE_PERMISSION = "DYNAMIC_RECEIVER_NOT_EXPORTED_PERMISSION" private const val DISCORD_DRNE_PERMISSION = "com.discord.$DRNE_PERMISSION" fun patchManifest( manifestBytes: ByteArray, packageName: String, appName: String, debuggable: Boolean ): ByteArray { val reader = AxmlReader(manifestBytes) val writer = AxmlWriter() reader.accept(object : AxmlVisitor(writer) { override fun child(ns: String?, name: String?) = object : ReplaceAttrsVisitor( super.child(ns, name), mapOf( PACKAGE to packageName, COMPILE_SDK_VERSION to 23, COMPILE_SDK_VERSION_CODENAME to "6.0-2438415" ) ) { private var addExternalStoragePerm = false override fun child(ns: String?, name: String): NodeVisitor { val nv = super.child(ns, name) // Add MANAGE_EXTERNAL_STORAGE when necessary if (addExternalStoragePerm) { super .child(null, "uses-permission") .attr( ANDROID_NAMESPACE, "name", android.R.attr.name, TYPE_STRING, Manifest.permission.MANAGE_EXTERNAL_STORAGE ) addExternalStoragePerm = false } return when (name) { "permission" -> object : NodeVisitor(nv) { override fun attr( ns: String?, name: String?, resourceId: Int, type: Int, value: Any? ) { if(name == "name" && value == DISCORD_DRNE_PERMISSION) { super.attr(ns, name, resourceId, type, "$packageName.$DRNE_PERMISSION") } else { super.attr(ns, name, resourceId, type, value) } } } "uses-permission" -> object : NodeVisitor(nv) { override fun attr( ns: String?, name: String?, resourceId: Int, type: Int, value: Any? ) { if(name == "name" && value == DISCORD_DRNE_PERMISSION) { super.attr(ns, name, resourceId, type, "$packageName.$DRNE_PERMISSION") } if (name != "maxSdkVersion") { super.attr(ns, name, resourceId, type, value) } // Set the add external storage permission to be added after WRITE_EXTERNAL_STORAGE (which is after read) if (name == "name" && value == Manifest.permission.READ_EXTERNAL_STORAGE) { addExternalStoragePerm = true } } } "application" -> object : ReplaceAttrsVisitor( nv, mapOf( LABEL to appName, DEBUGGABLE to debuggable, USES_CLEARTEXT_TRAFFIC to true, REQUEST_LEGACY_EXTERNAL_STORAGE to true ) ) { private var addDebuggable = debuggable private var addLegacyStorage = true private var addUseClearTextTraffic = true override fun attr( ns: String?, name: String, resourceId: Int, type: Int, value: Any? ) { super.attr(ns, name, resourceId, type, value) if (name == REQUEST_LEGACY_EXTERNAL_STORAGE) addLegacyStorage = false if (name == DEBUGGABLE) addDebuggable = false if (name == USES_CLEARTEXT_TRAFFIC) addUseClearTextTraffic = false } override fun child(ns: String?, name: String): NodeVisitor { val visitor = super.child(ns, name) return when (name) { "activity" -> ReplaceAttrsVisitor( visitor, mapOf("label" to appName) ) "provider" -> object : NodeVisitor(visitor) { override fun attr( ns: String?, name: String, resourceId: Int, type: Int, value: Any? ) { super.attr( ns, name, resourceId, type, if (name == "authorities") { (value as String).replace( "com.discord", packageName ) } else { value } ) } } else -> visitor } } override fun end() { if (addLegacyStorage) super.attr( ANDROID_NAMESPACE, REQUEST_LEGACY_EXTERNAL_STORAGE, -1, TYPE_INT_BOOLEAN, 1 ) if (addDebuggable) super.attr( ANDROID_NAMESPACE, DEBUGGABLE, -1, TYPE_INT_BOOLEAN, 1 ) if (addUseClearTextTraffic) super.attr( ANDROID_NAMESPACE, USES_CLEARTEXT_TRAFFIC, -1, TYPE_INT_BOOLEAN, 1 ) super.end() } } else -> nv } } } }) return writer.toByteArray() } fun renamePackage( manifestBytes: ByteArray, packageName: String ): ByteArray { val reader = AxmlReader(manifestBytes) val writer = AxmlWriter() reader.accept( object : AxmlVisitor(writer) { override fun child(ns: String?, name: String?): ReplaceAttrsVisitor { return ReplaceAttrsVisitor( super.child(ns, name), mapOf("package" to packageName) ) } } ) return writer.toByteArray() } private open class ReplaceAttrsVisitor( nv: NodeVisitor, private val attrs: Map ) : NodeVisitor(nv) { override fun attr(ns: String?, name: String, resourceId: Int, type: Int, value: Any?) { val replace = attrs.containsKey(name) val newValue = attrs[name] super.attr( ns, name, resourceId, if (newValue is String) TYPE_STRING else type, if (replace) newValue else value ) } } } ================================================ FILE: app/src/main/java/dev/beefers/vendetta/manager/installer/util/Patcher.kt ================================================ package dev.beefers.vendetta.manager.installer.util import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import org.lsposed.patch.LSPatch import org.lsposed.patch.util.Logger import java.io.File object Patcher { suspend fun patch( logger: Logger, outputDir: File, apkPaths: List, embeddedModules: List ) { withContext(Dispatchers.IO) { LSPatch( logger, *apkPaths.toTypedArray(), "-o", outputDir.absolutePath, "-l", "0", "-v", "-m", *embeddedModules.toTypedArray(), "-k", Signer.keyStore.absolutePath, "password", "alias", "password" ).doCommandLine() } } } ================================================ FILE: app/src/main/java/dev/beefers/vendetta/manager/installer/util/Signer.kt ================================================ package dev.beefers.vendetta.manager.installer.util import android.content.Context import com.android.apksig.ApkSigner import dev.beefers.vendetta.manager.utils.Constants 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.operator.jcajce.JcaContentSignerBuilder import org.koin.core.component.KoinComponent import org.koin.core.component.inject import java.io.File import java.math.BigInteger import java.security.KeyPairGenerator import java.security.KeyStore import java.security.PrivateKey import java.security.SecureRandom import java.security.cert.Certificate import java.security.cert.X509Certificate import java.util.Date import java.util.Locale import kotlin.io.path.Path import kotlin.io.path.moveTo object Signer : KoinComponent { private val password = "password".toCharArray() private val context by inject() private val cacheDir = context.cacheDir private val filesDir = context.filesDir val keyStore: File by lazy { val ks = filesDir.resolve("ks.keystore") migrate(cacheDir, filesDir) migrate(Constants.VENDETTA_DIR, filesDir) ks.also { if (!it.exists()) { it.createNewFile() newKeystore(it) } } ks } private val signerConfig: ApkSigner.SignerConfig by lazy { val keyStore = KeyStore.getInstance(KeyStore.getDefaultType()) this.keyStore.inputStream().use { stream -> keyStore.load(stream, null) } val alias = keyStore.aliases().nextElement() val certificate = keyStore.getCertificate(alias) as X509Certificate ApkSigner.SignerConfig.Builder( "Bunny", keyStore.getKey(alias, password) as PrivateKey, listOf(certificate) ).build() } private fun newKeystore(out: File) { val key = createKey() with(KeyStore.getInstance(KeyStore.getDefaultType())) { load(null, password) setKeyEntry("alias", key.privateKey, password, arrayOf(key.publicKey)) store(out.outputStream(), password) } } private fun createKey(): KeySet { var serialNumber: BigInteger do serialNumber = SecureRandom().nextInt().toBigInteger() while (serialNumber < BigInteger.ZERO) val x500Name = X500Name("CN=Bunny Manager") val pair = KeyPairGenerator.getInstance("RSA").run { initialize(2048) generateKeyPair() } val builder = X509v3CertificateBuilder( /* issuer = */ x500Name, /* serial = */ serialNumber, /* notBefore = */ Date(System.currentTimeMillis() - 1000L * 60L * 60L * 24L * 30L), /* notAfter = */ Date(System.currentTimeMillis() + 1000L * 60L * 60L * 24L * 366L * 30L), /* dateLocale = */ Locale.ENGLISH, /* subject = */ x500Name, /* publicKeyInfo = */ SubjectPublicKeyInfo.getInstance(pair.public.encoded) ) val signer = JcaContentSignerBuilder("SHA1withRSA").build(pair.private) return KeySet( JcaX509CertificateConverter().getCertificate(builder.build(signer)), pair.private ) } fun signApk(apkFile: File, output: File) { val outputApk = cacheDir.resolve(apkFile.name) ApkSigner.Builder(listOf(signerConfig)) .setV1SigningEnabled(false) // TODO: enable so api <24 devices can work, however zip-alignment breaks .setV2SigningEnabled(true) .setV3SigningEnabled(true) .setInputApk(apkFile) .setOutputApk(output) .build() .sign() outputApk.renameTo(apkFile) } private fun migrate(oldDir: File, newDir: File) { oldDir.resolve("ks.keystore").also { if (it.exists()) { Path(it.absolutePath) .moveTo( Path(newDir.resolve("ks.keystore").absolutePath), overwrite = true ) } } } private class KeySet(val publicKey: X509Certificate, val privateKey: PrivateKey) } ================================================ FILE: app/src/main/java/dev/beefers/vendetta/manager/network/dto/Commit.kt ================================================ package dev.beefers.vendetta.manager.network.dto import kotlinx.datetime.Instant import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable @Serializable data class Commit( val sha: String, @SerialName("commit") val info: Info, @SerialName("html_url") val url: String, val author: User? ) { @Serializable data class Info( val message: String, val committer: Committer ) @Serializable data class Committer( val date: Instant ) } ================================================ FILE: app/src/main/java/dev/beefers/vendetta/manager/network/dto/Index.kt ================================================ package dev.beefers.vendetta.manager.network.dto import kotlinx.serialization.Serializable @Serializable data class Index( val latest: Versions ) { @Serializable data class Versions( val alpha: String, val beta: String, val stable: String ) } ================================================ FILE: app/src/main/java/dev/beefers/vendetta/manager/network/dto/Release.kt ================================================ package dev.beefers.vendetta.manager.network.dto import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable @Serializable data class Release( @SerialName("tag_name") val tagName: String, @SerialName("name") val versionName: String ) ================================================ FILE: app/src/main/java/dev/beefers/vendetta/manager/network/dto/User.kt ================================================ package dev.beefers.vendetta.manager.network.dto import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable @Serializable data class User( @SerialName("login") val username: String, @SerialName("avatar_url") val avatar: String ) ================================================ FILE: app/src/main/java/dev/beefers/vendetta/manager/network/service/HttpService.kt ================================================ package dev.beefers.vendetta.manager.network.service import dev.beefers.vendetta.manager.network.utils.ApiError import dev.beefers.vendetta.manager.network.utils.ApiFailure import dev.beefers.vendetta.manager.network.utils.ApiResponse import io.ktor.client.HttpClient import io.ktor.client.request.HttpRequestBuilder import io.ktor.client.request.request import io.ktor.client.statement.bodyAsText import io.ktor.http.isSuccess import kotlinx.serialization.json.Json class HttpService( val json: Json, val http: HttpClient ) { suspend inline fun request(builder: HttpRequestBuilder.() -> Unit = {}): ApiResponse { 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(body)) } else { body = try { response.bodyAsText() } catch (e: Throwable) { null } ApiResponse.Error(ApiError(response.status, body)) } } catch (e: Throwable) { ApiResponse.Failure(ApiFailure(e, body)) } return response } } ================================================ FILE: app/src/main/java/dev/beefers/vendetta/manager/network/service/RestService.kt ================================================ package dev.beefers.vendetta.manager.network.service import dev.beefers.vendetta.manager.domain.manager.PreferenceManager import dev.beefers.vendetta.manager.network.dto.Commit import dev.beefers.vendetta.manager.network.dto.Index import dev.beefers.vendetta.manager.network.dto.Release import io.ktor.client.request.parameter import io.ktor.client.request.url import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext class RestService( private val httpService: HttpService, private val prefs: PreferenceManager ) { suspend fun getLatestRelease(repo: String) = withContext(Dispatchers.IO) { httpService.request { url("https://api.github.com/repos/$repo/releases/latest") } } suspend fun getLatestDiscordVersions() = withContext(Dispatchers.IO) { httpService.request { url("${prefs.mirror.baseUrl}/tracker/index") } } suspend fun getCommits(repo: String, page: Int = 1) = withContext(Dispatchers.IO) { httpService.request> { url("https://api.github.com/repos/$repo/commits") parameter("page", page) } } } ================================================ FILE: app/src/main/java/dev/beefers/vendetta/manager/network/utils/ApiResponse.kt ================================================ package dev.beefers.vendetta.manager.network.utils import io.ktor.http.HttpStatusCode sealed interface ApiResponse { data class Success(val data: D) : ApiResponse data class Error(val error: ApiError) : ApiResponse data class Failure(val error: ApiFailure) : ApiResponse } class ApiError(code: HttpStatusCode, body: String?) : Error("HTTP Code $code, Body: $body") class ApiFailure(error: Throwable, body: String?) : Error(body, error) val ApiResponse.dataOrNull get() = if (this is ApiResponse.Success) data else null val ApiResponse.dataOrThrow get() = when (this) { is ApiResponse.Success -> data is ApiResponse.Error -> throw error is ApiResponse.Failure -> throw error } inline fun ApiResponse.ifSuccessful(block: (D) -> Unit) { if (this is ApiResponse.Success) block(data) } fun ApiResponse.fold( onSuccess: (D) -> Unit = {}, onError: () -> Unit = {} ) { when (this) { is ApiResponse.Success -> onSuccess(data) is ApiResponse.Error, is ApiResponse.Failure -> onError() } } @Suppress("UNCHECKED_CAST") fun ApiResponse.transform(block: (T) -> R): ApiResponse { return when (this) { is ApiResponse.Success -> ApiResponse.Success(block(data)) is ApiResponse.Error -> this as ApiResponse.Error is ApiResponse.Failure -> this as ApiResponse.Failure } } ================================================ FILE: app/src/main/java/dev/beefers/vendetta/manager/network/utils/CommitsPagingSource.kt ================================================ package dev.beefers.vendetta.manager.network.utils import androidx.paging.PagingSource import androidx.paging.PagingState import dev.beefers.vendetta.manager.domain.repository.RestRepository import dev.beefers.vendetta.manager.network.dto.Commit class CommitsPagingSource( private val repo: RestRepository ) : PagingSource() { override fun getRefreshKey(state: PagingState): Int? = state.anchorPosition?.let { state.closestPageToPosition(it)?.prevKey } override suspend fun load(params: LoadParams): LoadResult { val page = params.key ?: 0 return when (val response = repo.getCommits("pyoncord/Bunny", page)) { is ApiResponse.Success -> LoadResult.Page( data = response.data, prevKey = if (page > 0) page - 1 else null, nextKey = if (response.data.isNotEmpty()) page + 1 else null ) is ApiResponse.Failure -> LoadResult.Error(response.error) is ApiResponse.Error -> LoadResult.Error(response.error) } } } ================================================ FILE: app/src/main/java/dev/beefers/vendetta/manager/ui/activity/MainActivity.kt ================================================ package dev.beefers.vendetta.manager.ui.activity import android.Manifest import android.content.pm.PackageManager import android.os.Bundle import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.activity.enableEdgeToEdge import androidx.core.app.ActivityCompat import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen import androidx.lifecycle.lifecycleScope import cafe.adriel.voyager.navigator.Navigator import cafe.adriel.voyager.transitions.SlideTransition import dev.beefers.vendetta.manager.domain.manager.InstallMethod import dev.beefers.vendetta.manager.domain.manager.PreferenceManager import dev.beefers.vendetta.manager.installer.shizuku.ShizukuPermissions import dev.beefers.vendetta.manager.ui.screen.home.HomeScreen import dev.beefers.vendetta.manager.ui.screen.installer.InstallerScreen import dev.beefers.vendetta.manager.ui.theme.VendettaManagerTheme import dev.beefers.vendetta.manager.utils.DiscordVersion import dev.beefers.vendetta.manager.utils.Intents import kotlinx.coroutines.launch import org.koin.android.ext.android.inject class MainActivity : ComponentActivity() { private val preferences: PreferenceManager by inject() override fun onCreate(savedInstanceState: Bundle?) { installSplashScreen() enableEdgeToEdge() super.onCreate(savedInstanceState) val version = intent.getStringExtra(Intents.Extras.VERSION) if (checkSelfPermission(Manifest.permission.POST_NOTIFICATIONS) != PackageManager.PERMISSION_GRANTED) { ActivityCompat.requestPermissions( this, arrayOf("android.permission.POST_NOTIFICATIONS"), 0 ) } if (preferences.installMethod == InstallMethod.SHIZUKU) { lifecycleScope.launch { if (!ShizukuPermissions.waitShizukuPermissions()) { preferences.installMethod = InstallMethod.DEFAULT } } } val screen = if (intent.action == Intents.Actions.INSTALL && version != null) { InstallerScreen(DiscordVersion.fromVersionCode(version)!!) } else { HomeScreen() } setContent { VendettaManagerTheme { Navigator(screen) { SlideTransition(it) } } } } } ================================================ FILE: app/src/main/java/dev/beefers/vendetta/manager/ui/components/Label.kt ================================================ package dev.beefers.vendetta.manager.ui.components import androidx.compose.foundation.background import androidx.compose.foundation.border import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.shape.CircleShape import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp @Composable fun Label( modifier: Modifier = Modifier, text: String? = null, icon: ImageVector? = null, textColor: Color, borderColor: Color = textColor, fillColor: Color = Color.Transparent, iconColor: Color = textColor ) { Row( verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(4.dp), modifier = Modifier .then(modifier) .clip(CircleShape) .background(fillColor) .border(1.dp, borderColor, CircleShape) .then( if (text == null) Modifier.padding(5.dp) else Modifier.padding(vertical = 5.dp, horizontal = 7.dp) ) ) { icon?.let { Icon( imageVector = it, contentDescription = null, tint = iconColor, modifier = Modifier.size(12.dp) ) } text?.let { Text( text = text, style = MaterialTheme.typography.labelSmall, fontSize = 10.sp, color = textColor, maxLines = 1 ) } } } ================================================ FILE: app/src/main/java/dev/beefers/vendetta/manager/ui/components/NavBarSpacer.kt ================================================ package dev.beefers.vendetta.manager.ui.components import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.height import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import dev.beefers.vendetta.manager.utils.DimenUtils @Composable fun NavBarSpacer() { Spacer( modifier = Modifier.height(DimenUtils.navBarPadding) ) } ================================================ FILE: app/src/main/java/dev/beefers/vendetta/manager/ui/components/RadioController.kt ================================================ package dev.beefers.vendetta.manager.ui.components import androidx.compose.foundation.clickable 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.material3.RadioButton import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @Composable inline fun > EnumRadioController( default: E, excludedOptions: List = emptyList(), labelFactory: (E) -> String = { it.toString() }, crossinline onChoiceSelected: (E) -> Unit ) { var choice by remember { mutableStateOf(default) } Column { enumValues().filterNot { excludedOptions.contains(it) }.forEach { Row( modifier = Modifier .fillMaxWidth() .clickable { choice = it onChoiceSelected(it) }, verticalAlignment = Alignment.CenterVertically ) { Text(labelFactory(it)) Spacer(Modifier.weight(1f)) RadioButton( selected = it == choice, onClick = { choice = it onChoiceSelected(it) }) } } } } ================================================ FILE: app/src/main/java/dev/beefers/vendetta/manager/ui/components/SegmentedButton.kt ================================================ package dev.beefers.vendetta.manager.ui.components 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.MaterialTheme import androidx.compose.material3.Text import androidx.compose.material3.surfaceColorAtElevation import androidx.compose.runtime.Composable 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 @Composable fun RowScope.SegmentedButton( icon: Any, iconDescription: String? = null, text: String, onClick: () -> Unit ) { Column( horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.spacedBy(12.dp, Alignment.CenterVertically), modifier = Modifier .clickable(onClick = onClick) .background(MaterialTheme.colorScheme.surfaceColorAtElevation(2.dp)) .weight(1f) .padding(16.dp) ) { when (icon) { is ImageVector -> { Icon( imageVector = icon, contentDescription = iconDescription, tint = MaterialTheme.colorScheme.primary ) } is Painter -> { Icon( painter = icon, contentDescription = iconDescription, tint = MaterialTheme.colorScheme.primary ) } } Text( text = text, style = MaterialTheme.typography.labelLarge, color = MaterialTheme.colorScheme.primary, maxLines = 1, modifier = Modifier.basicMarquee() ) } } ================================================ FILE: app/src/main/java/dev/beefers/vendetta/manager/ui/components/ThinDivider.kt ================================================ package dev.beefers.vendetta.manager.ui.components import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.ui.unit.dp @Composable fun ThinDivider() = HorizontalDivider( thickness = 0.5.dp, color = MaterialTheme.colorScheme.onSurface.copy(0.1f) ) ================================================ FILE: app/src/main/java/dev/beefers/vendetta/manager/ui/components/settings/SettingsButton.kt ================================================ package dev.beefers.vendetta.manager.ui.components.settings import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.heightIn import androidx.compose.foundation.layout.padding import androidx.compose.material3.Button import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp @Composable fun SettingsButton( label: String, onClick: () -> Unit = {} ) { Box( modifier = Modifier .heightIn(min = 64.dp) .fillMaxWidth() .padding(horizontal = 18.dp, vertical = 14.dp) ) { Button(onClick, modifier = Modifier.fillMaxWidth()) { Text(text = label) } } } ================================================ FILE: app/src/main/java/dev/beefers/vendetta/manager/ui/components/settings/SettingsCategory.kt ================================================ package dev.beefers.vendetta.manager.ui.components.settings import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Box import androidx.compose.material3.Icon import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.vector.ImageVector import cafe.adriel.voyager.core.screen.Screen import cafe.adriel.voyager.navigator.LocalNavigator import dev.beefers.vendetta.manager.utils.navigate @Composable fun SettingsCategory( icon: ImageVector, text: String, subtext: String, destination: (() -> Screen)? = null ) { val screen = destination?.invoke() val nav = LocalNavigator.current Box( modifier = Modifier .clickable { screen?.let { nav?.navigate(it) } } ) { SettingsItem( icon = { Icon(icon, null) }, text = { Text(text) }, secondaryText = { Text(subtext) } ) } } ================================================ FILE: app/src/main/java/dev/beefers/vendetta/manager/ui/components/settings/SettingsChoiceDialog.kt ================================================ package dev.beefers.vendetta.manager.ui.components.settings import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.slideInVertically import androidx.compose.animation.slideOutVertically import androidx.compose.material3.AlertDialog import androidx.compose.material3.FilledTonalButton import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.res.stringResource import dev.beefers.vendetta.manager.R import dev.beefers.vendetta.manager.ui.components.EnumRadioController @Composable inline fun > SettingsChoiceDialog( visible: Boolean = false, default: E, excludedOptions: List = emptyList(), noinline title: @Composable () -> Unit, crossinline labelFactory: (E) -> String = { it.toString() }, noinline onRequestClose: () -> Unit = {}, crossinline description: @Composable () -> Unit = {}, noinline onChoice: (E) -> Unit = {}, ) { var choice by remember { mutableStateOf(default) } AnimatedVisibility( visible = visible, enter = slideInVertically(), exit = slideOutVertically() ) { AlertDialog( onDismissRequest = { onRequestClose() }, title = title, text = { description() EnumRadioController( default, excludedOptions, labelFactory ) { choice = it } }, confirmButton = { FilledTonalButton(onClick = { onChoice(choice) }) { Text(text = stringResource(id = R.string.action_confirm)) } } ) } } ================================================ FILE: app/src/main/java/dev/beefers/vendetta/manager/ui/components/settings/SettingsHeader.kt ================================================ package dev.beefers.vendetta.manager.ui.components.settings 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.unit.dp @Composable fun SettingsHeader( text: String ) { Text( text = text, style = MaterialTheme.typography.titleSmall, modifier = Modifier.padding(18.dp, 24.dp, 18.dp, 10.dp) ) } ================================================ FILE: app/src/main/java/dev/beefers/vendetta/manager/ui/components/settings/SettingsItem.kt ================================================ package dev.beefers.vendetta.manager.ui.components.settings 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.fillMaxWidth import androidx.compose.foundation.layout.heightIn import androidx.compose.foundation.layout.padding import androidx.compose.material3.MaterialTheme import androidx.compose.material3.ProvideTextStyle import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp @Composable fun SettingsItem( modifier: Modifier = Modifier, icon: @Composable (() -> Unit)? = null, text: @Composable () -> Unit, secondaryText: @Composable (() -> Unit) = { }, trailing: @Composable (() -> Unit) = { }, ) { Row( modifier = modifier .heightIn(min = 64.dp) .fillMaxWidth() .padding(horizontal = 18.dp, vertical = 14.dp), horizontalArrangement = Arrangement.spacedBy(14.dp), verticalAlignment = Alignment.CenterVertically ) { if (icon != null) Box(modifier = Modifier.padding(8.dp)) { icon() } Column( verticalArrangement = Arrangement.spacedBy(5.dp), modifier = Modifier.weight(1f, true) ) { ProvideTextStyle( MaterialTheme.typography.titleLarge.copy( fontWeight = FontWeight.Normal, fontSize = 19.sp ) ) { text() } ProvideTextStyle( MaterialTheme.typography.bodyMedium.copy( color = MaterialTheme.colorScheme.onSurface.copy(0.6f) ) ) { secondaryText() } } trailing() } } ================================================ FILE: app/src/main/java/dev/beefers/vendetta/manager/ui/components/settings/SettingsItemChoice.kt ================================================ package dev.beefers.vendetta.manager.ui.components.settings import androidx.compose.foundation.clickable import androidx.compose.material3.FilledTonalButton import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext @Composable inline fun > SettingsItemChoice( label: String, title: String = label, disabled: Boolean = false, pref: E, excludedOptions: List = emptyList(), crossinline labelFactory: (E) -> String = { it.toString() }, crossinline onPrefChange: (E) -> Unit, ) { val ctx = LocalContext.current val choiceLabel = labelFactory(pref) val opened = remember { mutableStateOf(false) } SettingsItem( modifier = Modifier.clickable(enabled = !disabled) { opened.value = true }, text = { Text(text = label) }, ) { SettingsChoiceDialog( visible = opened.value, title = { Text(title) }, default = pref, labelFactory = labelFactory, excludedOptions = excludedOptions, onRequestClose = { opened.value = false }, onChoice = { opened.value = false onPrefChange(it) } ) FilledTonalButton(onClick = { opened.value = true }, enabled = !disabled) { Text(choiceLabel) } } } ================================================ FILE: app/src/main/java/dev/beefers/vendetta/manager/ui/components/settings/SettingsSwitch.kt ================================================ package dev.beefers.vendetta.manager.ui.components.settings import androidx.compose.foundation.clickable import androidx.compose.material3.Switch import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier @Composable fun SettingsSwitch( label: String, secondaryLabel: String? = null, disabled: Boolean = false, pref: Boolean, onPrefChange: (Boolean) -> Unit, ) { SettingsItem( modifier = Modifier.clickable(enabled = !disabled) { onPrefChange(!pref) }, text = { Text(text = label) }, secondaryText = { secondaryLabel?.let { Text(text = it) } } ) { Switch( checked = pref, enabled = !disabled, onCheckedChange = { onPrefChange(!pref) } ) } } ================================================ FILE: app/src/main/java/dev/beefers/vendetta/manager/ui/components/settings/SettingsTextField.kt ================================================ package dev.beefers.vendetta.manager.ui.components.settings import androidx.compose.foundation.layout.* import androidx.compose.material3.* import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp @Composable fun SettingsTextField( label: String, disabled: Boolean = false, pref: String, error: Boolean = false, supportingText: String? = null, onPrefChange: (String) -> Unit, ) { Box(modifier = Modifier.padding(horizontal = 18.dp, vertical = 10.dp)) { OutlinedTextField( modifier = Modifier.fillMaxWidth(), value = pref, onValueChange = onPrefChange, enabled = !disabled, label = { Text(label) }, isError = error, singleLine = true, supportingText = if (supportingText != null) { -> Text(supportingText) } else null ) } } ================================================ FILE: app/src/main/java/dev/beefers/vendetta/manager/ui/screen/about/AboutScreen.kt ================================================ package dev.beefers.vendetta.manager.ui.screen.about import androidx.compose.foundation.Image 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.WindowInsets import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.ArrowBack import androidx.compose.material3.ElevatedCard import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.LocalContentColor 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.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.asImageBitmap import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalUriHandler import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import cafe.adriel.voyager.core.screen.Screen import cafe.adriel.voyager.navigator.LocalNavigator import cafe.adriel.voyager.navigator.currentOrThrow import dev.beefers.vendetta.manager.BuildConfig import dev.beefers.vendetta.manager.R import dev.beefers.vendetta.manager.domain.manager.PreferenceManager import dev.beefers.vendetta.manager.ui.screen.libraries.LibrariesScreen import dev.beefers.vendetta.manager.ui.widgets.about.LinkItem import dev.beefers.vendetta.manager.ui.widgets.about.ListItem import dev.beefers.vendetta.manager.ui.widgets.about.UserEntry import dev.beefers.vendetta.manager.utils.Constants import dev.beefers.vendetta.manager.utils.DimenUtils import dev.beefers.vendetta.manager.utils.getBitmap import dev.beefers.vendetta.manager.utils.showToast import org.koin.compose.koinInject class AboutScreen : Screen { @Composable @OptIn(ExperimentalMaterial3Api::class) override fun Content() { val uriHandler = LocalUriHandler.current val prefs: PreferenceManager = koinInject() val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior() val ctx = LocalContext.current val bitmap = remember { ctx.getBitmap(R.drawable.ic_launcher, 60).asImageBitmap() } var tapCount by remember { mutableIntStateOf(0) } Scaffold( topBar = { TitleBar(scrollBehavior) }, contentWindowInsets = WindowInsets(0, 0, 0, 0), modifier = Modifier .nestedScroll(scrollBehavior.nestedScrollConnection) ) { Column( modifier = Modifier .padding(it) .verticalScroll(rememberScrollState()) .padding(bottom = DimenUtils.navBarPadding) ) { Column( verticalArrangement = Arrangement.spacedBy(16.dp), horizontalAlignment = Alignment.CenterHorizontally, modifier = Modifier .fillMaxWidth() .padding(top = 16.dp, bottom = 20.dp) ) { Image( bitmap = bitmap, contentDescription = null, modifier = Modifier .size(60.dp) .clip(CircleShape) ) Text( stringResource(R.string.app_name), style = MaterialTheme.typography.titleLarge ) Text( text = "v${BuildConfig.VERSION_NAME} (${BuildConfig.VERSION_CODE})", style = MaterialTheme.typography.labelLarge, color = LocalContentColor.current.copy(alpha = 0.5f), modifier = Modifier.clickable( enabled = !prefs.isDeveloper ) { tapCount++ when (tapCount) { 3 -> ctx.showToast(R.string.msg_seven_left) 5 -> ctx.showToast(R.string.msg_five_left) 8 -> ctx.showToast(R.string.msg_two_left) 10 -> { ctx.showToast(R.string.msg_unlocked) prefs.isDeveloper = true } } } ) Row( verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.SpaceEvenly, modifier = Modifier.fillMaxWidth() ) { LinkItem( icon = R.drawable.ic_github, label = R.string.label_github, link = "https://github.com/bunny-mod" ) LinkItem( icon = R.drawable.ic_discord, label = R.string.label_discord, link = "https://discord.gg/XjYgWXHb9Q" ) } } Row( verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.SpaceEvenly, modifier = Modifier .fillMaxWidth() .padding(top = 16.dp, bottom = 20.dp) ) { UserEntry("Fiery", "Lead dev\niOS", "FieryFlames") UserEntry("Maisy", "Creator\nVendetta", "maisymoe", isLarge = true) UserEntry("Wing", "Lead dev\nManager", "wingio") } Text( text = stringResource(R.string.label_team), style = MaterialTheme.typography.labelLarge, modifier = Modifier.padding(16.dp), color = MaterialTheme.colorScheme.primary ) Box( modifier = Modifier.padding(horizontal = 16.dp) ) { ElevatedCard { Constants.TEAM_MEMBERS.forEachIndexed { i, member -> ListItem( text = member.name, subtext = member.role, imageUrl = "https://github.com/${member.username}.png", onClick = { uriHandler.openUri("https://github.com/${member.username}") } ) if (i != Constants.TEAM_MEMBERS.lastIndex) { HorizontalDivider( modifier = Modifier.padding(horizontal = 16.dp), thickness = 0.5.dp, color = MaterialTheme.colorScheme.outline.copy(alpha = 0.3f) ) } } } } Text( text = stringResource(R.string.label_special_thanks), style = MaterialTheme.typography.labelLarge, modifier = Modifier.padding(16.dp), color = MaterialTheme.colorScheme.primary ) Box( modifier = Modifier.padding(horizontal = 16.dp) ) { ElevatedCard { ListItem( text = "rushii", subtext = "Installer, zip library, and a portion of patching", imageUrl = "https://github.com/rushiiMachine.png", onClick = { uriHandler.openUri("https://github.com/rushiiMachine") } ) HorizontalDivider( modifier = Modifier.padding(horizontal = 16.dp), thickness = 0.5.dp, color = MaterialTheme.colorScheme.outline.copy(alpha = 0.3f) ) ListItem( text = "Xinto", subtext = "for the preference manager", imageUrl = "https://github.com/X1nto.png", onClick = { uriHandler.openUri("https://github.com/X1nto") } ) } } Box( modifier = Modifier.padding(16.dp) ) { ElevatedCard { val navigator = LocalNavigator.currentOrThrow // ListItem( // text = stringResource(R.string.label_translate), // onClick = { uriHandler.openUri("https://crowdin.com/project/vendetta-manager") } // ) // HorizontalDivider( // modifier = Modifier.padding(horizontal = 16.dp), // thickness = 0.5.dp, // color = MaterialTheme.colorScheme.outline.copy(alpha = 0.3f) // ) ListItem( text = stringResource(R.string.title_os_libraries), onClick = { navigator.push(LibrariesScreen()) } ) } } } } } @Composable @OptIn(ExperimentalMaterial3Api::class) private fun TitleBar( scrollBehavior: TopAppBarScrollBehavior ) { val navigator = LocalNavigator.currentOrThrow TopAppBar( title = { Text(stringResource(R.string.title_about)) }, scrollBehavior = scrollBehavior, navigationIcon = { IconButton(onClick = { navigator.pop() }) { Icon( imageVector = Icons.AutoMirrored.Filled.ArrowBack, contentDescription = stringResource(R.string.action_back) ) } } ) } } ================================================ FILE: app/src/main/java/dev/beefers/vendetta/manager/ui/screen/home/HomeScreen.kt ================================================ package dev.beefers.vendetta.manager.ui.screen.home import androidx.compose.animation.AnimatedVisibility import androidx.compose.foundation.basicMarquee import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.OpenInNew import androidx.compose.material.icons.filled.Delete import androidx.compose.material.icons.filled.Info import androidx.compose.material.icons.filled.Refresh import androidx.compose.material.icons.outlined.Settings import androidx.compose.material3.Button import androidx.compose.material3.ElevatedCard import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.LocalContentColor import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold import androidx.compose.material3.Text import androidx.compose.material3.TopAppBar import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import androidx.paging.compose.collectAsLazyPagingItems import cafe.adriel.voyager.core.screen.Screen import cafe.adriel.voyager.koin.koinScreenModel import cafe.adriel.voyager.navigator.LocalNavigator import cafe.adriel.voyager.navigator.currentOrThrow import dev.beefers.vendetta.manager.BuildConfig import dev.beefers.vendetta.manager.R import dev.beefers.vendetta.manager.domain.manager.PreferenceManager import dev.beefers.vendetta.manager.ui.components.SegmentedButton import dev.beefers.vendetta.manager.ui.screen.installer.InstallerScreen import dev.beefers.vendetta.manager.ui.screen.settings.SettingsScreen import dev.beefers.vendetta.manager.ui.viewmodel.home.HomeViewModel import dev.beefers.vendetta.manager.ui.widgets.AppIcon import dev.beefers.vendetta.manager.ui.widgets.dialog.EndOfLifeDialog import dev.beefers.vendetta.manager.ui.widgets.dialog.StoragePermissionsDialog import dev.beefers.vendetta.manager.ui.widgets.home.CommitList import dev.beefers.vendetta.manager.ui.widgets.updater.UpdateDialog import dev.beefers.vendetta.manager.utils.Constants import dev.beefers.vendetta.manager.utils.DiscordVersion import dev.beefers.vendetta.manager.utils.navigate import org.koin.compose.koinInject class HomeScreen : Screen { @Composable override fun Content() { val navigator = LocalNavigator.currentOrThrow val prefs: PreferenceManager = koinInject() val viewModel: HomeViewModel = koinScreenModel() val currentVersion = remember { DiscordVersion.fromVersionCode(viewModel.installManager.current?.longVersionCode.toString()) } val latestVersion = remember(prefs.discordVersion, viewModel.discordVersions, prefs.channel) { when { prefs.discordVersion.isBlank() -> { val ver = viewModel.discordVersions?.get(prefs.channel) if (ver == null) return@remember null if (ver.isSupported()) ver else DiscordVersion.fromVersionCode(ver.type.maxVersionCode.toString()) } else -> DiscordVersion.fromVersionCode(prefs.discordVersion) } } // == Dialogs == // StoragePermissionsDialog() if (viewModel.showEolDialog) EndOfLifeDialog { viewModel.showEolDialog = false } if ( viewModel.showUpdateDialog && viewModel.release != null && !BuildConfig.DEBUG ) { UpdateDialog( release = viewModel.release!!, isUpdating = viewModel.isUpdating, onDismiss = { viewModel.showUpdateDialog = false }, onConfirm = { viewModel.downloadAndInstallUpdate() } ) } // == Screen == // Scaffold( topBar = { TitleBar() }, ) { pv -> Column( horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.spacedBy(16.dp), modifier = Modifier .padding(pv) .padding(16.dp) .fillMaxWidth() ) { AppIcon( customIcon = prefs.patchIcon, releaseChannel = prefs.channel, modifier = Modifier.size(60.dp) ) Text( text = prefs.appName, style = MaterialTheme.typography.titleLarge ) Column( horizontalAlignment = Alignment.CenterHorizontally, modifier = Modifier.fillMaxWidth() ) { AnimatedVisibility(visible = currentVersion != null) { Text( text = stringResource( R.string.version_current, currentVersion.toString() ), style = MaterialTheme.typography.labelLarge, color = LocalContentColor.current.copy(alpha = 0.5f), textAlign = TextAlign.Center ) } val latestLabel = if (prefs.discordVersion.isNotBlank()) R.string.version_target else R.string.version_latest AnimatedVisibility(visible = latestVersion != null) { Text( text = stringResource(latestLabel, latestVersion.toString()), style = MaterialTheme.typography.labelLarge, color = LocalContentColor.current.copy(alpha = 0.5f), textAlign = TextAlign.Center ) } } Button( onClick = { navigator.navigate(InstallerScreen(latestVersion!!)) }, enabled = latestVersion != null && (prefs.allowDowngrade || latestVersion >= (currentVersion ?: Constants.DUMMY_VERSION)), modifier = Modifier.fillMaxWidth() ) { val label = when { latestVersion == null -> R.string.msg_loading currentVersion == null -> R.string.action_install currentVersion == latestVersion -> R.string.action_reinstall latestVersion > currentVersion -> R.string.action_update else -> if (prefs.allowDowngrade) R.string.msg_downgrade else R.string.msg_downgrade_disallowed } Text( text = stringResource(label), textAlign = TextAlign.Center, maxLines = 1, modifier = Modifier .basicMarquee() .fillMaxWidth() ) } AnimatedVisibility(visible = viewModel.installManager.current != null) { Row( horizontalArrangement = Arrangement.spacedBy(2.dp), modifier = Modifier.clip(RoundedCornerShape(16.dp)) ) { SegmentedButton( icon = Icons.AutoMirrored.Filled.OpenInNew, text = stringResource(R.string.action_launch), onClick = { viewModel.launchVendetta() } ) SegmentedButton( icon = Icons.Filled.Info, text = stringResource(R.string.action_info), onClick = { viewModel.launchVendettaInfo() } ) SegmentedButton( icon = Icons.Filled.Delete, text = stringResource(R.string.action_uninstall), onClick = { viewModel.uninstallVendetta() } ) } } ElevatedCard( modifier = Modifier .fillMaxSize() ) { CommitList( commits = viewModel.commits.collectAsLazyPagingItems() ) } } } } @Composable @OptIn(ExperimentalMaterial3Api::class) private fun TitleBar() { TopAppBar( title = { Text(stringResource(R.string.title_home)) }, actions = { Actions() } ) } @Composable private fun Actions() { val viewModel: HomeViewModel = koinScreenModel() val navigator = LocalNavigator.currentOrThrow IconButton(onClick = { viewModel.getDiscordVersions() }) { Icon( imageVector = Icons.Filled.Refresh, contentDescription = stringResource(R.string.action_reload) ) } IconButton(onClick = { navigator.navigate(SettingsScreen()) }) { Icon( imageVector = Icons.Outlined.Settings, contentDescription = stringResource(R.string.action_open_about) ) } } } ================================================ FILE: app/src/main/java/dev/beefers/vendetta/manager/ui/screen/installer/InstallerScreen.kt ================================================ package dev.beefers.vendetta.manager.ui.screen.installer import android.content.Intent import androidx.activity.ComponentActivity import androidx.activity.compose.BackHandler import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height 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.automirrored.filled.ArrowBack import androidx.compose.material.icons.automirrored.outlined.Article import androidx.compose.material3.Button import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.FilledTonalButton import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.Scaffold import androidx.compose.material3.Text import androidx.compose.material3.TopAppBar import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.key import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import cafe.adriel.voyager.core.screen.Screen import cafe.adriel.voyager.core.screen.ScreenKey import cafe.adriel.voyager.koin.koinScreenModel import cafe.adriel.voyager.navigator.LocalNavigator import cafe.adriel.voyager.navigator.currentOrThrow import dev.beefers.vendetta.manager.R import dev.beefers.vendetta.manager.domain.manager.PreferenceManager import dev.beefers.vendetta.manager.installer.step.StepStatus import dev.beefers.vendetta.manager.ui.viewmodel.installer.InstallerViewModel import dev.beefers.vendetta.manager.ui.widgets.dialog.BackWarningDialog import dev.beefers.vendetta.manager.ui.widgets.dialog.DownloadFailedDialog import dev.beefers.vendetta.manager.ui.widgets.installer.StepGroupCard import dev.beefers.vendetta.manager.utils.DiscordVersion import okhttp3.internal.toImmutableList import org.koin.compose.koinInject import org.koin.core.parameter.parametersOf import java.util.UUID class InstallerScreen( val version: DiscordVersion ) : Screen { override val key: ScreenKey = "Installer-${UUID.randomUUID()}" @Composable override fun Content() { val nav = LocalNavigator.currentOrThrow val activity = LocalContext.current as? ComponentActivity val viewModel: InstallerViewModel = koinScreenModel { parametersOf(version) } LaunchedEffect(viewModel.runner.currentStep) { viewModel.expandGroup(viewModel.runner.currentStep?.group) } // Listen for error messages from InstallService val intentListener: (Intent) -> Unit = remember { { val msg = it.getStringExtra("vendetta.extras.EXTRA_MESSAGE") if (msg?.isNotBlank() == true) viewModel.logError(msg) } } DisposableEffect(Unit) { activity?.addOnNewIntentListener(intentListener) onDispose { activity?.removeOnNewIntentListener(intentListener) } } BackHandler( enabled = !viewModel.runner.completed ) { viewModel.openBackDialog() } if(viewModel.backDialogOpened) { BackWarningDialog( onExitClick = { viewModel.closeBackDialog() viewModel.cancelInstall() nav.pop() }, onClose = { viewModel.closeBackDialog() } ) } if(viewModel.runner.downloadErrored) { DownloadFailedDialog( onTryAgainClick = { viewModel.dismissDownloadFailedDialog() viewModel.cancelInstall() nav.replace(InstallerScreen(version)) }, onDismiss = { viewModel.dismissDownloadFailedDialog() } ) } Scaffold( topBar = { TitleBar( viewModel = viewModel, onBackClick = { if(!viewModel.runner.completed) viewModel.openBackDialog() else nav.pop() } ) } ) { Column( modifier = Modifier .padding(it) .padding(16.dp) .verticalScroll(rememberScrollState()) ) { for ((group, steps) in viewModel.groupedSteps) { key(group) { StepGroupCard( name = stringResource(group.nameRes), isCurrent = viewModel.expandedGroup == group, onClick = { viewModel.expandGroup(group) }, steps = steps, ) } } if (viewModel.runner.completed) { Spacer(modifier = Modifier.height(16.dp)) // Show launch only if success val installSuccessful = viewModel.runner.currentStep?.status == StepStatus.SUCCESSFUL if (installSuccessful) { Button( onClick = { viewModel.launchVendetta() }, modifier = Modifier.fillMaxWidth() ) { Text(stringResource(R.string.action_launch)) } } Spacer(modifier = Modifier.height(8.dp)) Row( horizontalArrangement = Arrangement.spacedBy(8.dp), modifier = Modifier .fillMaxWidth() ) { FilledTonalButton( onClick = { viewModel.shareLogs(activity!!) }, modifier = Modifier.weight(1f) ) { Text(stringResource(R.string.action_share_logs)) } FilledTonalButton( onClick = { viewModel.clearCache() }, modifier = Modifier.weight(1f) ) { Text(stringResource(R.string.action_clear_cache)) } } } } } } @Composable @OptIn(ExperimentalMaterial3Api::class) private fun TitleBar( onBackClick: () -> Unit, viewModel: InstallerViewModel ) { val prefs: PreferenceManager = koinInject() val nav = LocalNavigator.currentOrThrow TopAppBar( title = { Text(stringResource(R.string.title_installer)) }, navigationIcon = { IconButton(onClick = onBackClick) { Icon( imageVector = Icons.AutoMirrored.Filled.ArrowBack, contentDescription = stringResource(R.string.action_back) ) } }, actions = { if (prefs.isDeveloper && viewModel.runner.completed) { IconButton(onClick = { nav.push(LogViewerScreen(viewModel.runner.logger.logs.toImmutableList())) }) { Icon( imageVector = Icons.AutoMirrored.Outlined.Article, contentDescription = stringResource(R.string.action_view_logs) ) } } } ) } } ================================================ FILE: app/src/main/java/dev/beefers/vendetta/manager/ui/screen/installer/LogViewerScreen.kt ================================================ package dev.beefers.vendetta.manager.ui.screen.installer import androidx.compose.foundation.horizontalScroll import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.foundation.rememberScrollState import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.ArrowBack import androidx.compose.material.icons.filled.Share import androidx.compose.material.icons.outlined.MoreVert import androidx.compose.material.icons.outlined.Save import androidx.compose.material3.Checkbox import androidx.compose.material3.DropdownMenu import androidx.compose.material3.DropdownMenuItem 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.material3.TopAppBar import androidx.compose.material3.TopAppBarDefaults import androidx.compose.material3.TopAppBarScrollBehavior import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.DpOffset import androidx.compose.ui.unit.dp import cafe.adriel.voyager.core.screen.Screen import cafe.adriel.voyager.koin.koinScreenModel import cafe.adriel.voyager.navigator.LocalNavigator import cafe.adriel.voyager.navigator.currentOrThrow import dev.beefers.vendetta.manager.R import dev.beefers.vendetta.manager.domain.manager.PreferenceManager import dev.beefers.vendetta.manager.installer.util.LogEntry import dev.beefers.vendetta.manager.ui.viewmodel.installer.LogViewerViewModel import dev.beefers.vendetta.manager.ui.widgets.installer.LogLine import dev.beefers.vendetta.manager.utils.DimenUtils import dev.beefers.vendetta.manager.utils.rememberFileSaveLauncher import dev.beefers.vendetta.manager.utils.thenIf import org.koin.compose.koinInject import org.koin.core.parameter.parametersOf @OptIn(ExperimentalMaterial3Api::class) class LogViewerScreen( val logs: List ) : Screen { @Composable override fun Content() { val prefs: PreferenceManager = koinInject() val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior() val viewModel: LogViewerViewModel = koinScreenModel { parametersOf(logs) } Scaffold( topBar = { Toolbar(scrollBehavior, viewModel) }, contentWindowInsets = WindowInsets(0, 0, 0, 0), modifier = Modifier .nestedScroll(scrollBehavior.nestedScrollConnection) ) { pv -> LazyColumn( contentPadding = PaddingValues(bottom = DimenUtils.navBarPadding), modifier = Modifier .padding(pv) .thenIf(!prefs.logsLineWrap) { horizontalScroll(rememberScrollState()) } ) { itemsIndexed(viewModel.logs) { i, log -> LogLine( log = log, alternateBackground = i % 2 == 0 && prefs.logsAlternateBackground, wrapText = prefs.logsLineWrap, logPadding = viewModel.maxLogLength, onLongClick = { viewModel.copyLog(log) } ) } } } } @Composable private fun Toolbar( scrollBehavior: TopAppBarScrollBehavior, viewModel: LogViewerViewModel ) { val navigator = LocalNavigator.currentOrThrow val context = LocalContext.current val saveFile = rememberFileSaveLauncher(content = viewModel.logsString) TopAppBar( title = { Text(stringResource(R.string.title_logs)) }, navigationIcon = { IconButton(onClick = { navigator.pop() }) { Icon( imageVector = Icons.AutoMirrored.Filled.ArrowBack, contentDescription = stringResource(R.string.action_back) ) } }, actions = { var showDropdown by remember { mutableStateOf(false) } IconButton(onClick = { saveFile.launch("VD-Manager-${System.currentTimeMillis()}.log") }) { Icon( imageVector = Icons.Outlined.Save, contentDescription = stringResource(R.string.action_save_logs) ) } IconButton(onClick = { viewModel.shareLogs(context) }) { Icon( imageVector = Icons.Filled.Share, contentDescription = stringResource(R.string.action_share_logs) ) } IconButton(onClick = { showDropdown = true }) { Icon( imageVector = Icons.Outlined.MoreVert, contentDescription = stringResource(R.string.action_more_options) ) } Dropdown( viewModel, expanded = showDropdown, onDismiss = { showDropdown = false } ) }, scrollBehavior = scrollBehavior ) } @Composable fun Dropdown( viewModel: LogViewerViewModel, expanded: Boolean, onDismiss: () -> Unit ) { val prefs: PreferenceManager = koinInject() Box { DropdownMenu( expanded = expanded, onDismissRequest = onDismiss, offset = DpOffset( 10.dp, 26.dp ) ) { DropdownMenuItem( text = { Text(stringResource(R.string.settings_logs_line_wrap)) }, onClick = { prefs.logsLineWrap = !prefs.logsLineWrap }, trailingIcon = { Checkbox( checked = prefs.logsLineWrap, onCheckedChange = { prefs.logsLineWrap = it } ) } ) DropdownMenuItem( text = { Text(stringResource(R.string.settings_logs_alternate_lines)) }, onClick = { prefs.logsAlternateBackground = !prefs.logsAlternateBackground }, trailingIcon = { Checkbox( checked = prefs.logsAlternateBackground, onCheckedChange = { prefs.logsAlternateBackground = it } ) } ) DropdownMenuItem( text = { Text(stringResource(R.string.action_copy_logs)) }, onClick = { viewModel.copyLogs() } ) } } } } ================================================ FILE: app/src/main/java/dev/beefers/vendetta/manager/ui/screen/libraries/LibrariesScreen.kt ================================================ package dev.beefers.vendetta.manager.ui.screen.libraries import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.itemsIndexed 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.LargeTopAppBar import androidx.compose.material3.Scaffold import androidx.compose.material3.Text import androidx.compose.material3.TopAppBarDefaults import androidx.compose.material3.TopAppBarScrollBehavior import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.res.stringResource import cafe.adriel.voyager.core.screen.Screen import cafe.adriel.voyager.koin.koinScreenModel import cafe.adriel.voyager.navigator.LocalNavigator import cafe.adriel.voyager.navigator.currentOrThrow import dev.beefers.vendetta.manager.R import dev.beefers.vendetta.manager.ui.components.ThinDivider import dev.beefers.vendetta.manager.ui.viewmodel.libraries.LibrariesViewModel import dev.beefers.vendetta.manager.ui.widgets.libraries.LibraryItem class LibrariesScreen: Screen { @Composable @OptIn(ExperimentalMaterial3Api::class) override fun Content() { val viewModel: LibrariesViewModel = koinScreenModel() val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior() Scaffold( topBar = { TitleBar(scrollBehavior) }, contentWindowInsets = WindowInsets(0, 0, 0, 0), modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection) ) { pv -> LazyColumn( modifier = Modifier.padding(pv) ) { itemsIndexed(viewModel.libraries.libraries) { i, library -> Column { LibraryItem( library = library ) if(i != viewModel.libraries.libraries.lastIndex) { ThinDivider() } } } } } } @Composable @OptIn(ExperimentalMaterial3Api::class) fun TitleBar( scrollBehavior: TopAppBarScrollBehavior ) { val navigator = LocalNavigator.currentOrThrow LargeTopAppBar( title = { Text(stringResource(R.string.title_libraries)) }, navigationIcon = { IconButton(onClick = { navigator.pop() }) { Icon( imageVector = Icons.AutoMirrored.Filled.ArrowBack, contentDescription = stringResource(R.string.action_back) ) } }, scrollBehavior = scrollBehavior ) } } ================================================ FILE: app/src/main/java/dev/beefers/vendetta/manager/ui/screen/settings/AdvancedSettings.kt ================================================ package dev.beefers.vendetta.manager.ui.screen.settings import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.WindowInsets 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.automirrored.filled.ArrowBack import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.LargeTopAppBar import androidx.compose.material3.Scaffold import androidx.compose.material3.Text import androidx.compose.material3.TopAppBarDefaults import androidx.compose.material3.TopAppBarScrollBehavior import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.core.net.toUri import cafe.adriel.voyager.core.screen.Screen import cafe.adriel.voyager.koin.koinScreenModel import cafe.adriel.voyager.navigator.LocalNavigator import cafe.adriel.voyager.navigator.currentOrThrow import dev.beefers.vendetta.manager.R import dev.beefers.vendetta.manager.domain.manager.Mirror import dev.beefers.vendetta.manager.domain.manager.PreferenceManager import dev.beefers.vendetta.manager.ui.components.settings.SettingsButton import dev.beefers.vendetta.manager.ui.components.settings.SettingsItemChoice import dev.beefers.vendetta.manager.ui.components.settings.SettingsSwitch import dev.beefers.vendetta.manager.ui.viewmodel.settings.AdvancedSettingsViewModel import dev.beefers.vendetta.manager.utils.DimenUtils import org.koin.compose.koinInject class AdvancedSettings: Screen { @Composable @OptIn(ExperimentalMaterial3Api::class) override fun Content() { val ctx = LocalContext.current val prefs: PreferenceManager = koinInject() val viewModel: AdvancedSettingsViewModel = koinScreenModel() val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior() Scaffold( topBar = { TitleBar(scrollBehavior) }, contentWindowInsets = WindowInsets(0, 0, 0, 0), modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection) ) { pv -> Column( modifier = Modifier .padding(pv) .verticalScroll(rememberScrollState()) .padding(bottom = DimenUtils.navBarPadding) ) { SettingsItemChoice( label = stringResource(R.string.settings_check_updates), pref = prefs.updateDuration, labelFactory = { ctx.getString(it.labelRes) }, onPrefChange = { prefs.updateDuration = it viewModel.updateCheckerDuration(it) } ) SettingsItemChoice( label = stringResource(R.string.settings_mirror), pref = prefs.mirror, excludedOptions = listOf(Mirror.VENDETTA_ROCKS), labelFactory = { it.baseUrl.toUri().authority ?: it.baseUrl }, onPrefChange = { prefs.mirror = it } ) SettingsItemChoice( label = stringResource(R.string.install_method), pref = prefs.installMethod, labelFactory = { ctx.getString(it.labelRes) }, onPrefChange = viewModel::setInstallMethod, ) SettingsSwitch( label = stringResource(R.string.settings_auto_clear_cache), secondaryLabel = stringResource(R.string.settings_auto_clear_cache_description), pref = prefs.autoClearCache, onPrefChange = { prefs.autoClearCache = it } ) SettingsButton( label = stringResource(R.string.action_clear_cache), onClick = { viewModel.clearCache() } ) } } } @Composable @OptIn(ExperimentalMaterial3Api::class) fun TitleBar( scrollBehavior: TopAppBarScrollBehavior ) { val navigator = LocalNavigator.currentOrThrow LargeTopAppBar( title = { Text(stringResource(R.string.settings_advanced)) }, navigationIcon = { IconButton(onClick = { navigator.pop() }) { Icon( imageVector = Icons.AutoMirrored.Filled.ArrowBack, contentDescription = stringResource(R.string.action_back) ) } }, scrollBehavior = scrollBehavior ) } } ================================================ FILE: app/src/main/java/dev/beefers/vendetta/manager/ui/screen/settings/AppearanceSettings.kt ================================================ package dev.beefers.vendetta.manager.ui.screen.settings import android.os.Build import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll import androidx.compose.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.LargeTopAppBar import androidx.compose.material3.Scaffold import androidx.compose.material3.Text import androidx.compose.material3.TopAppBarDefaults import androidx.compose.material3.TopAppBarScrollBehavior import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import cafe.adriel.voyager.core.screen.Screen import cafe.adriel.voyager.navigator.LocalNavigator import cafe.adriel.voyager.navigator.currentOrThrow import dev.beefers.vendetta.manager.R import dev.beefers.vendetta.manager.domain.manager.PreferenceManager import dev.beefers.vendetta.manager.ui.components.settings.SettingsSwitch import dev.beefers.vendetta.manager.ui.widgets.settings.ThemePicker import dev.beefers.vendetta.manager.utils.DimenUtils import org.koin.compose.koinInject class AppearanceSettings: Screen { @Composable @OptIn(ExperimentalMaterial3Api::class) override fun Content() { val prefs: PreferenceManager = koinInject() val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior() Scaffold( topBar = { TitleBar(scrollBehavior) }, contentWindowInsets = WindowInsets(0, 0, 0, 0), modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection) ) { pv -> Column( modifier = Modifier .padding(pv) .verticalScroll(rememberScrollState()) .padding(bottom = DimenUtils.navBarPadding) ) { ThemePicker(prefs = prefs) Spacer(modifier = Modifier.height(16.dp)) SettingsSwitch( label = stringResource(R.string.settings_dynamic_color), secondaryLabel = stringResource(R.string.settings_dynamic_color_description), pref = prefs.monet, onPrefChange = { prefs.monet = it }, disabled = Build.VERSION.SDK_INT < Build.VERSION_CODES.S ) } } } @Composable @OptIn(ExperimentalMaterial3Api::class) fun TitleBar( scrollBehavior: TopAppBarScrollBehavior ) { val navigator = LocalNavigator.currentOrThrow LargeTopAppBar( title = { Text(stringResource(R.string.settings_appearance)) }, navigationIcon = { IconButton(onClick = { navigator.pop() }) { Icon( imageVector = Icons.AutoMirrored.Filled.ArrowBack, contentDescription = stringResource(R.string.action_back) ) } }, scrollBehavior = scrollBehavior ) } } ================================================ FILE: app/src/main/java/dev/beefers/vendetta/manager/ui/screen/settings/CustomizationSettings.kt ================================================ package dev.beefers.vendetta.manager.ui.screen.settings import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.WindowInsets 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.automirrored.filled.ArrowBack import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.LargeTopAppBar import androidx.compose.material3.Scaffold import androidx.compose.material3.Text import androidx.compose.material3.TopAppBarDefaults import androidx.compose.material3.TopAppBarScrollBehavior import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import cafe.adriel.voyager.core.screen.Screen import cafe.adriel.voyager.navigator.LocalNavigator import cafe.adriel.voyager.navigator.currentOrThrow import dev.beefers.vendetta.manager.R import dev.beefers.vendetta.manager.domain.manager.PreferenceManager import dev.beefers.vendetta.manager.ui.components.settings.SettingsItemChoice import dev.beefers.vendetta.manager.ui.components.settings.SettingsSwitch import dev.beefers.vendetta.manager.ui.components.settings.SettingsTextField import dev.beefers.vendetta.manager.utils.DimenUtils import org.koin.compose.koinInject class CustomizationSettings: Screen { @Composable @OptIn(ExperimentalMaterial3Api::class) override fun Content() { val ctx = LocalContext.current val prefs: PreferenceManager = koinInject() val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior() Scaffold( topBar = { TitleBar(scrollBehavior) }, contentWindowInsets = WindowInsets(0, 0, 0, 0), modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection) ) { pv -> Column( modifier = Modifier .padding(pv) .verticalScroll(rememberScrollState()) .padding(bottom = DimenUtils.navBarPadding) ) { SettingsTextField( label = stringResource(R.string.settings_app_name), pref = prefs.appName, onPrefChange = { prefs.appName = it } ) SettingsSwitch( label = stringResource(R.string.settings_app_icon), secondaryLabel = stringResource(R.string.settings_app_icon_description), pref = prefs.patchIcon, onPrefChange = { prefs.patchIcon = it } ) SettingsItemChoice( label = stringResource(R.string.settings_channel), pref = prefs.channel, labelFactory = { ctx.getString(it.labelRes) }, onPrefChange = { prefs.channel = it } ) } } } @Composable @OptIn(ExperimentalMaterial3Api::class) fun TitleBar( scrollBehavior: TopAppBarScrollBehavior ) { val navigator = LocalNavigator.currentOrThrow LargeTopAppBar( title = { Text(stringResource(R.string.settings_customization)) }, navigationIcon = { IconButton(onClick = { navigator.pop() }) { Icon( imageVector = Icons.AutoMirrored.Filled.ArrowBack, contentDescription = stringResource(R.string.action_back) ) } }, scrollBehavior = scrollBehavior ) } } ================================================ FILE: app/src/main/java/dev/beefers/vendetta/manager/ui/screen/settings/DeveloperSettings.kt ================================================ package dev.beefers.vendetta.manager.ui.screen.settings import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.WindowInsets 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.automirrored.filled.ArrowBack import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.LargeTopAppBar import androidx.compose.material3.Scaffold import androidx.compose.material3.Text import androidx.compose.material3.TopAppBarDefaults import androidx.compose.material3.TopAppBarScrollBehavior import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.res.stringResource import cafe.adriel.voyager.core.screen.Screen import cafe.adriel.voyager.navigator.LocalNavigator import cafe.adriel.voyager.navigator.currentOrThrow import dev.beefers.vendetta.manager.R import dev.beefers.vendetta.manager.domain.manager.InstallManager import dev.beefers.vendetta.manager.domain.manager.PreferenceManager import dev.beefers.vendetta.manager.ui.components.settings.SettingsButton import dev.beefers.vendetta.manager.ui.components.settings.SettingsSwitch import dev.beefers.vendetta.manager.ui.components.settings.SettingsTextField import dev.beefers.vendetta.manager.utils.DimenUtils import dev.beefers.vendetta.manager.utils.DiscordVersion import org.koin.compose.koinInject import java.io.File class DeveloperSettings: Screen { @Composable @OptIn(ExperimentalMaterial3Api::class) override fun Content() { val prefs: PreferenceManager = koinInject() val installManager: InstallManager = koinInject() val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior() var version by remember { mutableStateOf(prefs.discordVersion) } var versionError by remember { mutableStateOf(false) } val supportingText = when { versionError -> stringResource(R.string.msg_invalid_version) version.isNotBlank() -> DiscordVersion.fromVersionCode(version).toString() else -> null } var moduleLocation by remember { mutableStateOf(prefs.moduleLocation.absolutePath) } Scaffold( topBar = { TitleBar(scrollBehavior) }, contentWindowInsets = WindowInsets(0, 0, 0, 0), modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection) ) { pv -> Column( modifier = Modifier .padding(pv) .verticalScroll(rememberScrollState()) .padding(bottom = DimenUtils.navBarPadding) ) { SettingsTextField( label = stringResource(R.string.settings_package_name), pref = prefs.packageName, onPrefChange = { prefs.packageName = it installManager.getInstalled() } ) SettingsTextField( label = stringResource(R.string.settings_version), pref = version, error = versionError, supportingText = supportingText, onPrefChange = { version = it if (DiscordVersion.fromVersionCode(it) == null && it.isNotBlank()) { versionError = true } else { versionError = false prefs.discordVersion = it } } ) SettingsSwitch( label = stringResource(R.string.settings_debuggable), secondaryLabel = stringResource(R.string.settings_debuggable_description), pref = prefs.debuggable, onPrefChange = { prefs.debuggable = it } ) SettingsTextField( label = stringResource(R.string.settings_module_location), supportingText = stringResource(R.string.settings_module_location_description), pref = moduleLocation, onPrefChange = { moduleLocation = it prefs.moduleLocation = File(it) } ) SettingsButton( label = stringResource(R.string.settings_module_location_reset), onClick = { prefs.moduleLocation = prefs.DEFAULT_MODULE_LOCATION } ) SettingsSwitch( label = stringResource(R.string.settings_allow_downgrade), secondaryLabel = stringResource(R.string.settings_allow_downgrade_description), pref = prefs.allowDowngrade, onPrefChange = { prefs.allowDowngrade = it } ) } } } @Composable @OptIn(ExperimentalMaterial3Api::class) fun TitleBar( scrollBehavior: TopAppBarScrollBehavior ) { val navigator = LocalNavigator.currentOrThrow LargeTopAppBar( title = { Text(stringResource(R.string.settings_developer)) }, navigationIcon = { IconButton(onClick = { navigator.pop() }) { Icon( imageVector = Icons.AutoMirrored.Filled.ArrowBack, contentDescription = stringResource(R.string.action_back) ) } }, scrollBehavior = scrollBehavior ) } } ================================================ FILE: app/src/main/java/dev/beefers/vendetta/manager/ui/screen/settings/SettingsScreen.kt ================================================ package dev.beefers.vendetta.manager.ui.screen.settings import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.WindowInsets 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.automirrored.filled.ArrowBack import androidx.compose.material.icons.outlined.AutoAwesome import androidx.compose.material.icons.outlined.Code import androidx.compose.material.icons.outlined.Info import androidx.compose.material.icons.outlined.Palette import androidx.compose.material.icons.outlined.Tune import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.LargeTopAppBar import androidx.compose.material3.Scaffold import androidx.compose.material3.Text import androidx.compose.material3.TopAppBarDefaults import androidx.compose.material3.TopAppBarScrollBehavior import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.res.stringResource import cafe.adriel.voyager.core.screen.Screen import cafe.adriel.voyager.navigator.LocalNavigator import cafe.adriel.voyager.navigator.currentOrThrow import dev.beefers.vendetta.manager.BuildConfig import dev.beefers.vendetta.manager.R import dev.beefers.vendetta.manager.domain.manager.PreferenceManager import dev.beefers.vendetta.manager.ui.components.settings.SettingsCategory import dev.beefers.vendetta.manager.ui.screen.about.AboutScreen import dev.beefers.vendetta.manager.utils.DimenUtils import org.koin.compose.koinInject class SettingsScreen : Screen { @Composable @OptIn(ExperimentalMaterial3Api::class) override fun Content() { val preferences: PreferenceManager = koinInject() val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior() Scaffold( topBar = { TitleBar(scrollBehavior) }, contentWindowInsets = WindowInsets(0, 0, 0, 0), modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection) ) { pv -> Column( modifier = Modifier .padding(pv) .verticalScroll(rememberScrollState()) .padding(bottom = DimenUtils.navBarPadding) ) { SettingsCategory( icon = Icons.Outlined.Palette, text = stringResource(R.string.settings_appearance), subtext = stringResource(R.string.settings_appearance_description), destination = ::AppearanceSettings ) SettingsCategory( icon = Icons.Outlined.AutoAwesome, text = stringResource(R.string.settings_customization), subtext = stringResource(R.string.settings_customization_description), destination = ::CustomizationSettings ) SettingsCategory( icon = Icons.Outlined.Tune, text = stringResource(R.string.settings_advanced), subtext = stringResource(R.string.settings_advanced_description), destination = ::AdvancedSettings ) if (preferences.isDeveloper) { SettingsCategory( icon = Icons.Outlined.Code, text = stringResource(R.string.settings_developer), subtext = stringResource(R.string.settings_developer_description), destination = ::DeveloperSettings ) } SettingsCategory( icon = Icons.Outlined.Info, text = stringResource(R.string.title_about), subtext = buildString { append(stringResource(R.string.app_name)) append(" v${BuildConfig.VERSION_NAME}") if (preferences.isDeveloper) { append(" (${BuildConfig.GIT_COMMIT}") if (BuildConfig.GIT_LOCAL_CHANGES || BuildConfig.GIT_LOCAL_COMMITS) { append(" - Local") } append(")") } }, destination = ::AboutScreen ) } } } @Composable @OptIn(ExperimentalMaterial3Api::class) fun TitleBar( scrollBehavior: TopAppBarScrollBehavior ) { val navigator = LocalNavigator.currentOrThrow LargeTopAppBar( title = { Text(stringResource(R.string.title_settings)) }, navigationIcon = { IconButton(onClick = { navigator.pop() }) { Icon( imageVector = Icons.AutoMirrored.Filled.ArrowBack, contentDescription = stringResource(R.string.action_back) ) } }, scrollBehavior = scrollBehavior ) } } ================================================ FILE: app/src/main/java/dev/beefers/vendetta/manager/ui/theme/Theme.kt ================================================ package dev.beefers.vendetta.manager.ui.theme import android.os.Build import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.material3.MaterialTheme import androidx.compose.material3.darkColorScheme import androidx.compose.material3.dynamicDarkColorScheme import androidx.compose.material3.dynamicLightColorScheme import androidx.compose.material3.lightColorScheme import androidx.compose.runtime.Composable import androidx.compose.ui.platform.LocalContext import dev.beefers.vendetta.manager.domain.manager.PreferenceManager import dev.beefers.vendetta.manager.domain.manager.Theme import org.koin.compose.koinInject @Composable fun VendettaManagerTheme( content: @Composable () -> Unit ) { val prefs = koinInject() val dynamicColor = prefs.monet val darkTheme = when (prefs.theme) { Theme.SYSTEM -> isSystemInDarkTheme() Theme.DARK -> true Theme.LIGHT -> false } 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() } MaterialTheme( colorScheme = colorScheme, typography = Typography, content = content ) } ================================================ FILE: app/src/main/java/dev/beefers/vendetta/manager/ui/theme/Type.kt ================================================ package dev.beefers.vendetta.manager.ui.theme import androidx.compose.material3.Typography // Set of Material typography styles to start with val Typography = Typography() ================================================ FILE: app/src/main/java/dev/beefers/vendetta/manager/ui/viewmodel/home/HomeViewModel.kt ================================================ package dev.beefers.vendetta.manager.ui.viewmodel.home import android.content.Context import android.content.Intent import android.net.Uri import android.os.Environment import android.provider.Settings import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue import androidx.paging.Pager import androidx.paging.PagingConfig import androidx.paging.cachedIn import cafe.adriel.voyager.core.model.ScreenModel import cafe.adriel.voyager.core.model.screenModelScope import dev.beefers.vendetta.manager.BuildConfig import dev.beefers.vendetta.manager.domain.manager.DownloadManager import dev.beefers.vendetta.manager.domain.manager.InstallManager import dev.beefers.vendetta.manager.domain.manager.InstallMethod import dev.beefers.vendetta.manager.domain.manager.PreferenceManager import dev.beefers.vendetta.manager.domain.repository.RestRepository import dev.beefers.vendetta.manager.installer.Installer import dev.beefers.vendetta.manager.installer.session.SessionInstaller import dev.beefers.vendetta.manager.installer.shizuku.ShizukuInstaller import dev.beefers.vendetta.manager.network.dto.Release import dev.beefers.vendetta.manager.network.utils.CommitsPagingSource import dev.beefers.vendetta.manager.network.utils.dataOrNull import dev.beefers.vendetta.manager.network.utils.ifSuccessful import dev.beefers.vendetta.manager.utils.DiscordVersion import dev.beefers.vendetta.manager.utils.isMiui import kotlinx.coroutines.launch import java.io.File class HomeViewModel( private val repo: RestRepository, val context: Context, val prefs: PreferenceManager, val installManager: InstallManager, private val downloadManager: DownloadManager ) : ScreenModel { private val cacheDir = context.externalCacheDir ?: File( Environment.getExternalStorageDirectory(), Environment.DIRECTORY_DOWNLOADS ).resolve("BunnyManager").also { it.mkdirs() } var discordVersions by mutableStateOf?>(null) private set var release by mutableStateOf(null) private set var showUpdateDialog by mutableStateOf(false) var showEolDialog by mutableStateOf(true) var isUpdating by mutableStateOf(false) val commits = Pager(PagingConfig(pageSize = 30)) { CommitsPagingSource(repo) }.flow.cachedIn(screenModelScope) init { getDiscordVersions() checkForUpdate() } fun getDiscordVersions() { screenModelScope.launch { discordVersions = repo.getLatestDiscordVersions().dataOrNull if (prefs.autoClearCache) autoClearCache() } } fun launchVendetta() { installManager.current?.let { val intent = context.packageManager.getLaunchIntentForPackage(it.packageName)?.apply { addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) } context.startActivity(intent) } } fun uninstallVendetta() { installManager.uninstall() } fun launchVendettaInfo() { installManager.current?.let { Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).apply { addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) data = Uri.parse("package:${it.packageName}") context.startActivity(this) } } } private fun autoClearCache() { val currentVersion = DiscordVersion.fromVersionCode(installManager.current?.longVersionCode.toString()) ?: return val latestVersion = when { prefs.discordVersion.isBlank() -> discordVersions?.get(prefs.channel) else -> DiscordVersion.fromVersionCode(prefs.discordVersion) } ?: return if (latestVersion > currentVersion) { for (file in (context.externalCacheDir ?: context.cacheDir).listFiles() ?: emptyArray()) { if (file.isDirectory) file.deleteRecursively() } } } private fun checkForUpdate() { screenModelScope.launch { release = repo.getLatestRelease("pyoncord/BunnyManager").dataOrNull release?.let { showUpdateDialog = it.tagName.toInt() > BuildConfig.VERSION_CODE } repo.getLatestRelease("pyoncord/BunnyXposed").ifSuccessful { if (prefs.moduleVersion != it.tagName) { prefs.moduleVersion = it.tagName val module = File(cacheDir, "xposed.apk") if (module.exists()) module.delete() } } } } fun downloadAndInstallUpdate() { screenModelScope.launch { val update = File(cacheDir, "update.apk") if (update.exists()) update.delete() isUpdating = true downloadManager.downloadUpdate(update) isUpdating = false val installer: Installer = when (prefs.installMethod) { InstallMethod.DEFAULT -> SessionInstaller(context) InstallMethod.SHIZUKU -> ShizukuInstaller(context) } installer.installApks(silent = !isMiui, update) } } } ================================================ FILE: app/src/main/java/dev/beefers/vendetta/manager/ui/viewmodel/installer/InstallerViewModel.kt ================================================ package dev.beefers.vendetta.manager.ui.viewmodel.installer import android.content.Context import android.content.Intent import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue import androidx.core.app.ShareCompat import androidx.core.content.FileProvider import cafe.adriel.voyager.core.model.ScreenModel import cafe.adriel.voyager.core.model.screenModelScope import dev.beefers.vendetta.manager.BuildConfig import dev.beefers.vendetta.manager.R import dev.beefers.vendetta.manager.domain.manager.InstallManager import dev.beefers.vendetta.manager.installer.step.Step import dev.beefers.vendetta.manager.installer.step.StepGroup import dev.beefers.vendetta.manager.installer.step.StepRunner import dev.beefers.vendetta.manager.utils.DiscordVersion import dev.beefers.vendetta.manager.utils.showToast import kotlinx.collections.immutable.ImmutableMap import kotlinx.collections.immutable.toImmutableMap import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.cancel import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import java.io.File import java.util.concurrent.atomic.AtomicBoolean class InstallerViewModel( private val context: Context, private val installManager: InstallManager, discordVersion: DiscordVersion ) : ScreenModel { val runner = StepRunner(discordVersion) val groupedSteps: ImmutableMap> = StepGroup.entries .associateWith { group -> runner.steps.filter { step -> step.group == group } } .toImmutableMap() private val tempLogStorageDir = context.filesDir.resolve("logsTmp").also { it.mkdirs() } private val logsString by lazy { runner.logger.logs.joinToString("\n") { it.toString() } } private val installationRunning = AtomicBoolean(false) private val job = screenModelScope.launch(Dispatchers.Main) { if (installationRunning.getAndSet(true)) { return@launch } withContext(Dispatchers.IO) { runner.runAll() } } var backDialogOpened by mutableStateOf(false) private set var expandedGroup by mutableStateOf(null) private set fun logError(msg: String?) { runner.logger.e("") runner.logger.e(msg) } fun clearCache() { runner.clearCache() context.showToast(R.string.msg_cleared_cache) } fun openBackDialog() { backDialogOpened = true } fun closeBackDialog() { backDialogOpened = false } fun dismissDownloadFailedDialog() { runner.downloadErrored = false } fun expandGroup(group: StepGroup?) { expandedGroup = group } fun launchVendetta() { installManager.current?.let { val intent = context.packageManager.getLaunchIntentForPackage(it.packageName)?.apply { addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) } context.startActivity(intent) } } fun cancelInstall() { runCatching { job.cancel("User exited the installer") } } private fun saveToAppStorage(): File { // Delete old logs to prevent junk buildup tempLogStorageDir.deleteRecursively() tempLogStorageDir.mkdirs() val tmpFile = tempLogStorageDir.resolve("VD-Manager-${System.currentTimeMillis()}.log") tmpFile.outputStream().use { stream -> stream.write(logsString.toByteArray()) } return tmpFile } fun shareLogs(activityContext: Context) { val saved = saveToAppStorage() val uri = FileProvider.getUriForFile( activityContext, BuildConfig.APPLICATION_ID + ".provider", saved ) ShareCompat.IntentBuilder(activityContext) .setType("text/plain") .setStream(uri) .apply { intent.apply { addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) } } .startChooser() } } ================================================ FILE: app/src/main/java/dev/beefers/vendetta/manager/ui/viewmodel/installer/LogViewerViewModel.kt ================================================ package dev.beefers.vendetta.manager.ui.viewmodel.installer import android.content.Context import android.content.Intent import androidx.core.app.ShareCompat import androidx.core.content.FileProvider import cafe.adriel.voyager.core.model.ScreenModel import dev.beefers.vendetta.manager.BuildConfig import dev.beefers.vendetta.manager.R import dev.beefers.vendetta.manager.installer.util.LogEntry import dev.beefers.vendetta.manager.utils.copyText import dev.beefers.vendetta.manager.utils.showToast import java.io.File class LogViewerViewModel( private val context: Context, val logs: List ): ScreenModel { private val tempLogStorageDir = context.filesDir.resolve("logsTmp").also { it.mkdirs() } val logsString by lazy { logs.joinToString("\n") { it.toString() } } val maxLogLength = logs.maxOf { it.message.length } fun copyLog(log: LogEntry) { context.copyText(log.toString()) context.showToast(R.string.msg_copied) } fun copyLogs() { context.copyText(logsString) context.showToast(R.string.msg_copied) } private fun saveToAppStorage(): File { tempLogStorageDir.deleteRecursively() tempLogStorageDir.mkdirs() val tmpFile = tempLogStorageDir.resolve("VD-Manager-${System.currentTimeMillis()}.log") tmpFile.outputStream().use { stream -> stream.write(logsString.toByteArray()) } return tmpFile } fun shareLogs(activityContext: Context) { val saved = saveToAppStorage() val uri = FileProvider.getUriForFile( activityContext, BuildConfig.APPLICATION_ID + ".provider", saved ) ShareCompat.IntentBuilder(activityContext) .setType("text/plain") .setStream(uri) .apply { intent.apply { addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) } } .startChooser() } } ================================================ FILE: app/src/main/java/dev/beefers/vendetta/manager/ui/viewmodel/libraries/LibrariesViewModel.kt ================================================ package dev.beefers.vendetta.manager.ui.viewmodel.libraries import android.content.Context import cafe.adriel.voyager.core.model.ScreenModel import com.mikepenz.aboutlibraries.Libs import com.mikepenz.aboutlibraries.util.withContext class LibrariesViewModel( context: Context ): ScreenModel { val libraries = Libs.Builder().withContext(context).build() } ================================================ FILE: app/src/main/java/dev/beefers/vendetta/manager/ui/viewmodel/settings/AdvancedSettingsViewModel.kt ================================================ package dev.beefers.vendetta.manager.ui.viewmodel.settings import android.content.Context import android.os.Environment import androidx.work.ExistingPeriodicWorkPolicy import androidx.work.PeriodicWorkRequestBuilder import androidx.work.WorkManager import cafe.adriel.voyager.core.model.ScreenModel import cafe.adriel.voyager.core.model.screenModelScope import dev.beefers.vendetta.manager.R import dev.beefers.vendetta.manager.domain.manager.InstallMethod import dev.beefers.vendetta.manager.domain.manager.PreferenceManager import dev.beefers.vendetta.manager.domain.manager.UpdateCheckerDuration import dev.beefers.vendetta.manager.installer.shizuku.ShizukuPermissions import dev.beefers.vendetta.manager.updatechecker.worker.UpdateWorker import dev.beefers.vendetta.manager.utils.showToast import kotlinx.coroutines.launch import java.io.File class AdvancedSettingsViewModel( private val context: Context, private val prefs: PreferenceManager, ) : ScreenModel { private val cacheDir = context.externalCacheDir ?: File(Environment.getExternalStorageDirectory(), Environment.DIRECTORY_DOWNLOADS).resolve("BunnyManager").also { it.mkdirs() } fun clearCache() { cacheDir.deleteRecursively() context.showToast(R.string.msg_cleared_cache) } fun updateCheckerDuration(updateCheckerDuration: UpdateCheckerDuration) { val wm = WorkManager.getInstance(context) when (updateCheckerDuration) { UpdateCheckerDuration.DISABLED -> wm.cancelUniqueWork("dev.beefers.vendetta.manager.UPDATE_CHECK") else -> wm.enqueueUniquePeriodicWork( "dev.beefers.vendetta.manager.UPDATE_CHECK", ExistingPeriodicWorkPolicy.CANCEL_AND_REENQUEUE, PeriodicWorkRequestBuilder( updateCheckerDuration.time, updateCheckerDuration.unit ).build() ) } } fun setInstallMethod(method: InstallMethod) { when (method) { InstallMethod.SHIZUKU -> screenModelScope.launch { if (ShizukuPermissions.waitShizukuPermissions()) { prefs.installMethod = InstallMethod.SHIZUKU } else { context.showToast(R.string.msg_shizuku_denied) } } else -> prefs.installMethod = method } } } ================================================ FILE: app/src/main/java/dev/beefers/vendetta/manager/ui/widgets/AppIcon.kt ================================================ package dev.beefers.vendetta.manager.ui.widgets import androidx.compose.foundation.Image import androidx.compose.foundation.background import androidx.compose.foundation.shape.CircleShape import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.painterResource import dev.beefers.vendetta.manager.R import dev.beefers.vendetta.manager.utils.DiscordVersion @Composable fun AppIcon( customIcon: Boolean, releaseChannel: DiscordVersion.Type, modifier: Modifier = Modifier ) { val iconColor = remember(customIcon, releaseChannel) { when { customIcon -> Color(0xFF48488B) releaseChannel == DiscordVersion.Type.ALPHA -> Color(0xFFFBB33C) else -> Color(0xFF5865F2) } } Image( painter = painterResource(id = R.drawable.ic_discord_icon), contentDescription = null, modifier = modifier .clip(CircleShape) .background(iconColor) ) } ================================================ FILE: app/src/main/java/dev/beefers/vendetta/manager/ui/widgets/about/LinkItem.kt ================================================ package dev.beefers.vendetta.manager.ui.widgets.about import androidx.annotation.DrawableRes import androidx.annotation.StringRes import androidx.compose.foundation.clickable import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.material3.ripple import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalUriHandler import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp @Composable fun LinkItem( @DrawableRes icon: Int, @StringRes label: Int, link: String ) { val uriHandler = LocalUriHandler.current Column( verticalArrangement = Arrangement.spacedBy(8.dp), horizontalAlignment = Alignment.CenterHorizontally, modifier = Modifier .clickable( onClick = { uriHandler.openUri(link) }, indication = ripple(bounded = false, radius = 40.dp), interactionSource = remember { MutableInteractionSource() } ) .padding(8.dp) ) { Icon( painter = painterResource(icon), contentDescription = stringResource(label), modifier = Modifier.size(30.dp) ) Text( text = stringResource(label), style = MaterialTheme.typography.labelMedium ) } } ================================================ FILE: app/src/main/java/dev/beefers/vendetta/manager/ui/widgets/about/ListItem.kt ================================================ package dev.beefers.vendetta.manager.ui.widgets.about 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.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.shape.CircleShape import androidx.compose.material3.LocalContentColor import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.unit.dp import coil.compose.AsyncImage @Composable fun ListItem( text: String, subtext: String? = null, imageUrl: String? = null, onClick: (() -> Unit)? = null ) { Row( verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(16.dp), modifier = Modifier .clickable( enabled = onClick != null, onClick = onClick ?: { } ) .padding(16.dp) .fillMaxWidth() ) { imageUrl?.let { AsyncImage( model = it, contentDescription = null, modifier = Modifier .size(35.dp) .clip(CircleShape) ) } Column( verticalArrangement = Arrangement.spacedBy(4.dp) ) { Text( text = text, style = MaterialTheme.typography.bodyLarge ) subtext?.let { Text( text = it, style = MaterialTheme.typography.labelMedium, color = LocalContentColor.current.copy(alpha = 0.5f) ) } } } } ================================================ FILE: app/src/main/java/dev/beefers/vendetta/manager/ui/widgets/about/UserEntry.kt ================================================ package dev.beefers.vendetta.manager.ui.widgets.about import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.widthIn import androidx.compose.foundation.shape.CircleShape import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.material3.ripple import androidx.compose.material3.surfaceColorAtElevation import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.platform.LocalUriHandler import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import coil.compose.AsyncImage @Composable fun UserEntry( name: String, roles: String, username: String = name, isLarge: Boolean = false ) { val uriHandler = LocalUriHandler.current Column( horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.spacedBy(16.dp), modifier = Modifier .clickable( onClick = { uriHandler.openUri("https://github.com/$username") }, indication = ripple(bounded = false, radius = 90.dp), interactionSource = remember { MutableInteractionSource() } ) .widthIn(min = 100.dp) ) { AsyncImage( modifier = Modifier .size(if (isLarge) 70.dp else 50.dp) .clip(CircleShape) .background(MaterialTheme.colorScheme.surfaceColorAtElevation(1.dp)), model = "https://github.com/$username.png", contentDescription = username ) Column( horizontalAlignment = Alignment.CenterHorizontally ) { Text( text = name, style = MaterialTheme.typography.titleMedium.copy( fontSize = 18.sp ) ) Text( text = roles, style = MaterialTheme.typography.titleSmall.copy( color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.6f) ), textAlign = TextAlign.Center ) } } } ================================================ FILE: app/src/main/java/dev/beefers/vendetta/manager/ui/widgets/dialog/BackWarningDialog.kt ================================================ package dev.beefers.vendetta.manager.ui.widgets.dialog import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Warning import androidx.compose.material3.AlertDialog import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.FilledTonalButton 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 dev.beefers.vendetta.manager.R @Composable fun BackWarningDialog( onExitClick: () -> Unit, onClose: () -> Unit ) { AlertDialog( onDismissRequest = onClose, confirmButton = { FilledTonalButton( onClick = onClose, colors = ButtonDefaults.filledTonalButtonColors( containerColor = MaterialTheme.colorScheme.error, contentColor = MaterialTheme.colorScheme.onError ) ) { Text(stringResource(R.string.action_dismiss_nevermind)) } }, dismissButton = { TextButton( onClick = onExitClick, colors = ButtonDefaults.textButtonColors( contentColor = MaterialTheme.colorScheme.onErrorContainer ) ) { Text(stringResource(R.string.action_confirm_exit)) } }, title = { Text(stringResource(R.string.title_warning)) }, text = { Text(stringResource(R.string.msg_back_warning)) }, icon = { Icon(Icons.Filled.Warning, contentDescription = null) }, containerColor = MaterialTheme.colorScheme.errorContainer, iconContentColor = MaterialTheme.colorScheme.onErrorContainer, titleContentColor = MaterialTheme.colorScheme.onErrorContainer, textContentColor = MaterialTheme.colorScheme.onErrorContainer ) } ================================================ FILE: app/src/main/java/dev/beefers/vendetta/manager/ui/widgets/dialog/DownloadFailedDialog.kt ================================================ package dev.beefers.vendetta.manager.ui.widgets.dialog import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.material3.AlertDialog import androidx.compose.material3.Button import androidx.compose.material3.FilledTonalButton import androidx.compose.material3.Text import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import androidx.core.net.toUri import dev.beefers.vendetta.manager.R import dev.beefers.vendetta.manager.domain.manager.PreferenceManager import dev.beefers.vendetta.manager.ui.components.settings.SettingsChoiceDialog import org.koin.compose.koinInject @Composable fun DownloadFailedDialog( onTryAgainClick: () -> Unit, onDismiss: () -> Unit ) { val prefs: PreferenceManager = koinInject() var mirrorPickerOpened by remember { mutableStateOf(false) } SettingsChoiceDialog( visible = mirrorPickerOpened, default = prefs.mirror, title = { Text(stringResource(R.string.settings_mirror)) }, labelFactory = { it.baseUrl.toUri().authority ?: it.baseUrl }, onRequestClose = { mirrorPickerOpened = false }, onChoice = { prefs.mirror = it mirrorPickerOpened = false } ) AlertDialog( onDismissRequest = onDismiss, text = { Column( verticalArrangement = Arrangement.spacedBy(16.dp) ) { Text(stringResource(R.string.msg_change_mirror)) FilledTonalButton( onClick = { mirrorPickerOpened = true }, modifier = Modifier.fillMaxWidth() ) { Text( text = prefs.mirror.baseUrl.toUri().authority ?: prefs.mirror.baseUrl ) } } }, dismissButton = { TextButton(onClick = onDismiss) { Text(stringResource(R.string.action_dismiss_no_thanks)) } }, confirmButton = { Button(onClick = onTryAgainClick) { Text(stringResource(R.string.action_try_again)) } }, title = { Text(stringResource(R.string.title_dl_failed)) } ) } ================================================ FILE: app/src/main/java/dev/beefers/vendetta/manager/ui/widgets/dialog/EndOfLifeDialog.kt ================================================ package dev.beefers.vendetta.manager.ui.widgets.dialog import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Warning import androidx.compose.material3.AlertDialog import androidx.compose.material3.FilledTonalButton import androidx.compose.material3.Icon import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import kotlinx.coroutines.delay @Composable fun EndOfLifeDialog(onDismiss: () -> Unit) { var secondsLeft by remember { mutableIntStateOf(5) } val canDismiss = secondsLeft <= 0 LaunchedEffect(secondsLeft) { if (secondsLeft > 0) { delay(1000L) secondsLeft-- } } val maybeOnDismiss = { if (canDismiss) onDismiss() } AlertDialog( onDismissRequest = maybeOnDismiss, title = { Text(text = "Project Unmaintained") }, text = { Text( text = "Bunny is no longer maintained. Versions have been set to the latest ones supported by Bunny. Please consider switching to an alternative client mod." ) }, icon = { Icon(Icons.Filled.Warning, contentDescription = null) }, confirmButton = { FilledTonalButton( enabled = canDismiss, onClick = maybeOnDismiss, ) { Text(text = "Understood${if (!canDismiss) " ($secondsLeft)" else ""}") } } ) } ================================================ FILE: app/src/main/java/dev/beefers/vendetta/manager/ui/widgets/dialog/StoragePermissionsDialog.kt ================================================ package dev.beefers.vendetta.manager.ui.widgets.dialog import android.Manifest import android.annotation.SuppressLint import android.content.Intent import android.os.Build import android.os.Environment import android.provider.Settings import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.result.contract.ActivityResultContracts import androidx.compose.material3.* import androidx.compose.runtime.* import androidx.compose.ui.res.stringResource import androidx.compose.ui.window.DialogProperties import androidx.core.net.toUri import com.google.accompanist.permissions.* import dev.beefers.vendetta.manager.BuildConfig import dev.beefers.vendetta.manager.R @Composable fun StoragePermissionsDialog() { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { ManageStorageDialog() } else { ExternalStorageDialog() } } @Composable @SuppressLint("NewApi") private fun ManageStorageDialog() { var manageStorageGranted by remember { mutableStateOf(Environment.isExternalStorageManager()) } if (!manageStorageGranted) { AlertDialog( onDismissRequest = {}, confirmButton = { val launcher = rememberLauncherForActivityResult(ActivityResultContracts.StartActivityForResult()) { if (Environment.isExternalStorageManager()) { manageStorageGranted = true } } Button( onClick = { Intent(Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION) .setData("package:${BuildConfig.APPLICATION_ID}".toUri()) .let { launcher.launch(it) } } ) { Text(stringResource(R.string.action_open_settings)) } }, title = { Text(stringResource(R.string.title_permission_grant)) }, text = { Text(stringResource(R.string.msg_permission_grant)) }, properties = DialogProperties( dismissOnBackPress = false, dismissOnClickOutside = false ) ) } } @OptIn(ExperimentalPermissionsApi::class) @Composable private fun ExternalStorageDialog() { val writeStorageState = rememberPermissionState(Manifest.permission.WRITE_EXTERNAL_STORAGE) if (!writeStorageState.status.isGranted) { AlertDialog( onDismissRequest = {}, confirmButton = { Button(onClick = writeStorageState::launchPermissionRequest) { Text(stringResource(R.string.action_confirm)) } }, title = { Text(stringResource(R.string.title_permission_grant)) }, text = { Text(stringResource(R.string.msg_permission_grant)) }, properties = DialogProperties( dismissOnBackPress = false, dismissOnClickOutside = false ) ) } } ================================================ FILE: app/src/main/java/dev/beefers/vendetta/manager/ui/widgets/home/Commit.kt ================================================ package dev.beefers.vendetta.manager.ui.widgets.home 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.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.shape.CircleShape import androidx.compose.material3.LocalContentColor import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.platform.LocalUriHandler import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import coil.compose.AsyncImage import dev.beefers.vendetta.manager.network.dto.Commit import kotlinx.datetime.toJavaInstant import java.text.SimpleDateFormat import java.util.Date @Composable fun Commit( commit: Commit ) { val uriHandler = LocalUriHandler.current Column( verticalArrangement = Arrangement.spacedBy(10.dp), modifier = Modifier .fillMaxWidth() .clickable { uriHandler.openUri(commit.url) } .padding(16.dp) ) { Row( horizontalArrangement = Arrangement.spacedBy(8.dp), verticalAlignment = Alignment.CenterVertically ) { Row( horizontalArrangement = Arrangement.spacedBy(10.dp), verticalAlignment = Alignment.CenterVertically ) { AsyncImage( model = commit.author?.avatar ?: "https://github.com/ghost.png", contentDescription = commit.author?.username ?: "ghost", modifier = Modifier .size(20.dp) .clip(CircleShape) ) Text( text = commit.author?.username ?: "ghost", style = MaterialTheme.typography.labelMedium ) } Text( "•", style = MaterialTheme.typography.labelLarge ) Text( text = commit.sha.substring(0, 7), style = MaterialTheme.typography.labelMedium, color = MaterialTheme.colorScheme.primary, fontFamily = FontFamily.Monospace ) Text( text = SimpleDateFormat .getDateInstance(SimpleDateFormat.SHORT) .format(Date.from(commit.info.committer.date.toJavaInstant())), style = MaterialTheme.typography.labelMedium, color = LocalContentColor.current.copy(alpha = 0.5f), textAlign = TextAlign.End, modifier = Modifier.weight(1f) ) } Text( text = commit.info.message.split("\n").first(), style = MaterialTheme.typography.labelLarge ) } } ================================================ FILE: app/src/main/java/dev/beefers/vendetta/manager/ui/widgets/home/CommitList.kt ================================================ package dev.beefers.vendetta.manager.ui.widgets.home import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box 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.foundation.lazy.LazyColumn import androidx.compose.material3.Button import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import androidx.paging.LoadState import androidx.paging.compose.LazyPagingItems import dev.beefers.vendetta.manager.R import dev.beefers.vendetta.manager.network.dto.Commit import dev.beefers.vendetta.manager.utils.itemsIndexed @Composable fun CommitList( commits: LazyPagingItems ) { val loading = commits.loadState.append is LoadState.Loading || commits.loadState.refresh is LoadState.Loading val failed = commits.loadState.append is LoadState.Error || commits.loadState.refresh is LoadState.Error LazyColumn { itemsIndexed( items = commits, key = { _, commit -> commit.sha } ) { i, commit -> Column { Commit(commit = commit) if (i < commits.itemSnapshotList.lastIndex) { HorizontalDivider( modifier = Modifier.padding(horizontal = 16.dp), thickness = 0.5.dp, color = MaterialTheme.colorScheme.outline.copy(alpha = 0.3f) ) } } } if (loading) { item { Box( contentAlignment = Alignment.TopCenter, modifier = Modifier .fillMaxWidth() .padding(16.dp) ) { CircularProgressIndicator( strokeWidth = 3.dp, modifier = Modifier.size(30.dp) ) } } } if (failed) { item { Column( verticalArrangement = Arrangement.spacedBy(12.dp), horizontalAlignment = Alignment.CenterHorizontally, modifier = Modifier .fillMaxWidth() .padding(16.dp) ) { Text( text = stringResource(R.string.msg_load_fail), style = MaterialTheme.typography.labelLarge, textAlign = TextAlign.Center ) Button(onClick = { commits.retry() }) { Text(stringResource(R.string.action_retry)) } } } } } } ================================================ FILE: app/src/main/java/dev/beefers/vendetta/manager/ui/widgets/installer/LogLine.kt ================================================ package dev.beefers.vendetta.manager.ui.widgets.installer import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.background import androidx.compose.foundation.combinedClickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.padding import androidx.compose.material3.LocalContentColor import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.luminance import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import dev.beefers.vendetta.manager.R import dev.beefers.vendetta.manager.installer.util.LogEntry import dev.beefers.vendetta.manager.utils.thenIf /** * UI for a log entry, displays the level and message and can be expanded with a click to show the timestamp * * @param log The log to display * @param alternateBackground Whether or not to use the alternating background * @param wrapText Whether or not the message should have line wrapping * @param logPadding Force message to contain this many characters, used to make all lines equal in length * @param onLongClick Action to take when long clicking, should just copy the log to clipboard * @param modifier [Modifier] for this component */ @Composable @OptIn(ExperimentalFoundationApi::class) fun LogLine( log: LogEntry, alternateBackground: Boolean, wrapText: Boolean, logPadding: Int, onLongClick: () -> Unit, modifier: Modifier = Modifier ) { var expanded by remember { mutableStateOf(false) } val levelColor = when (log.level) { LogEntry.Level.DEBUG -> Color(0xFF10AF6F) // Green LogEntry.Level.INFO -> if (MaterialTheme.colorScheme.background.luminance() >= 0.5f) Color.Black else Color.White LogEntry.Level.ERROR -> MaterialTheme.colorScheme.error } Row ( verticalAlignment = Alignment.CenterVertically, modifier = modifier .combinedClickable( onLongClickLabel = stringResource(R.string.action_copy_log), onLongClick = onLongClick, onClickLabel = stringResource(R.string.action_show_timestamp), onClick = { expanded = !expanded } ) .thenIf(alternateBackground) { // Alternate background on each line background(MaterialTheme.colorScheme.secondaryContainer.copy(alpha = 0.15f)) } .thenIf(log.level == LogEntry.Level.ERROR) { background(MaterialTheme.colorScheme.errorContainer.copy(alpha = 0.3f)) } .padding(vertical = 4.5.dp, horizontal = 16.dp) ) { Text( text = "[${log.level.name[0]}] ", color = levelColor.copy(alpha = 0.5f), fontSize = 11.sp, fontFamily = FontFamily.Monospace ) Column( verticalArrangement = Arrangement.spacedBy(3.dp) ) { Text( text = log.message.padEnd(logPadding), softWrap = wrapText, fontSize = 13.sp, fontFamily = FontFamily.Monospace, color = LocalContentColor.current.copy(alpha = if (log.level == LogEntry.Level.DEBUG) 0.5f else 0.85f) ) if (expanded) { Text( text = log.formatTimestamp(), softWrap = false, fontSize = 11.sp, fontFamily = FontFamily.Monospace, color = LocalContentColor.current.copy(alpha = 0.5f) ) } } } } ================================================ FILE: app/src/main/java/dev/beefers/vendetta/manager/ui/widgets/installer/StepGroupCard.kt ================================================ package dev.beefers.vendetta.manager.ui.widgets.installer import androidx.compose.animation.AnimatedVisibility import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.KeyboardArrowDown import androidx.compose.material.icons.filled.KeyboardArrowUp import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.material3.surfaceColorAtElevation import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import dev.beefers.vendetta.manager.R import dev.beefers.vendetta.manager.installer.step.Step import dev.beefers.vendetta.manager.installer.step.StepStatus import dev.beefers.vendetta.manager.installer.step.download.base.DownloadStep import dev.beefers.vendetta.manager.utils.thenIf /** * Collapsable card containing a group of steps * * @param name The name of this group * @param isCurrent Whether this card is expanded * @param steps The steps belonging to this group * @param onClick Action taken when the header of the group is clicked */ @Composable fun StepGroupCard( name: String, isCurrent: Boolean, steps: List, onClick: () -> Unit ) { val status = when { steps.all { it.status == StepStatus.QUEUED } -> StepStatus.QUEUED steps.all { it.status == StepStatus.SUCCESSFUL } -> StepStatus.SUCCESSFUL steps.any { it.status == StepStatus.ONGOING } -> StepStatus.ONGOING else -> StepStatus.UNSUCCESSFUL } Column( modifier = Modifier .fillMaxWidth() .clip(RoundedCornerShape(16.dp)) .thenIf(isCurrent) { background(MaterialTheme.colorScheme.surfaceColorAtElevation(1.dp)) } ) { Row( verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(12.dp), modifier = Modifier .clickable(onClick = onClick) .fillMaxWidth() .padding(16.dp) ) { StepIcon(status, 24.dp, progress = null) Text(text = name) Spacer(modifier = Modifier.weight(1f)) if (status != StepStatus.ONGOING && status != StepStatus.QUEUED) { Text( text = "%.2fs".format(steps.sumOf { it.durationMs } / 1000f), // Displays the duration rounded to the hundredths place. ex. 10.13s style = MaterialTheme.typography.labelMedium ) } val (arrow, cd) = when { isCurrent -> Icons.Filled.KeyboardArrowUp to R.string.action_collapse else -> Icons.Filled.KeyboardArrowDown to R.string.action_expand } Icon( imageVector = arrow, contentDescription = stringResource(cd) ) } AnimatedVisibility(visible = isCurrent) { Column( verticalArrangement = Arrangement.spacedBy(16.dp), modifier = Modifier .background(MaterialTheme.colorScheme.background.copy(0.6f)) .fillMaxWidth() .padding(16.dp) .padding(start = 4.dp) ) { steps.forEach { step -> StepRow( name = stringResource(step.nameRes), status = step.status, progress = step.progress, cached = (step as? DownloadStep)?.cached ?: false, duration = step.durationMs / 1000f ) } } } } } ================================================ FILE: app/src/main/java/dev/beefers/vendetta/manager/ui/widgets/installer/StepIcon.kt ================================================ package dev.beefers.vendetta.manager.ui.widgets.installer import androidx.compose.foundation.layout.size 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.CircularProgressIndicator import androidx.compose.material3.Icon import androidx.compose.material3.LocalContentColor import androidx.compose.material3.MaterialTheme 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.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.semantics.contentDescription import androidx.compose.ui.semantics.semantics import androidx.compose.ui.unit.Dp import dev.beefers.vendetta.manager.R import dev.beefers.vendetta.manager.installer.step.StepStatus import kotlin.math.floor import kotlin.math.roundToInt /** * Icon representing the status of a step * * Ongoing - Progress indicator * * Queued - Outlined circle, tinted grey * * Successful - Green check * * Unsuccessful - Red X */ @Composable fun StepIcon( status: StepStatus, size: Dp, progress: Float? ) { val strokeWidth = Dp(floor(size.value / 10) + 1) val context = LocalContext.current when (status) { StepStatus.ONGOING -> { if(progress != null) { CircularProgressIndicator( progress = { progress }, modifier = Modifier .size(size) .semantics { contentDescription = "${(progress * 100).roundToInt()}%" }, strokeWidth = strokeWidth, trackColor = ProgressIndicatorDefaults.circularIndeterminateTrackColor, ) } else { CircularProgressIndicator( strokeWidth = strokeWidth, modifier = Modifier .size(size) .semantics { contentDescription = context.getString(R.string.status_ongoing) } ) } } StepStatus.SUCCESSFUL -> { Icon( imageVector = Icons.Filled.CheckCircle, contentDescription = stringResource(R.string.status_successful), tint = Color(0xFF59B463), modifier = Modifier.size(size) ) } StepStatus.UNSUCCESSFUL -> { Icon( imageVector = Icons.Filled.Cancel, contentDescription = stringResource(R.string.status_fail), tint = MaterialTheme.colorScheme.error, modifier = Modifier.size(size) ) } StepStatus.QUEUED -> { Icon( imageVector = Icons.Outlined.Circle, contentDescription = stringResource(R.string.status_queued), tint = LocalContentColor.current.copy(alpha = 0.4f), modifier = Modifier.size(size) ) } } } ================================================ FILE: app/src/main/java/dev/beefers/vendetta/manager/ui/widgets/installer/StepRow.kt ================================================ package dev.beefers.vendetta.manager.ui.widgets.installer import androidx.compose.animation.core.animateFloatAsState import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Row 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.text.font.FontStyle import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import dev.beefers.vendetta.manager.R import dev.beefers.vendetta.manager.installer.step.StepStatus /** * Displays a steps name, status, and progress * * @param name The name of the step * @param status The steps current status * @param progress Represents the download progress, as a decimal * @param cached Whether the file this step downloads was already cached * @param duration How long the step took to run, in seconds * @param modifier [Modifier] for this StepRow */ @Composable fun StepRow( name: String, status: StepStatus, progress: Float?, cached: Boolean, duration: Float, modifier: Modifier = Modifier ) { Row( verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(16.dp), modifier = modifier ) { @Suppress("LocalVariableName") val _progress by animateFloatAsState(progress ?: 0f, label = "Progress") // Smoothly animate the progress indicator StepIcon(status, size = 18.dp, progress = if(progress == null) null else _progress) Text( text = name, style = MaterialTheme.typography.labelLarge, maxLines = 1, overflow = TextOverflow.Ellipsis, modifier = Modifier.weight(1f, true), ) if (status != StepStatus.ONGOING && status != StepStatus.QUEUED) { // Only display for completed steps if (cached) { Text( text = stringResource(R.string.installer_cached), style = MaterialTheme.typography.labelSmall, color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.6f), fontStyle = FontStyle.Italic, fontSize = 11.sp, maxLines = 1, ) } Text( text = "%.2fs".format(duration), // Displays the duration rounded to the hundredths place. ex. 10.13s style = MaterialTheme.typography.labelSmall, maxLines = 1, ) } } } ================================================ FILE: app/src/main/java/dev/beefers/vendetta/manager/ui/widgets/libraries/LibraryItem.kt ================================================ package dev.beefers.vendetta.manager.ui.widgets.libraries import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.combinedClickable 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.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.material.icons.Icons import androidx.compose.material.icons.outlined.Balance import androidx.compose.material.icons.outlined.Info import androidx.compose.material.icons.outlined.Person import androidx.compose.material3.LocalContentColor import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.material3.surfaceColorAtElevation import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalUriHandler import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import com.mikepenz.aboutlibraries.entity.Library import com.mikepenz.aboutlibraries.util.author import dev.beefers.vendetta.manager.R import dev.beefers.vendetta.manager.ui.components.Label import dev.beefers.vendetta.manager.utils.contentDescription @Composable @OptIn(ExperimentalLayoutApi::class, ExperimentalFoundationApi::class) fun LibraryItem( library: Library ) { val linkHandler = LocalUriHandler.current var licenseSheetOpened by remember { mutableStateOf(false) } if (licenseSheetOpened && library.licenses.firstOrNull()?.licenseContent != null) { LicenseBottomSheet( license = library.licenses.first(), libraryName = library.name, onDismiss = { licenseSheetOpened = false } ) } Column( verticalArrangement = Arrangement.spacedBy(6.dp), modifier = Modifier .combinedClickable( onClick = { licenseSheetOpened = true }, onLongClick = { library.website?.let { linkHandler.openUri(it) } } ) .fillMaxWidth() .padding(16.dp) ) { Text( text = library.name, style = MaterialTheme.typography.bodyMedium, fontSize = 15.sp, fontWeight = FontWeight.Bold ) if (!library.description.isNullOrBlank()) { Text( text = library.description!!, style = MaterialTheme.typography.bodyMedium, fontSize = 15.sp, color = LocalContentColor.current.copy(alpha = 0.7f) ) } FlowRow( horizontalArrangement = Arrangement.spacedBy(6.dp), verticalArrangement = Arrangement.spacedBy(10.dp), modifier = Modifier.padding(top = 4.dp) ) { val labelColor = MaterialTheme.colorScheme.surfaceColorAtElevation(3.dp) library.artifactVersion?.let { Label( text = "v$it", icon = Icons.Outlined.Info, textColor = MaterialTheme.colorScheme.onSurface, borderColor = labelColor, fillColor = labelColor ) } if (library.author.isNotBlank()) { Label( text = library.author, icon = Icons.Outlined.Person, textColor = MaterialTheme.colorScheme.onSurface, borderColor = labelColor, fillColor = labelColor, modifier = Modifier.contentDescription( R.string.cd_library_author, library.author, merge = true ) ) } library.licenses.forEach { license -> Label( text = license.name, icon = Icons.Outlined.Balance, textColor = MaterialTheme.colorScheme.onSurface, borderColor = labelColor, fillColor = labelColor, modifier = Modifier.contentDescription( R.string.cd_library_license, license.name, merge = true ) ) } } } } ================================================ FILE: app/src/main/java/dev/beefers/vendetta/manager/ui/widgets/libraries/LicenseBottomSheet.kt ================================================ package dev.beefers.vendetta.manager.ui.widgets.libraries import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.text.selection.SelectionContainer import androidx.compose.foundation.verticalScroll 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.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import com.mikepenz.aboutlibraries.entity.License import dev.beefers.vendetta.manager.utils.DimenUtils @OptIn(ExperimentalMaterial3Api::class) @Composable fun LicenseBottomSheet( license: License, libraryName: String, onDismiss: () -> Unit ) { ModalBottomSheet( onDismissRequest = onDismiss, contentWindowInsets = { WindowInsets(0, 0, 0, 0) } ) { Column( verticalArrangement = Arrangement.spacedBy(12.dp), modifier = Modifier .padding(16.dp) .padding(bottom = DimenUtils.navBarPadding) ) { Text( text = license.name, style = MaterialTheme.typography.headlineMedium, textAlign = TextAlign.Center, modifier = Modifier.fillMaxWidth() ) Text( text = libraryName, style = MaterialTheme.typography.titleSmall, textAlign = TextAlign.Center, modifier = Modifier.fillMaxWidth() ) Spacer(Modifier) SelectionContainer { Text( text = license.licenseContent!!, style = MaterialTheme.typography.bodySmall, fontFamily = FontFamily.Monospace, color = MaterialTheme.colorScheme.onSecondaryContainer, modifier = Modifier .clip(RoundedCornerShape(12.dp)) .background(MaterialTheme.colorScheme.secondaryContainer) .verticalScroll(rememberScrollState()) .padding(12.dp) ) } } } } ================================================ FILE: app/src/main/java/dev/beefers/vendetta/manager/ui/widgets/settings/ThemePicker.kt ================================================ package dev.beefers.vendetta.manager.ui.widgets.settings import android.annotation.SuppressLint import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height 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.foundation.selection.selectableGroup import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.outlined.NavigateBefore import androidx.compose.material.icons.automirrored.outlined.NavigateNext import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.material3.darkColorScheme import androidx.compose.material3.dynamicDarkColorScheme import androidx.compose.material3.dynamicLightColorScheme import androidx.compose.material3.lightColorScheme import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import dev.beefers.vendetta.manager.R import dev.beefers.vendetta.manager.domain.manager.PreferenceManager import dev.beefers.vendetta.manager.domain.manager.Theme import dev.beefers.vendetta.manager.utils.contentDescription import dev.beefers.vendetta.manager.utils.thenIf import kotlinx.coroutines.launch @Composable @SuppressLint("NewApi") // Dynamic color option shouldn't ever be enabled on unsupported apis anyways fun ThemePicker( prefs: PreferenceManager ) { val context = LocalContext.current val pagerState = rememberPagerState(prefs.theme.ordinal) { Theme.entries.size } val scope = rememberCoroutineScope() val lightScheme = remember(prefs.monet) { if(prefs.monet) dynamicLightColorScheme(context) else lightColorScheme() } val darkScheme = remember(prefs.monet) { if(prefs.monet) dynamicDarkColorScheme(context) else darkColorScheme() } val systemTheme = if(isSystemInDarkTheme()) darkScheme else lightScheme Column( verticalArrangement = Arrangement.spacedBy(16.dp) ) { Box { HorizontalPager( state = pagerState, contentPadding = PaddingValues(16.dp) ) { page -> val (colors, theme) = when (page) { 0 -> systemTheme to Theme.SYSTEM 1 -> lightScheme to Theme.LIGHT 2 -> darkScheme to Theme.DARK else -> systemTheme to Theme.SYSTEM } Box( contentAlignment = Alignment.Center, modifier = Modifier.fillMaxWidth() ) { Column( horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.spacedBy(12.dp), modifier = Modifier .clip(RoundedCornerShape(16.dp)) .thenIf(prefs.theme == theme) { background(MaterialTheme.colorScheme.tertiaryContainer) } .clickable { prefs.theme = theme } .padding(16.dp) ) { ThemePreview( colorScheme = colors, modifier = Modifier.contentDescription(theme.labelRes, merge = true) ) Text( text = stringResource(theme.labelRes), style = MaterialTheme.typography.labelLarge, fontSize = 16.sp ) } } } if (pagerState.currentPage > 0) { // Not first page IconButton( onClick = { scope.launch { pagerState.animateScrollToPage(pagerState.currentPage - 1) } }, modifier = Modifier.align(Alignment.CenterStart) ) { Icon( imageVector = Icons.AutoMirrored.Outlined.NavigateBefore, contentDescription = stringResource(R.string.action_previous_theme), modifier = Modifier.size(34.dp) ) } } if (pagerState.currentPage < 2) { // Not last page IconButton( onClick = { scope.launch { pagerState.animateScrollToPage(pagerState.currentPage + 1) } }, modifier = Modifier.align(Alignment.CenterEnd) ) { Icon( imageVector = Icons.AutoMirrored.Outlined.NavigateNext, contentDescription = stringResource(R.string.action_next_theme), modifier = Modifier.size(34.dp) ) } } } Row( verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(32.dp, Alignment.CenterHorizontally), modifier = Modifier .fillMaxWidth() .height(80.dp) .selectableGroup() ) { ThemePickerOption( theme = Theme.SYSTEM, colors = systemTheme, page = 0, pagerState = pagerState, prefs = prefs ) ThemePickerOption( theme = Theme.LIGHT, colors = lightScheme, page = 1, pagerState = pagerState, prefs = prefs ) ThemePickerOption( theme = Theme.DARK, colors = darkScheme, page = 2, pagerState = pagerState, prefs = prefs ) } } } ================================================ FILE: app/src/main/java/dev/beefers/vendetta/manager/ui/widgets/settings/ThemePickerOption.kt ================================================ package dev.beefers.vendetta.manager.ui.widgets.settings import androidx.compose.animation.animateContentSize import androidx.compose.animation.core.tween import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.background import androidx.compose.foundation.border import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.size import androidx.compose.foundation.pager.PagerState import androidx.compose.foundation.selection.selectable import androidx.compose.foundation.shape.CircleShape import androidx.compose.material.icons.Icons import androidx.compose.material.icons.outlined.Sync import androidx.compose.material3.ColorScheme import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.unit.dp import dev.beefers.vendetta.manager.domain.manager.PreferenceManager import dev.beefers.vendetta.manager.domain.manager.Theme import dev.beefers.vendetta.manager.utils.contentDescription import kotlinx.coroutines.launch @Composable fun ThemePickerOption( theme: Theme, colors: ColorScheme, page: Int, pagerState: PagerState, prefs: PreferenceManager ) { val scope = rememberCoroutineScope() val isSelected = pagerState.currentPage == page Column( verticalArrangement = Arrangement.spacedBy(10.dp), horizontalAlignment = Alignment.CenterHorizontally, modifier = Modifier .animateContentSize(animationSpec = tween()) ) { if (isSelected) { Box( modifier = Modifier .size(8.dp) .background( color = MaterialTheme.colorScheme.secondary, shape = CircleShape ) ) } Box( contentAlignment = Alignment.Center, modifier = Modifier .clip(CircleShape) .background(colors.background) .size(50.dp) .contentDescription(theme.labelRes, merge = true) .selectable(prefs.theme == theme) { prefs.theme = theme scope.launch { pagerState.animateScrollToPage(page) } } .run { if (prefs.theme == theme) border(BorderStroke(4.dp, MaterialTheme.colorScheme.tertiary), CircleShape) else border(BorderStroke(2.dp, colors.inverseSurface), CircleShape) } ) { if (theme == Theme.SYSTEM) { Icon( imageVector = Icons.Outlined.Sync, contentDescription = null, // Covered by parent component tint = colors.onSurface ) } } } } ================================================ FILE: app/src/main/java/dev/beefers/vendetta/manager/ui/widgets/settings/ThemePreview.kt ================================================ package dev.beefers.vendetta.manager.ui.widgets.settings import androidx.compose.foundation.Image import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.height import androidx.compose.material3.ColorScheme import androidx.compose.material3.Icon import androidx.compose.material3.contentColorFor import androidx.compose.material3.surfaceColorAtElevation 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.luminance import androidx.compose.ui.res.painterResource import androidx.compose.ui.unit.dp import dev.beefers.vendetta.manager.R import dev.beefers.vendetta.manager.domain.manager.PreferenceManager import dev.beefers.vendetta.manager.utils.DiscordVersion import org.koin.compose.koinInject @Composable fun ThemePreview( colorScheme: ColorScheme, modifier: Modifier = Modifier ) { val prefs: PreferenceManager = koinInject() val light = colorScheme.background.luminance() > 0.5f val layerModifier = Modifier.height(300.dp) val iconColor = remember(prefs.patchIcon, prefs.channel) { when { prefs.patchIcon -> Color(0xFF48488B) prefs.channel == DiscordVersion.Type.ALPHA -> Color(0xFFFBB33C) else -> Color(0xFF5865F2) } } Box( modifier = modifier ) { Icon( painter = painterResource(R.drawable.ts_bg), contentDescription = null, tint = colorScheme.background, modifier = layerModifier ) Icon( painter = painterResource(R.drawable.ts_surface_l2), contentDescription = null, tint = colorScheme.surfaceColorAtElevation(2.dp), modifier = layerModifier ) Icon( painter = painterResource(R.drawable.ts_surface_l1), contentDescription = null, tint = colorScheme.surfaceColorAtElevation(1.dp), modifier = layerModifier ) Icon( painter = painterResource(R.drawable.ts_outline), contentDescription = null, tint = colorScheme.outline.copy(alpha = 0.3f), modifier = layerModifier ) Image( painter = painterResource(R.drawable.ts_avatars), contentDescription = null, modifier = layerModifier ) Icon( painter = painterResource(R.drawable.ts_primary), contentDescription = null, tint = colorScheme.primary, modifier = layerModifier ) Icon( painter = painterResource(R.drawable.ts_content_50), contentDescription = null, tint = colorScheme.contentColorFor(colorScheme.background).copy(alpha = 0.5f), modifier = layerModifier ) Icon( painter = painterResource(R.drawable.ts_content), contentDescription = null, tint = colorScheme.contentColorFor(colorScheme.background), modifier = layerModifier ) Icon( painter = painterResource(R.drawable.ts_icon), contentDescription = null, tint = iconColor, modifier = layerModifier ) Icon( painter = painterResource(R.drawable.ts_status), contentDescription = null, tint = if(light) Color(0xFF686568) else Color.White, modifier = layerModifier ) } } ================================================ FILE: app/src/main/java/dev/beefers/vendetta/manager/ui/widgets/updater/UpdateDialog.kt ================================================ package dev.beefers.vendetta.manager.ui.widgets.updater import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.size import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.SystemUpdate import androidx.compose.material3.AlertDialog import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.FilledTonalButton import androidx.compose.material3.Icon import androidx.compose.material3.LocalContentColor import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import androidx.compose.ui.window.DialogProperties import dev.beefers.vendetta.manager.R import dev.beefers.vendetta.manager.network.dto.Release @Composable fun UpdateDialog( release: Release, isUpdating: Boolean, onDismiss: () -> Unit, onConfirm: () -> Unit ) { AlertDialog( properties = DialogProperties( dismissOnBackPress = true, dismissOnClickOutside = true ), onDismissRequest = { onDismiss() }, confirmButton = { FilledTonalButton( onClick = onConfirm, enabled = !isUpdating ) { Box { Text( text = stringResource(R.string.action_start_update), color = if (isUpdating) Color.Transparent else LocalContentColor.current ) if (isUpdating) { CircularProgressIndicator( strokeWidth = 3.dp, modifier = Modifier .size(24.dp) .align(Alignment.Center) ) } } } }, title = { Text(stringResource(R.string.title_update)) }, text = { Text( text = stringResource(R.string.update_description, release.versionName), textAlign = TextAlign.Center ) }, icon = { Icon( imageVector = Icons.Filled.SystemUpdate, contentDescription = null ) } ) } ================================================ FILE: app/src/main/java/dev/beefers/vendetta/manager/updatechecker/reciever/UpdateBroadcastReceiver.kt ================================================ package dev.beefers.vendetta.manager.updatechecker.reciever import android.app.NotificationManager import android.app.PendingIntent import android.content.BroadcastReceiver import android.content.Context import android.content.Intent import androidx.core.app.NotificationCompat import dev.beefers.vendetta.manager.BuildConfig import dev.beefers.vendetta.manager.R import dev.beefers.vendetta.manager.utils.Channels import dev.beefers.vendetta.manager.utils.DiscordVersion import dev.beefers.vendetta.manager.utils.Intents class UpdateBroadcastReceiver : BroadcastReceiver() { override fun onReceive(context: Context, intent: Intent) { val nm = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager val versionCode = intent.getStringExtra(Intents.Extras.VERSION) ?: return val version = DiscordVersion.fromVersionCode(versionCode) ?: return val notificationId = versionCode.toInt() val clickIntent = PendingIntent.getActivity( context, notificationId, context.packageManager.getLaunchIntentForPackage(BuildConfig.APPLICATION_ID)!!.apply { action = Intents.Actions.INSTALL putExtra(Intents.Extras.VERSION, versionCode) addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) }, PendingIntent.FLAG_IMMUTABLE ) NotificationCompat.Builder(context, Channels.UPDATE).apply { setSmallIcon(R.drawable.ic_update) setContentTitle(context.getString(R.string.title_update_available)) setContentText(context.getString(R.string.action_tap_to_install, version.toString())) setContentIntent(clickIntent) setAutoCancel(true) priority = NotificationCompat.PRIORITY_DEFAULT nm.notify(notificationId, build()) } } } ================================================ FILE: app/src/main/java/dev/beefers/vendetta/manager/updatechecker/worker/UpdateWorker.kt ================================================ package dev.beefers.vendetta.manager.updatechecker.worker import android.content.Context import android.content.Intent import androidx.work.CoroutineWorker import androidx.work.WorkerParameters import dev.beefers.vendetta.manager.domain.manager.InstallManager import dev.beefers.vendetta.manager.domain.manager.PreferenceManager import dev.beefers.vendetta.manager.domain.repository.RestRepository import dev.beefers.vendetta.manager.network.utils.ApiResponse import dev.beefers.vendetta.manager.updatechecker.reciever.UpdateBroadcastReceiver import dev.beefers.vendetta.manager.utils.DiscordVersion import dev.beefers.vendetta.manager.utils.Intents import org.koin.core.component.KoinComponent import org.koin.core.component.inject class UpdateWorker( private val context: Context, params: WorkerParameters ) : CoroutineWorker(context, params), KoinComponent { val api: RestRepository by inject() val prefs: PreferenceManager by inject() val installManager: InstallManager by inject() override suspend fun doWork(): Result { if (prefs.discordVersion.isNotBlank()) return Result.success() return when (val res = api.getLatestDiscordVersions()) { is ApiResponse.Success -> { val currentVersion = DiscordVersion.fromVersionCode(installManager.current?.longVersionCode.toString()) val latestVersion = res.data[prefs.channel] if (latestVersion == null || currentVersion == null) return Result.failure() if (latestVersion > currentVersion && latestVersion.isSupported()) { context.sendBroadcast( Intent( context, UpdateBroadcastReceiver::class.java ).apply { putExtra(Intents.Extras.VERSION, latestVersion.toVersionCode()) }) } Result.success() } else -> Result.failure() } } } ================================================ FILE: app/src/main/java/dev/beefers/vendetta/manager/utils/Constants.kt ================================================ package dev.beefers.vendetta.manager.utils import android.os.Environment import dev.beefers.vendetta.manager.BuildConfig object Constants { val TEAM_MEMBERS = listOf( TeamMember("Pylix", "Developer - Bunny & Vendetta", "pylixonly"), TeamMember("Kasi", "Developer - Xposed Module", "redstonekasi") ) // NOTE: This is no longer used val VENDETTA_DIR = Environment.getExternalStorageDirectory().resolve("Bunny") val DUMMY_VERSION = DiscordVersion(1, 0, DiscordVersion.Type.STABLE) val START_TIME = System.currentTimeMillis() } object Intents { object Actions { const val INSTALL = "${BuildConfig.APPLICATION_ID}.intents.actions.INSTALL" } object Extras { const val VERSION = "${BuildConfig.APPLICATION_ID}.intents.extras.VERSION" } } object Channels { const val UPDATE = "${BuildConfig.APPLICATION_ID}.notifications.UPDATE" } data class TeamMember( val name: String, val role: String, val username: String = name ) ================================================ FILE: app/src/main/java/dev/beefers/vendetta/manager/utils/Context.kt ================================================ package dev.beefers.vendetta.manager.utils import android.content.Context import android.util.TypedValue import androidx.annotation.AnyRes import java.io.InputStream /** * Get the raw bytes for a resource. * @param id The resource identifier * @return The resource's raw bytes as stored inside the APK */ fun Context.getResBytes(@AnyRes id: Int): ByteArray { val tValue = TypedValue() this.resources.getValue( /* id = */ id, /* outValue = */ tValue, /* resolveRefs = */ true, ) val resPath = tValue.string.toString() return this.javaClass.classLoader ?.getResourceAsStream(resPath) ?.use(InputStream::readBytes) ?: error("Failed to get resource file $resPath from APK") } ================================================ FILE: app/src/main/java/dev/beefers/vendetta/manager/utils/DimenUtils.kt ================================================ package dev.beefers.vendetta.manager.utils import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.asPaddingValues import androidx.compose.foundation.layout.systemBars import androidx.compose.runtime.Composable import androidx.compose.ui.unit.Dp object DimenUtils { val navBarPadding: Dp @Composable get() = WindowInsets.systemBars.asPaddingValues().calculateBottomPadding() } ================================================ FILE: app/src/main/java/dev/beefers/vendetta/manager/utils/LazyUtils.kt ================================================ package dev.beefers.vendetta.manager.utils import androidx.compose.foundation.lazy.LazyListScope import androidx.compose.runtime.Composable import androidx.paging.compose.LazyPagingItems import androidx.paging.compose.itemContentType inline fun LazyListScope.itemsIndexed( items: LazyPagingItems, noinline key: ((i: Int, item: T) -> Any)? = null, crossinline itemContent: @Composable (i: Int, item: T) -> Unit ) { items( count = items.itemCount, key = { i -> key?.invoke(i, items[i]!!) ?: Unit }, contentType = items.itemContentType() ) { itemContent(it, items[it]!!) } } ================================================ FILE: app/src/main/java/dev/beefers/vendetta/manager/utils/ModifierUtils.kt ================================================ package dev.beefers.vendetta.manager.utils import androidx.compose.ui.Modifier import androidx.compose.ui.composed import androidx.compose.ui.res.stringResource import androidx.compose.ui.semantics.contentDescription import androidx.compose.ui.semantics.semantics fun Modifier.contentDescription(description: String, merge: Boolean = false): Modifier = semantics(mergeDescendants = merge) { contentDescription = description } fun Modifier.contentDescription(res: Int, vararg param: Any, merge: Boolean = false): Modifier = composed { contentDescription(stringResource(res, *param), merge) } inline fun Modifier.thenIf(predicate: Boolean, block: Modifier.() -> Modifier): Modifier = if (predicate) then(Modifier.Companion.block()) else this ================================================ FILE: app/src/main/java/dev/beefers/vendetta/manager/utils/NavUtils.kt ================================================ package dev.beefers.vendetta.manager.utils import cafe.adriel.voyager.core.screen.Screen import cafe.adriel.voyager.navigator.Navigator tailrec fun Navigator.navigate(screen: Screen) { if (level == 0) push(screen) else this.parent!!.navigate(screen) } ================================================ FILE: app/src/main/java/dev/beefers/vendetta/manager/utils/Utils.kt ================================================ package dev.beefers.vendetta.manager.utils import android.content.ClipData import android.content.ClipboardManager import android.content.Context import android.graphics.Bitmap import android.net.Uri import android.os.Handler import android.os.Looper import android.widget.Toast import androidx.activity.compose.ManagedActivityResultLauncher import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.result.contract.ActivityResultContracts import androidx.annotation.DrawableRes import androidx.annotation.StringRes import androidx.appcompat.content.res.AppCompatResources import androidx.collection.ObjectList import androidx.compose.runtime.Composable import androidx.compose.ui.platform.LocalContext import androidx.core.graphics.drawable.toBitmap import dev.beefers.vendetta.manager.BuildConfig import java.io.BufferedReader import java.io.IOException import java.io.InputStreamReader val mainThread = Handler(Looper.getMainLooper()) fun mainThread(block: () -> Unit) { mainThread.post(block) } fun Context.copyText(text: String) { val clipboardManager = getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager clipboardManager.setPrimaryClip(ClipData.newPlainText(BuildConfig.APPLICATION_ID, text)) } fun Context.showToast(@StringRes res: Int, vararg params: Any, short: Boolean = true) { Toast.makeText( this, getString(res, *params), if (short) Toast.LENGTH_SHORT else Toast.LENGTH_LONG ).show() } /** * Remembers an activity result launcher used to save files to a user-specified location */ @Composable fun rememberFileSaveLauncher(content: String, mimeType: String = "text/plain"): ManagedActivityResultLauncher { val context = LocalContext.current return rememberLauncherForActivityResult(contract = ActivityResultContracts.CreateDocument(mimeType)) { uri -> uri?.let { context.contentResolver.openOutputStream(uri).use { stream -> stream?.write(content.toByteArray()) } } } } private val cachedBitmaps: MutableMap> = mutableMapOf() context(Context) private val Int.dp: Int get() = (45 * this@Context.resources.displayMetrics.density + 0.5f).toInt() fun Context.getBitmap(@DrawableRes icon: Int, size: Int): Bitmap { cachedBitmaps[icon]?.let { it[size]?.let { bitmap -> return bitmap } } val sizePx = size.dp val bitmap = AppCompatResources.getDrawable(this, icon)!!.toBitmap( height = sizePx, width = sizePx ) cachedBitmaps[icon] = mutableMapOf() cachedBitmaps[icon]?.let { it[size] = bitmap } return bitmap } // Thanks to https://gist.github.com/Muyangmin/e8ec1002c930d8df3df46b306d03315d fun getSystemProp(prop: String): String? { val line: String var input = null as BufferedReader? try { val proc = Runtime.getRuntime().exec("getprop $prop") input = BufferedReader(InputStreamReader(proc.inputStream), 1024) line = input.readLine() input.close() } catch (e: IOException) { return null } finally { input?.let { try { input.close() } catch (e: IOException) { e.printStackTrace() } } } return line } val isMiui: Boolean get() = getSystemProp("ro.miui.ui.version.name")?.isNotEmpty() ?: false inline fun ObjectList.find(block: (E) -> Boolean): E? { forEach { value -> if (block(value)) return value } return null } ================================================ FILE: app/src/main/java/dev/beefers/vendetta/manager/utils/VersionUtils.kt ================================================ package dev.beefers.vendetta.manager.utils import androidx.annotation.StringRes import dev.beefers.vendetta.manager.R import java.io.Serializable import kotlin.text.toCharArray data class DiscordVersion( val major: Int, val minor: Int, val type: Type ) : Serializable, Comparable { enum class Type(val label: String, @StringRes val labelRes: Int, val maxVersionCode: Int) { STABLE("Stable", R.string.channel_stable, 279013), BETA("Beta", R.string.channel_beta, 279112), ALPHA("Alpha", R.string.channel_alpha, 280200) } override fun compareTo(other: DiscordVersion): Int = toVersionCode().toInt() - other.toVersionCode().toInt() override fun toString() = "$major.$minor - ${type.label}" fun toVersionCode() = "$major${type.ordinal}${if (minor < 10) 0 else ""}${minor}" fun isSupported() = toVersionCode().toInt() <= type.maxVersionCode companion object { fun fromVersionCode(string: String): DiscordVersion? = with(string) { if (length < 4) return@with null val versionCodeInt = toIntOrNull() ?: return@with null if (versionCodeInt <= 126021) return@with null val codeReversed = toCharArray().reversed().joinToString("") val typeInt = codeReversed[2].toString().toInt() val type = Type.entries.getOrNull(typeInt) ?: return@with null DiscordVersion( codeReversed.slice(3..codeReversed.lastIndex).reversed().toInt(), codeReversed.substring(0, 2).reversed().toInt(), type ) } } } ================================================ FILE: app/src/main/res/drawable/bunny_logo.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_discord.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_discord_icon.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_github.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_update.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ts_avatars.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ts_bg.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ts_content.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ts_content_50.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ts_icon.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ts_outline.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ts_primary.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ts_status.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ts_surface_l1.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ts_surface_l2.xml ================================================ ================================================ FILE: app/src/main/res/drawable-v26/ic_launcher.xml ================================================ ================================================ FILE: app/src/main/res/values/colors.xml ================================================ #FFBB86FC #FF6200EE #FF3700B3 #FF03DAC5 #FF018786 #FF000000 #FFFFFFFF #FF48488B ================================================ FILE: app/src/main/res/values/strings.xml ================================================ Bunny Manager Author: %1$s License: %1$s Cache cleared successfully Failed to load commits Coming soon Invalid Discord version 7 more taps 5 more taps 2 more taps Loading… Downgrade Cannot downgrade, try uninstalling first You are now a developer In order for Bunny Manager to function, file permissions are required. Since shared data is stored in ~/Bunny, permissions are required in order to access it. Failed to obtain Shizuku permissions Would you like to try again using a download mirror? APK was corrupted, try clearing cache then reinstalling Download was aborted Download failed Could not verify downloaded file, check logs for more details Copied to clipboard Download APKs Patching Installing Downloading base apk Downloading libraries apk Downloading language apk Downloading resources apk Downloading Bunny module Changing app icon Patching app manifests Injecting Bunny Signing APKs Installing APKs Installed successfully Install canceled Failed to install: Unknown reason Installation was blocked One or more APKs were invalid or corrupt Conflicts with an existing app, usually due to mismatched signatures Not enough available storage to install Application is incompatible with this device Installation timed out Ongoing Successful Failed Queued Collapse Expand Go back Copy logs Clear cache Confirm Start update Install Update Reinstall Launch Uninstall Info Retry Reload Open about Open settings Tap to install Bunny for Discord %1$s Exit anyways Nevermind No thanks Try again See previous theme See next theme View logs Show timestamp Copy log Save logs to file Share logs More options Exiting the installer before its finished could corrupt downloaded files, are you sure you want to do that? Cached Installer Home Settings Update available! About Grant Permissions New Discord update available! Warning Download failed Logs Open source libraries Libraries System Light Dark Disabled Quarter hourly Half hourly Hourly Bi-hourly Twice daily Daily Weekly Appearance Change the look and feel of the app Dynamic color Only available on Android 12 and up Theme Check for Discord updates Customization Customize your Bunny install App name Replace app icon Uses the Bunny icon instead of Discord\'s Advanced Modify downloading and installation Release channel Download mirror Clear cache automatically Clears cache when Discord gets an update Developer only These settings may break things if used improperly Module location For use with developing the Xposed module. BE CAREFUL Reset module location Package name Discord version Debuggable Enable debuggable flag Allow downgrading Bypass downgrade check by Bunny Manager, useful if your device already allows downgrading through root Alternate lines Wrap lines Bunny Manager version %1$s is now available! Stable Beta Alpha Github Discord Team Special thanks Translate Latest: %1$s Target: %1$s Current: %1$s Install method PackageManager (recommended) Shizuku ================================================ FILE: app/src/main/res/values/themes.xml ================================================ ================================================ FILE: app/src/main/res/values-af-rZA/strings.xml ================================================ Bunny Bestuurder Author: %1$s License: %1$s Kas is suksesvol skoongemaak Kon nie commits laai nie Kom binnekort Ongeldige Discord-weergawe Nog 7 krane Nog 5 krane Nog 2 krane Laai tans… Kan nie afgradeer nie, probeer eers deïnstalleer Jy is nou \'n ontwikkelaar Vir Bunny Bestuurder om te funksioneer, word lêertoestemmings vereis. Aangesien gedeelde data in ~/Bunny gestoor word, word toestemmings vereis om toegang daartoe te verkry. Failed to obtain Shizuku permissions Wil jy weer probeer met \'n aflaaispieël? APK is korrupteer, probeer om kas skoon te maak en dan weer te installeer Download was aborted Download failed Could not verify downloaded file, check logs for more details Copied to clipboard Laai APK\'s af Pleister Installeer tans Laai tans basis apk af Laai library base apk af Laai tans taalbasis Apk af Laai tans hulpbronne Apk af Laai tans Bunny-module af Verander toepassing-ikoon Patching app manifesteer Inspuiting van Bunny Ondertekening van APK\'s Installeer tans APK\'s Suksesvol geïnstalleer Installering is gekanselleer Kon nie installeer nie: Onbekende rede Installasie is geblokkeer Een of meer APK\'s was ongeldig of korrup Konflik met \'n bestaande toepassing, gewoonlik as gevolg van nie-ooreenstemmende handtekeninge Nie genoeg beskikbare berging om te installeer nie Toepassing is onversoenbaar met hierdie toestel Installasie het uitgetel Deurlopend Suksesvol Misluk In die ry Inval Brei uit Gaan terug Kopieer logs Vee kas uit Bevestig Begin opdatering Installeer Opdateer Herinstalleer Begin Deïnstalleer Inligting Probeer weer Herlaai Maak oop oor Maak instellings oop Tik om Bunny vir Discord %1$s te installeer Verlaat in elk geval Toemaar Nee dankie Probeer weer See previous theme See next theme View logs Show timestamp Copy log Save logs to file Share logs More options As jy die installeerder verlaat voordat dit klaar is, kan afgelaaide lêers beskadig word. Is jy seker jy wil dit doen? Gekas Installeerder Tuis Instellings Opdatering beskikbaar! Oor Gee toestemmings Nuwe Discord-opdatering beskikbaar! Waarskuwing Aflaai het misluk Logs Open source libraries Libraries Stelsel Lig Donker Gestrem Kwartierliks Halfuurliks Uurliks Twee-uurliks Twee keer per dag Daagliks Weekliks Voorkoms Change the look and feel of the app Dinamiese kleur Slegs beskikbaar op Android 12 en hoër Tema Kyk vir Discord-opdaterings Customization Customize your Bunny install Programnaam Vervang app-ikoon Gebruik die Bunny-ikoon in plaas van Discord s\'n Gevorderd Modify downloading and installation Stel kanaal vry Laai spieël af Vee kas outomaties uit Vee kas uit wanneer Discord \'n opdatering kry Slegs ontwikkelaar These settings may break things if used improperly Module ligging Vir gebruik met die ontwikkeling van die Xposed-module. WEES VERSIGTIG Stel module ligging terug Pakketnaam Discord weergawe Ontfoutbaar Aktiveer ontfoutbare vlag Alternate lines Wrap lines Bunny Bestuurder weergawe %1$s is nou beskikbaar! Stabiel Beta Alfa Github Onenigheid Span Spesiale dankie Vertaal Nuutste: %1$s Teiken: %1$s Huidig: %1$s Install method Default (recommended) Shizuku ================================================ FILE: app/src/main/res/values-ar-rSA/strings.xml ================================================ مدير الثأر Author: %1$s License: %1$s تم مسح ذاكرة التخزين المؤقت بنجاح فشل تحميل البيانات سيأتي قريباً إصدار ديسكورد غير صحيح 7 نقرات أخرى 5 نقرات أخرى نقرتان أخران جاري التحميل… لا يمكن رجوع إلى النسخة السابقة، حاول إزالة التثبيت أولاً أنت الآن مبرمِج لكي يعمل Bunny Manager، مطلوب أذونات التخزين. بما أن البيانات المخزنة يتم تخزينها في ~/Bunny، فإن الأذونات مطلوبة الحصول عليها. أخفق الحصول على أذوات Shizuku هل ترغب في التحميل مرة اخري من مصدر بديل؟ ملف الAPK تالف، حاول مسح ذاكرة التخزين المؤقت ثم إعادة التثبيت تم إحباط التنزيل فشل التنزيل تعذر التحقق من المِلَفّ الذي تم تنزيله، راجع السجلات للحصول على مزيد من التفاصيل Copied to clipboard تنزيل برنامج التشغيل للأندرويد (APKs) تصحيح جاري التثبيت تنزيل APK الأساسي تحميل الملفات الأساسية APK تحميل لغة APK تحميل موارد APK تحميل وحدة Bunny تغيير ايقونة التطبيق تصحيح بيانات التطبيق تحقين Bunny توقيع APKs تثبيت APKs تم التثبيت بنجاح تم إلغاء التثبيت فشل في التثبيت: سبب غير معروف تم حظر التثبيت واحد أو أكثر من ال APKs غير صالح أو فاسد التعارض مع تطبيق مثبت، عادة ما يكون ذلك بسبب عدم تطابق التوقيعات لا توجد مساحة تخزين كافية للتثبيت التطبيق غير متوافق مع هذا الجهاز انتهت مهلة تثبيت مستمر تم بنجاح فشلت العملية في قائمة الإنتظار انهدام إظهار رجوع نسخ السجلات مسح ذاكرة التخزين المؤقت تأكيد بدء التحديث تثبيت تحديث إعادة التثبيت تشغيل إزالة التثبيت معلومات إعادة المحاولة إعادة التحميل فتح حول فتح الإعدادات انقر لتثبيت Bunny للديسكورد %1$s الإنهاء على أية حال رجوع لا، شكراً حاول مرة أخرى راجع الموضوع السابق انظر الموضوع التالي View logs Show timestamp Copy log Save logs to file Share logs More options الخروج من المثبت قبل انتهائه يمكن أن يؤدي إلى تلف الملفات التي تم تحميلها، هل أنت متأكد من أنك تريد القيام بذلك؟ ذاكرة التخزين المؤقت المثبت الصفحة الرئيسية الإعدادات تحديث متوفر! حول منح الأذونات تحديث ديسكورد جديد متوفر! تحذير فشل التنزيل Logs Open source libraries Libraries النظام مضيء مظلم معطل ربع ساعة نصف الساعة كل ساعة كل ساعتين مرتين يومياً يومياً إسبوعياً إعدادات المظهر Change the look and feel of the app ألوان حيويّة متوفر فقط على أندرويد 12 وما فوق المظهر تحقق من وجود تحديثات للديسكورد Customization Customize your Bunny install اسم التطبيق استبدال أيقونة التطبيق استخدم أيقونة Bunny بدلا من ديسكورد خيارات متقدمة Modify downloading and installation اصدار القناة تحميل بديل مسح الذاكرة المؤقتة تلقائياً مسح الذاكرة المؤقتة عندما يحصل ديسكورد على تحديث المطوِّر فقط These settings may break things if used improperly موقع الوحدة للاستخدام في تطوير وحدة Xpose. احرص إعادة تعيين موقع الوحدة اسم الباقة إصدار ديسكورد قابل للتصحيح تفعيل علم التصحيح Alternate lines Wrap lines إصدار Bunny Manager %1$s متاح الآن! مستقر تجريبي نسخة أولية Github Discord الفريق شكر خاص الترجمة الإصدار الأخير: %1$s الهدف: %1$s الحالي: %1$s Install method Default (recommended) Shizuku ================================================ FILE: app/src/main/res/values-ca-rES/strings.xml ================================================ Gerent Bunny Author: %1$s License: %1$s La memòria cau s\'ha esborrat correctament No s\'han pogut carregar les confirmacions Pròximament Versió de Discord no vàlida 7 tocs més 5 tocs més 2 tocs més Carregant… No es pot desactualitzar, primer prova de desinstal·lar Ara ets desenvolupador Perquè Bunny Manager funcioni, calen permisos de fitxers. Com que les dades compartides s\'emmagatzemen a ~/Bunny, calen permisos per accedir-hi. Failed to obtain Shizuku permissions Vols tornar-ho a provar amb una baixada? L\'APK s\'ha fet malbé, proveu d\'esborrar la memòria cau i torna-ho a instal·lar Download was aborted Download failed Could not verify downloaded file, check logs for more details Copied to clipboard Baixa APK Patching Instal·lant Baixant l\'apk base Baixant biblioteques apk Baixant l\'apk d\'idioma Baixant l\'apk de recursos Baixant el mòdul Bunny Canviant l\'icona de l\'app Patching app manifests Injectant Bunny Signant APK Instal·lació d\'APK Instal·lat correctament Instal·lació cancelada No s\'ha pogut instal·lar: motiu desconegut La instal·lació s\'ha bloquejat Un o més APK no eren vàlids o malmesos Conflictes amb una aplicació existent, normalment a causa de signatures que no coincideixen No hi ha prou espai disponible per instal·lar-lo L\'aplicació és incompatible amb aquest dispositiu S\'ha esgotat el temps d\'instal·lació En marxa Correcte Error En cua Col·lapse Expandir Torna Copia els registres Netejar cache Confirmar Inicia actualització Instal·lar Actualitzar Reinstal·lar Iniciar Desinstal·lar Informació Torna-ho a provar Recarregar Obre sobre Obre la configuració Toca per instal·lar Bunny per a Discord %1$s Sortiu de totes maneres No importa No gràcies Torna-ho a provar See previous theme See next theme View logs Show timestamp Copy log Save logs to file Share logs More options Sortir de l\'instal·lador abans que s\'acabi podria danyar els fitxers descarregats, esteu segur que voleu fer-ho? Cached Instal·lador Inici Configuració Actualització disponible! Sobre Concedir permisos Nova actualització de Discord disponible! Avís La baixada ha fallat Logs Open source libraries Libraries Sistema Llum Fosc Inhabilitat Quart d\'hora Mitja hora Cada hora Cada dues hores Dues vegades al dia Diàriament Setmanalment Aparença Change the look and feel of the app Color dinàmic Només disponible a Android 12 i posteriors Tema Comprova si hi ha actualitzacions de Discord Customization Customize your Bunny install Nom de l\'aplicació Substitueix la icona de l\'aplicació Utilitza la icona de Bunny en comptes de la de Discord Avançat Modify downloading and installation Canal release Descàrrega mirall Esborra cache automàticament Esborra cache quan Discord rep una actualització Només desenvolupador These settings may break things if used improperly Ubicació del mòdul Per utilitzar-lo amb el desenvolupament del mòdul Xposed. VES AMB COMPTE Restableix la ubicació del mòdul Nom del paquet Versió de Discord Depurable Activa el senyalador depurable Alternate lines Wrap lines La versió %1$s de Bunny Manager ja està disponible! Estable Beta Alpha Github Discord Equip Agraïments especials Traduir Última: %1$s Objectiu: %1$s Actual: %1$s Install method Default (recommended) Shizuku ================================================ FILE: app/src/main/res/values-cs-rCZ/strings.xml ================================================ Správce Vendetty Author: %1$s License: %1$s Mezipaměť úspěšně vymazána Nepodařilo se načíst commity Již brzy Neplatná verze Discordu Ještě 7 klepnutí Ještě 5 klepnutí Ještě 2 klepnutí Načítání… Nelze jít o verzi zpět. Nejdříve zkuste odinstalovat Nyní jste vývojářem Aby Správce Vendetty fungoval, jsou vyžadována oprávnění k přístupu k souborům. Protože jsou sdílená data uložena ve složce ~/Bunny, je pro přístup k ní požadováno oprávnění. Nepodařilo se získat oprávnění pro Shizuku Chcete to zkusit znovu pomocí zrcadla? Soubor APK byl poškozen, zkuste vyčistit paměť cache a poté přeinstalovat Stahování bylo přerušeno Stahování selhalo Could not verify downloaded file, check logs for more details Copied to clipboard Stáhnout soubory APK Vkládání Instalování Stahování základní apk Stahování apk knihoven Stahování apk jazyků Stahování apk zdrojů Stahování modulu Bunny Měnění ikony aplikace Vkládání manifestů aplikace Vkládání Vendetty Podepisování APK souborů Instalování APK souborů Instalace úspěšná Instalace zrušena Instalace se nepovedla: Neznámý důvod Instalace byla zablokována Některé APK soubory byly neplatné nebo požkozené Konflikuje s existující aplikací, obvykle kvůli neshodě podpisů Nedostatek úložiště pro instalaci Aplikace není s tímto zařízením kompatibilní Instalace dosáhla časového limitu Probíhající Úspěšné Selhalo Čekající Sbalit Rozbalit Jít zpět Zkopírovat zápisy Vyčistit mezipaměť Potvrdit Spustit aktualizaci Nainstalovat Aktualizovat Přeinstalovat Spustit Odinstalovat Informace Opakovat Obnovit Otevřít O aplikaci Otevřít nastavení Klepni pro nainstalování Vendetty pro Discord %1$s Přesto ukončit Zrušit Ne, díky Zkusit znovu Zobrazit předchozí motiv Zobrazit další motiv View logs Show timestamp Copy log Save logs to file Share logs More options Opuštění instalátoru před dokončením může poškodit stažené soubory, opravdu chcete pokračovat? V mezipaměti Instalátor Domů Nastavení Dostupná aktualizace! O aplikaci Udělit oprávnění Je k dispozici nová verze Discordu! Pozor Stahování se nezdařilo Záznamy Open source libraries Libraries Systémový Světlý Tmavý Nikdy Každou čtvrt hodinu Každou půl hodinu Každou hodinu Každé dvě hodiny Dvakrát denně Denně Týdně Vzhled Změnit vzhled aplikace Dynamická barva Dostupné jen na Androidu 12 a výš Motiv Kontrolovat pro aktualizace Discordu Přizpůsobení Přizpůsobte si instalaci Vendetty Jméno aplikace Nahradit ikonu aplikace Používá ikonu Bunny místo ikony Discordu Pokročilé Upravit stahování a instalaci Kanál vydání Zrcadlo stahování Automaticky mazat mezipaměť Vymaže mezipaměť když se aktualizuje Discord Pouze pro vývojáře Tyto nastavení mohou při nesprávném použití něco pokazit Umístění modulu Pro použití při programování Xposed modulu. DÁVEJTE SI POZOR Resetovat umístění modulu Jméno balíčku Verze Discordu Laditelné Povolit vlajku laditelné Alternate lines Wrap lines Verze %1$s Správce Vendetty je nyní dostupná! Stabilní Beta Alpha Github Discord Tým Zvláštní poděkování Přeložit Nejnovější: %1$s Cílená: %1$s Aktuální: %1$s Metoda instalace Default (recommended) Shizuku ================================================ FILE: app/src/main/res/values-da-rDK/strings.xml ================================================ Bunny manager Author: %1$s License: %1$s Cache ryddet Kunne ikke indlæse commits Kommer snart Ugyldig Discord version 7 tryk mere 5 tryk mere 2 tryk mere Indlæser… Kan ikke nedgradere, Prøv at afinstallere først Du er nu udvikler For at Bunny Manager kan fungere, kræves filtilladelser. Da delte data er gemt i ~/Bunny, kræves der tilladelser for at få adgang til dem. Failed to obtain Shizuku permissions Vil du prøve igen med et downloadspejl? APK var beskadiget, Prøv at rydde cachen og derefter geninstallere Download was aborted Download failed Could not verify downloaded file, check logs for more details Copied to clipboard Download APK\'er Patching Installerer Henter grund Apk\'en Henter biblioteks Apk\'en Henter sprog Apk\'en Henter ressource Apk\'en Henter Bunny modulet Ændring af app ikon Patching app manifesterer Injicerer Bunny Signering af APK\'er Installerer APK\'er Installeret med succes Installationen blev annulleret Kunne ikke installere: Ukendt årsag Installationen blev blokeret En eller flere APK-filer var ugyldige eller korruptes Konflikter med en eksisterende app, normalt på grund af uoverensstemmende signaturer Ikke nok tilgængelig lagerplads til at installere Applikationen er inkompatibel med denne enhed Installationen udløb Igangværende Vellykket Mislykkedes I kø Bryder sammen Udvide Gå tilbage Kopier logfiler Ryd cache Bekræfte Start opdatering Installere Opdatering Geninstaller Lancering Afinstaller Info Prøve igen Genindlæs Åben om Åbn indstillinger Tryk for at installere Bunny for Discord %1$s Afslut alligevel Glem det Nej tak Prøv igen See previous theme See next theme View logs Show timestamp Copy log Save logs to file Share logs More options Hvis du afslutter installationsprogrammet, før det er færdigt, kan det ødelægge downloadede filer. Er du sikker på, at du vil gøre det? Cachelagret Installatør Hjem Indstillinger Opdatering tilgængelig! Om Giv tilladelser Ny Discord opdatering tilgængelig! Advarsel Download fejlede Logs Open source libraries Libraries System Lys Mørk Handicappet Kvartals time En halv time Hver time Hver time To gange dagligt Daglige Ugentlig Udseende Change the look and feel of the app Dynamisk farve Kun tilgængelig på Android 12 og nyere Tema Se efter Discord opdateringer Customization Customize your Bunny install App navn Udskift app ikon Bruger Bunny ikonet i stedet for Discords Fremskreden Modify downloading and installation Slip kanal Hent spejl Ryd cache automatisk Rydder cachen, når Discord får en opdatering Kun udvikler These settings may break things if used improperly Modulplacering Til brug ved udvikling af Xposed modulet. VÆR FORSIGTIG Nulstil modulplacering Pakkenavn Discord version Fejlbarlig Aktiver flag, der kan fejlfindes Alternate lines Wrap lines Bunny Manager version %1$s er nu tilgængelig! Stabil Beta Alfa Github Discord Hold Specielt tak Oversætte Seneste: %1$s Target: %1$s Current: %1$s Install method Default (recommended) Shizuku ================================================ FILE: app/src/main/res/values-de-rDE/strings.xml ================================================ Bunny Manager Author: %1$s License: %1$s Cache wurde erfolgreich geleert Laden von Commits fehlgeschlagen Demnächst Ungültige Discord-Version Noch 7 mal tippen Noch 5 mal tippen Noch 2 mal tippen Wird geladen… Downgraden nicht möglich, versuche zuerst zu deinstallieren Du bist jetzt ein Entwickler Damit Bunny Manager funktionieren kann, sind Dateiberechtigungen erforderlich. Da freigegebene Daten in ~/Bunny gespeichert sind, sind Berechtigungen erforderlich, um darauf zuzugreifen. Fehler beim Abruf der Shizuku-Berechtigungen Möchten Sie es noch einmal mit einem Download Mirror versuchen? APK wurde beschädigt. versuche den Cache zu leeren und danach neu zu installieren Download abgebrochen Download fehlgeschlagen Konnte nicht die heruntergeladene Datei überprüfen, siehe Logs für mehr Details In die Zwischenablage kopiert APKs herunterladen App wird gepatcht Wird installiert Base apk wird heruntergeladen Bibliotheken APK wird heruntergeladen Language apk wird heruntergeladen Resources apk wird heruntergeladen Bunny-Modul wird heruntergeladen App-Icon wird geändert App-Manifest wird gepatcht Bunny wird hinzugefügt APKs werden signiert APKs werden installiert Erfolgreich installiert Installation abgebrochen Fehler bei der Installation: Unbekannter Fehler Installation wurde blockiert Ein oder mehrere APKs sind korrupt Konflikt mit einer existierenden App, meist durch nicht übereinstimmende Signaturen Nicht genug Speicher für Installation App ist inkompatibel mit diesem Gerät Installation hat zu lange gedauert Fortlaufend Erfolgreich Fehlgeschlagen In der Warteschlange Minimieren Erweitern Zurück Logs kopieren Cache leeren Bestätigen Update starten Installieren Aktualisieren Neuinstallieren Starten Deinstallieren Info Erneut versuchen Neuladen Über Einstellungen öffnen Tippen um Bunny für Discord %1$s zu installieren Trotzdem verlassen Vergiss es Nein danke Versuch es noch einmal Vorheriges Theme ansehen Nächstes Theme ansehen Logs ansehen Zeitstempel anzeigen Log kopieren Save logs to file Share logs More options Verlassen könnte heruntergeladene Dateien beschädigen. Sind sie sich sicher, dass Sie den Installer verlassen wollen? Zwischengespeichert Installer Home Einstellungen Aktualisierung verfügbar! Über Berechtigungen erteilen Neue Aktualisierung verfügbar! Achtung Herunterladen fehlgeschlagen Logs Open source libraries Libraries System Hell Dunkel Deaktiviert Viertelstündlich Halbstündlich Stündlich Zweistündig Zweimal täglich Täglich Wöchentlich Erscheinungsbild Ändere das Aussehen der App Dynamische Farbe Nur verfügbar ab Android 12 Erscheinungsbild Nach Aktualisierung suchen Personalisierung Personalisiere deine Bunny-Installation App Name App-Icon ersetzen Bunny-Icon anstatt Discord-Icon benutzen Erweiterte Einstellungen Ändere Download und Installation Release-Kanal Download-Mirror Cache automatisch leeren Cache leeren, wenn Discord aktualisiert wird Entwicklereinstellungen Diese Einstellungen können Dinge beschädigen, falls Sie falsch genutzt werden Modulspeicherort Für Xposed-Modul-Entwicklung. SEI VORSICHTIG Modulspeicherort zurücksetzen Paketname Discord Version Debugfähig Aktiviert das \"debuggable\" Attribut im Manifest Alternate lines Wrap lines Bunny-Manager Version %1$s ist jetzt verfügbar! Stable Beta Alpha GitHub Discord Team Besonderen Dank an Übersetzung Neueste Version: %1$s Zielversion: %1$s Aktuelle Version: %1$s Installationsmethode Default (recommended) Shizuku ================================================ FILE: app/src/main/res/values-el-rGR/strings.xml ================================================ Bunny Manager Author: %1$s License: %1$s Επιτυχής εκκαθάριση προσωρινής μνήμης Αποτυχία φόρτωσης των ρυθμίσεων Έρχεται σύντομα Μη έγκυρη έκδοση Discord 7 ακόμα πατήματα 5 ακόμα πατήματα 2 ακόμα πατήματα Φόρτωση… Αδυναμία υποβάθμισης, δοκιμάστε πρώτα την απεγκατάσταση Είστε πλέον προγραμματιστής Για να λειτουργήσει ο Bunny Manager, απαιτούνται δικαιώματα αρχείων. Δεδομένου ότι τα κοινόχρηστα δεδομένα αποθηκεύονται στο ~/Bunny, απαιτούνται δικαιώματα πρόσβασης σε αυτά. Failed to obtain Shizuku permissions Θα θέλατε να δοκιμάσετε ξανά χρησιμοποιώντας έναν καθρέφτη λήψης; Το APK ήταν κατεστραμμένο, προσπαθήστε να καθαρίσετε την προσωρινή μνήμη και έπειτα να επανεγκαταστήσετε Download was aborted Download failed Could not verify downloaded file, check logs for more details Copied to clipboard Λήψη APK Patching Γίνεται εγκατάσταση Λήψη βασικού apk Λήψη libraries apk Λήψη language apk Λήψη resources apk Λήψη πακέτου Bunny Αλλαγή εικονιδίου εφαρμογής Ενημέρωση manifests τής εφαρμογής Ενσωμάτωση Bunny Συγχρονισμός των APKs Εγκατάσταση APKs Εγκαταστάθηκε επιτυχώς Η εγκατάσταση ακυρώθηκε Αποτυχία εγκατάστασης: Άγνωστος λόγος Η εγκατάσταση αποκλείστηκε Ένα ή περισσότερα APKs ήταν άκυρα ή κατεστραμμένα Συγκρούσεις με μια υπάρχουσα εφαρμογή, συνήθως λόγω αναντιστοιχίας υπογραφών Δεν υπάρχει αρκετός διαθέσιμος χώρος για εγκατάσταση Η εφαρμογή δεν είναι συμβατή με αυτήν τη συσκευή Το χρονικό όριο εγκατάστασης έληξε Σε εξέλιξη Επιτυχής Αποτυχία Σε αναμονή Σύμπτυξη Ανάπτυξη Επιστροφή Αντιγραφή καταγραφών Εκκαθάριση προσωρινής μνήμης Επιβεβαίωση Έναρξη ενημέρωσης Εγκατάσταση Ενημέρωση Επανεγκατάσταση Άνοιγμα Απεγκατάσταση Πληροφορίες Επανάληψη Ανανέωση Άνοιγμα σχετικά με Άνοιγμα ρυθμίσεων Πατήστε για να εγκαταστήσετε το Bunny για το Discord %1$s Έξοδος ούτως ή άλλως Δεν πειράζει Όχι, ευχαριστώ Προσπάθησε ξανά See previous theme See next theme View logs Show timestamp Copy log Save logs to file Share logs More options Η έξοδος από το πρόγραμμα εγκατάστασης πριν από την ολοκλήρωση του θα μπορούσε να καταστρέψει τα κατεβασμένα αρχεία, είστε σίγουροι ότι θέλετε να το κάνετε αυτό; Στη προσωρινή μνήμη Εγκαταστάτης Αρχική Ρυθμίσεις Διαθέσιμη ενημέρωση! Σχετικά με Εκχώρηση Δικαιωμάτων Νέα ενημέρωση για το Discord είναι διαθέσιμη! Προσοχή Αποτυχία λήψης Logs Open source libraries Libraries Σύστημα Ανοιχτό Σκούρο Πότε Ανά 15 λεπτά Ανά μισή ώρα Ανά μία ώρα Ανά 2 ώρες Δύο φορές την ημέρα Καθημερινά Εβδομαδιαία Εμφάνιση Change the look and feel of the app Δυναμικό χρώμα Διαθέσιμο μόνο για Android 12 και άνω Θέμα Έλεγχος για ενημερώσεις του Discord Customization Customize your Bunny install Όνομα εφαρμογής Αντικατάσταση εικονιδίου εφαρμογής Χρησιμοποίηση του εικονιδίου Bunny αντί του Discord Για προχωρημένους Modify downloading and installation Κανάλι κυκλοφορίας Λήψη mirror Αυτόματη εκκαθάριση προσωρινής μνήμης Καθαρίζει την προσωρινή μνήμη όταν το Discord λαμβάνει μια ενημέρωση Μόνο για προγραμματιστές These settings may break things if used improperly Τοποθεσία module Για χρήση με την ανάπτυξη του Xposed module. ΝΑ ΕΊΣΤΕ ΠΡΟΣΕΚΤΙΚΟΊ Επαναφορά τοποθεσίας module Όνομα πακέτου Έκδοση Discord Αποσφαλματώσιμο Ενεργοποίηση σημαίας αποσφαλμάτωσης Alternate lines Wrap lines Η έκδοση %1$s του Bunny Manager είναι τώρα διαθέσιμη! Σταθερή Δοκιμαστική Alpha Github Discord Ομάδα Ιδιαίτερες ευχαριστίες Μετάφραση Πρόσφατο: %1$s Στόχος: %1$s Τρέχουσα: %1$s Install method Default (recommended) Shizuku ================================================ FILE: app/src/main/res/values-en-rUS/strings.xml ================================================ Bunny Manager Author: %1$s License: %1$s Cache cleared successfully Failed to load commits Never coming out Invalid Discord version 7 more taps 5 more taps 2 more taps Loading… Cannot downgrade, try uninstalling first You are now a developer In order for Bunny Manager to function, file permissions are required. Since shared data is stored in ~/Bunny, permissions are required in order to access it. Failed to obtain Shizuku permissions Would you like to try again using a download mirror? APK was corrupted, try clearing cache then reinstalling Download was aborted Download failed Could not verify downloaded file, check logs for more details Copied to clipboard Download APKs Patching Installing Downloading base APK Downloading libraries APK Downloading language APK Downloading resources APK Downloading Bunny module Changing app icon Patching app manifests Injecting Bunny Signing APKs Installing APKs Installed successfully Install canceled Failed to install: Unknown reason Installation was blocked One or more APKs were invalid or corrupt Conflicts with an existing app, usually due to mismatched signatures Not enough available storage to install Application is incompatible with this device Installation timed out Ongoing Successful Failed Queued Collapse Expand Go back Copy logs Clear cache Confirm Start update Install Update Reinstall Launch Uninstall Info Retry Reload Open about Open settings Tap to install Bunny for Discord %1$s Exit anyways Never mind No thanks Try again See previous theme See next theme View logs Show timestamp Copy log Save logs to file Share logs More options Exiting the installer before its finished could corrupt downloaded files, are you sure you want to do that? Cached Installer Home Settings Update available! About Grant Permissions New Discord update available! Warning Download failed Logs Open source libraries Libraries System Light Dark Disabled Quarter hourly Half hourly Hourly Bi-hourly Twice daily Daily Weekly Appearance Change the look and feel of the app Dynamic color Only available on Android 12 and up Theme Check for Discord updates Customization Customize your Bunny install App name Replace app icon Uses the Bunny icon instead of Discord\'s Advanced Modify downloading and installation Release channel Download mirror Clear cache automatically Clears cache when Discord gets an update Developer only These settings may break things if used improperly Module location For use with developing the Xposed module. BE CAREFUL Reset module location Package name Discord version Debuggable Enable debuggable flag Alternate lines Wrap lines Bunny Manager version %1$s is now available! Stable Beta Alpha Github Discord Team Special thanks Translate Latest: %1$s Target: %1$s Current: %1$s Install method Default (recommended) Shizuku ================================================ FILE: app/src/main/res/values-es-rES/strings.xml ================================================ Bunny Manager Author: %1$s License: %1$s Caché borrado con éxito Error al cargar commits Próximamente Versión inválida de Discord 7 toques más 5 toques más 2 toques más Cargando… No se puede desactualizar, intente desinstalándolo primero Ahora eres un desarrollador Para que Bunny Manager funcione, se requieren permisos de archivos. Dado que los datos compartidos se almacenan en ~/Bunny, se requieren permisos para acceder a ellos. Failed to obtain Shizuku permissions ¿Te gustaría volver a intentar usando un servidor réplica de descarga? APK estaba dañado, intente borrar la caché y vuelva a instalar Download was aborted Download failed Could not verify downloaded file, check logs for more details Copied to clipboard Descargar APKs Parcheando Instalando Descargando apk base Descargando apk de librerías Descargando apk de idiomas Descargando apk de recursos Descargando módulo Bunny Cambiando icono de la aplicación Parcheando manifiestos de aplicación Inyectando Bunny Firmando APKs Instalando APKs Instalado con éxito Instalación cancelada Error al instalar: Razón desconocida La instalación fue bloqueada Uno o más APKs no son válidos o están corruptos Conflictos con una aplicación existente, generalmente debido a firmas inadecuadas No hay suficiente almacenamiento disponible para instalar La aplicación es incompatible con este dispositivo Tiempo de instalación agotado En progreso Éxito Fallido En cola Colapsar Expandir Volver atrás Copiar logs Limpiar caché Confirmar Iniciar actualización Instalar Actualizar Reinstalar Iniciar Desinstalar Información Reintentar Actualizar Abrir \"acerca de\" Abrir configuración Toca para instalar Bunny para Discord %1$s Salir de todos modos Mejor no No, gracias Intentar de nuevo See previous theme See next theme View logs Show timestamp Copy log Save logs to file Share logs More options Salir del instalador antes de que finalice podría corromper los archivos descargados, ¿está seguro de que quieres hacerlo? En caché Instalador Inicio Configuración ¡Actualización disponible! Acerca de Otorgar Permisos ¡Nueva actualización de Discord disponible! Advertencia Descarga fallida Logs Open source libraries Libraries Sistema Claro Oscuro Deshabilitado Cada 15 minutos Cada media hora Cada hora Cada dos horas Dos veces al día Cada día Semanalmente Apariencia Change the look and feel of the app Color dinámico Sólo disponible en Android 12 y superior Tema Comprobar actualizaciones de Discord Customization Customize your Bunny install Nombre de la aplicación Reemplazar el icono de la app Usa el ícono de Bunny en lugar de Discord Avanzado Modify downloading and installation Canal de lanzamientos Descargar desde servidor réplica Borrar caché automáticamente Limpiar el caché cuando Discord recibe una actualización Sólo desarrolladores These settings may break things if used improperly Ubicación del módulo Para el uso con el desarrollo del módulo Xposed. SE CUIDADOSO Restablecer la ubicación del módulo Nombre del paquete Versión de Discord Depurable Activar modo de depuración Alternate lines Wrap lines ¡La versión %1$s de Bunny Manager está disponible! Estable Beta Alfa Github Discord Equipo Agradecimientos especiales Traducir Más reciente: %1$s Selección: %1$s Actual: %1$s Método de instalación Por defecto (recomendado) Shizuku ================================================ FILE: app/src/main/res/values-fi-rFI/strings.xml ================================================ Bunny Manager Author: %1$s License: %1$s Cache cleared successfully Failed to load commits Coming soon Invalid Discord version 7 more taps 5 more taps 2 more taps Loading… Cannot downgrade, try uninstalling first You are now a developer In order for Bunny Manager to function, file permissions are required. Since shared data is stored in ~/Bunny, permissions are required in order to access it. Failed to obtain Shizuku permissions Would you like to try again using a download mirror? APK was corrupted, try clearing cache then reinstalling Download was aborted Download failed Could not verify downloaded file, check logs for more details Copied to clipboard Download APKs Patching Installing Downloading base apk Downloading libraries apk Downloading language apk Downloading resources apk Downloading Bunny module Changing app icon Patching app manifests Injecting Bunny Signing APKs Installing APKs Installed successfully Install canceled Failed to install: Unknown reason Installation was blocked One or more APKs were invalid or corrupt Conflicts with an existing app, usually due to mismatched signatures Not enough available storage to install Application is incompatible with this device Installation timed out Ongoing Successful Failed Queued Collapse Expand Go back Copy logs Clear cache Confirm Start update Install Update Reinstall Launch Uninstall Info Retry Reload Open about Open settings Tap to install Bunny for Discord %1$s Exit anyways Nevermind No thanks Try again See previous theme See next theme View logs Show timestamp Copy log Save logs to file Share logs More options Exiting the installer before its finished could corrupt downloaded files, are you sure you want to do that? Cached Installer Home Settings Update available! About Grant Permissions New Discord update available! Warning Download failed Logs Open source libraries Libraries System Light Dark Disabled Quarter hourly Half hourly Hourly Bi-hourly Twice daily Daily Weekly Appearance Change the look and feel of the app Dynamic color Only available on Android 12 and up Theme Check for Discord updates Customization Customize your Bunny install App name Replace app icon Uses the Bunny icon instead of Discord\'s Advanced Modify downloading and installation Release channel Download mirror Clear cache automatically Clears cache when Discord gets an update Developer only These settings may break things if used improperly Module location For use with developing the Xposed module. BE CAREFUL Reset module location Package name Discord version Debuggable Enable debuggable flag Alternate lines Wrap lines Bunny Manager version %1$s is now available! Stable Beta Alpha Github Discord Team Special thanks Translate Latest: %1$s Target: %1$s Current: %1$s Install method Default (recommended) Shizuku ================================================ FILE: app/src/main/res/values-fil-rPH/strings.xml ================================================ Manager ng Bunny Author: %1$s License: %1$s Na-clear na ang cache Nabigong i-load ang mga commit Parating na Hindi wastong bersyon ng Discord 7 pang mga tap 5 pang mga tap 2 pang mga tap Naglo-load… Hindi maka-downgrade, subukang mag-uninstall muna Isa ka nang developer ngayon Upang gumana ang Bunny Manager, kinakailangan ang pahintulot ng file. Dahil nakaimbak ang nakabahaging data sa ~/Bunny, kailangan ng mga pahintulot para ma-access ito. Nabigong makuha ang mga pahintulot ng Shizuku Gusto mo bang subukan muli gamit ng download mirror? Nasira ang APK, subukang i-clear ang cache at pag-reinstall Na-abort ang pag-download Nabigo ang pag-download Could not verify downloaded file, check logs for more details Copied to clipboard I-download ang mga APK Pina-patch Ini-install Dina-download ang base apk Dina-download ang libraries apk Dina-download ang language apk Dina-download ang resources apk Dina-download ang Bunny module Pinapalitan ang app icon Pina-patch ang mga app manifest Ini-inject ang Bunny Sina-sign ang mga APK Ini-install ang mga APK Matagumpay na na-install Kinansela ang pag-install Nabigong i-install: Hiindi alam na dahilan Na-block ang installation Ang isa o higit pang mga APK ay hindi wasto o sira Mga salungatan sa isang umiiral nang app, kadalasan dahil sa mga hindi tugmang signature Walang sapat na available na storage para ma-install Hindi compatible ang application sa device na ito Nag-time out ang installation Isinasagawa Matagumpay Nabigo Naka-queue Itago Ipakita Bumalik Kopyahin ang mga log I-clear ang cache Kumpirmahin Simulan ang update I-install I-update Muling i-install Buksan I-Uninstall Impormasyon Muling subukan I-reload Buksan ang about Buksan ang mga setting I-tap para i-install ang Bunny para sa Discord %1$s Umalis na lang Huwag na lang Hindi na, salamat Subukan muli See previous theme See next theme View logs Show timestamp Copy log Save logs to file Share logs More options Ang pagalis sa installer bago matapos ay maaring mag-corrupt ng mga file, sigurado ka bang gusto mo yan gawin? Naka-cache Installer Home Mga Setting Mayroong update! Tungkol sa Magbigay ng Mga Pahintulot May bagong available na update sa Discord! Babala Nabigo ang pag-download Logs Open source libraries Libraries Sistema Maliwanag Madilim Naka-disable Quarter hourly Kalahating oras Oras-oras Bi-hourly Dalawang beses araw-araw Araw-araw Lingguhan Hitsura Baguhin ang hitsura ng app Dinamika na kulay Available lamang sa Android 12 o mas mataas Tema Mag-check ng mga bagong update sa Discord Pag-customize I-customize ang iyong Bunny install Pangalan ng app Palitan ang app icon Gamitin ang Bunny icon sa halip ng Discord Advanced Baguhin ang pag-download at pag-install Release channel Download mirror I-clear ang cache ng awtomatiko Iki-clear ang cache kapag nakakuha ng update ang Discord Developer lamang Maaring masira ang iba\'t ibang bagay kapag hindi ginamit ng mabuti Lokasyon ng module Para sa paggamit sa pag-develop ng Xposed module. MAG-INGAT KA I-reset ang lokasyon ng module Pangalan ng package Bersyon ng Discord Debuggable Paganahin ang debuggable flag Alternate lines Wrap lines Available na ang Bunny Manager bersyon %1$s! Stable Beta Alpha GitHub Discord Koponan Espesyal na Pasasalamat Isalin Pinakabago: %1$s Target: %1$s Kasalukuyan: %1$s Paraan ng pag-install Default (recommended) Shizuku ================================================ FILE: app/src/main/res/values-fr-rFR/strings.xml ================================================ Gestionnaire de Bunny Auteur: %1$s Licence : %1$s Le cache a été supprimé avec succès Échec lors du chargement des commits Bientôt disponible Version de Discord invalide Appuyez encore 7 fois Appuyez encore 5 fois Appuyez encore 2 fois Chargement en cours… Impossible de revenir à la version ultérieure, essayez d\'abord de la désinstaller puis réessayez Vous êtes maintenant un développeur Pour que le Gestionnaire de Bunny fonctionne correctement, la permission de stockage de fichier est requise. Les fichiers sont stockés dans le dossier Bunny, des autorisations sont nécessaires pour y accéder. Échec de l\'obtention des permissions Shizuku Voulez-vous réessayer en utilisant un autre serveur de téléchargement ? L\'APK a été corrompu, essayez de vider le cache puis de réinstaller Le téléchargement a été annulé Téléchargements échoué Impossible de vérifier le fichier téléchargé, vérifiez les logs pour plus de détails Copié dans le presse-papiers Télécharger les APKs Patch en cours Installation en cours Téléchargement de l\'APK de base Téléchargement de l\'APK de librairies Téléchargement de l\'APK de langue Téléchargement des ressources pour l\'APK Téléchargement du module Bunny Changement de l\'icône de l\'app Patch des manifests de l\'app Injection de Bunny Signature des APKs Installation des APKs Installation réussie Installation annulée Échec de l\'installation : raison inconnue L\'installation a été bloquée Un ou plusieurs APKs sont invalides ou corrompus Conflit avec une application existante, généralement à cause de signatures non identiques Stockage insuffisant pour installer L\'application n\'est pas compatible avec cet appareil Installation échouée : Temps limite dépassée En cours Succès Échec En file d\'attente Réduire Agrandir Précédent Copier les logs Vider le cache Confirmer Démarrer la mise à jour Installer Mettre à jour Réinstaller Démarrer Désinstaller Information Réessayer Recharger Ouvrir la page « à propos » Ouvrir les paramètres Appuyez ici pour installer Bunny pour Discord %1$s Quitter quand même Jamais Non, merci Réessayer Voir le thème précédent Voir le thème suivant Voir les logs Afficher l\'horodatage Copier les logs Enregistrer les logs dans un fichier Partager les logs Plus d\'options Quitter le programme d\'installation avant qu\'il ne soit terminé pourrait corrompre les fichiers téléchargés. Êtes-vous sûr de vouloir le faire ? Mis en cache Installateur Accueil Paramètres Mise à jour disponible ! À propos Accorder les permissions Une nouvelle mise à jour de Discord est disponible ! Avertissement Téléchargements échoué Logs Bibliothèques open-source Bibliothèques Système Clair Sombre Désactivé Chaque quart d\'heure Chaque demi-heure Chaque heure Chaque deux heures Deux fois par jour Quotidien Hebdomadaire Apparence Choisissez l\'apparence de l\'application Couleur dynamique Disponible uniquement sur Android 12 et plus Thème Vérifier les mises à jour pour Discord Personnalisation Personnalisez votre installation de Bunny Nom de l\'appli Remplacer l\'icône de l\'appli Utiliser l\'icône de Bunny au lieu de celle de Discord Avancé Modifier le téléchargement et l\'installation Type de version Serveur de téléchargement Effacer automatiquement le cache Effacer le cache lorsque Discord reçois une mise à jour Développeur uniquement Ces paramètres peuvent entraîner des dysfonctionnements s\'ils sont utilisés de manière inappropriée Emplacement du module Pour une utilisation avec le développement du module Xposed. VEUILLEZ FAIRE ATTENTION Réinitialiser l\'emplacement du module Nom du package Version de Discord Débogable Activer le flag de débogage Lignes alternatives Retour à la ligne automatique La version %1$s du Gestionnaire de Bunny est maintenant disponible ! Stable Bêta Alpha Github Discord Équipe Remerciements Aidez-nous à traduire Dernière version : %1$s Version cible : %1$s Version actuelle : %1$s Méthode d\'installation Default (recommended) Shizuku ================================================ FILE: app/src/main/res/values-hu-rHU/strings.xml ================================================ Bunny Kezelő Szerző: %1$s Licenc: %1$s A gyorsítótár sikeresen törlődött Nem sikerült betölteni a véglegesítéseket Hamarosan Érvénytelen Discord verzió még 7 érintés még 5 érintés még 2 érintés Betöltés… Nem lehet leminősíteni, próbálja meg először eltávolítani Ön most már fejlesztő A Bunny Kezelő működéséhez fájlengedélyekre van szükség. Mivel a megosztott adatokat a ~/Bunny fájlban tárolja, a hozzáféréshez engedélyekre van szükség. Nem sikerült megszerezni Shizuku engedélyeit Szeretné újra megpróbálni egy letöltési mirror segítségével? Az APK sérült, próbálja meg a gyorsítótár törlését, majd az újratelepítést A letöltés megszakadt Letöltés sikertelen Could not verify downloaded file, check logs for more details Vágólapra másolva APK-k letöltése Foltozás Telepítés folyamatban Alap apk letöltése Könyvár apk letöltése Nyelvi apk letöltése Források apk letöltése folyamatban Bunny modul letöltése App ikon megváltoztatása Alkalmazás manifesztek javítása Bunny beépítése APK-k aláírása APK-k telepítése folyamatban Sikeresen telepítve Telepítés megszakítva A telepítés sikertelen: Ismeretlen ok A telepítés blokkolva Egy vagy több APK érvénytelen vagy sérült Konfliktus egy meglévő alkalmazással, általában az aláírás eltérése miatt Nincs elég hely a telepítéshez Az alkalmazás nem kompatibilis ezzel az eszközzel A telepítés leállt Folyamatban Sikeresem Sikertelenül Várólistán Összeomlott Kibontás Vissza Naplók másolása Gyorsítótár törlése Megerősítés Frissítés most Telepítés Frissítés Újratelepítés Megnyitás Eltávolítás Információ Újra Újratöltés Rólunk megnyitása Beállítások megnyitása Érintse meg a Bunny A Discordhoz telepítéséhez %1$s Kilépés Mindegy Nem, köszönöm Próbáld újra Lásd a korábbi témát Lásd a következő témát Naplók megtekintése Időbélyeg megjelenítése Napló másolása Naplófájlok mentése fáljba Napló megosztása További beállítások A telepítő befejezés előtti kilépésével sérülhetnek a letöltött fájlok, biztos, hogy ezt szeretné? Gyorsítótárazott Telepítő Kezdőlap Beállítások Frissítés érhető el! Rólunk Engedélyek megadása Új Discord frissítés elérhető! Figyelmeztetés Sikertelen letöltés Naplók Nyílt forráskódú könyvtárak Könyvtárak Rendszer Világos Sötét Kikapcsolva Negyedóránként Félóránként Óránként Kétóránként Naponta kétszer Naponta Hetente Megjelenés Az alkalmazás megjelenésének megváltoztatása Rendszer szerinti színek Csak Android 12+ rendszeren elérhető Téma Discord frissítések keresése Testreszabás A Bunny telepítésének testreszabása Alkalmazás neve Alkalmazás ikon kicserélése A Bunny ikon használata a Discord ikonja helyett Haladó Letöltés és telepítés módosítása Kiadási csatorna Letöltési tükör A gyorsítótár automatikus törlése Törli a gyorsítótárat, amikor a Discordhoz elérhető frissítés Csak fejlesztőknek Ezek a beállítások tönkretehetik a dolgokat, ha helytelenül használja őket Modul helye Az Xposed modul fejlesztéséhez. LEGYEN ÓVATOS A modul helyének visszaállítása Csomag neve Discord verzió Debuggolható Engedélyezze a debuggolható jelzőt Alternatív sorok Felhúzott vonalak A Bunny Manager %1$s verziója már elérhető! Stabil Béta Alpha Github Discord Csapat Külön köszönet Fordítás Legújabb: %1$s Cél: %1$s Jelenlegi: %1$s Telepítési módszer Alapértelmezett (ajánlott) Shizuku ================================================ FILE: app/src/main/res/values-in-rID/strings.xml ================================================ Bunny Manager Author: %1$s License: %1$s Berkas singgahan (cache) berhasil dikosongkan Failed to load commits Segera hadir Versi Discord tidak valid 7 ketukan lagi 5 ketukan lagi 2 ketukan lagi Memuat… Tidak bisa menurunkan versi (downgrade), coba copot pemasangan dahulu Kamu sekarang adalah seorang pengembang In order for Bunny Manager to function, file permissions are required. Since shared data is stored in ~/Bunny, permissions are required in order to access it. Failed to obtain Shizuku permissions Would you like to try again using a download mirror? APK was corrupted, try clearing cache then reinstalling Download was aborted Download failed Could not verify downloaded file, check logs for more details Copied to clipboard Download APKs Patching Installing Downloading base apk Downloading libraries apk Downloading language apk Downloading resources apk Downloading Bunny module Changing app icon Patching app manifests Injecting Bunny Signing APKs Installing APKs Installed successfully Install canceled Failed to install: Unknown reason Installation was blocked One or more APKs were invalid or corrupt Conflicts with an existing app, usually due to mismatched signatures Not enough available storage to install Application is incompatible with this device Installation timed out Ongoing Successful Failed Queued Collapse Expand Go back Copy logs Clear cache Confirm Start update Install Update Reinstall Launch Uninstall Info Retry Reload Open about Open settings Tap to install Bunny for Discord %1$s Exit anyways Nevermind No thanks Try again See previous theme See next theme View logs Show timestamp Copy log Save logs to file Share logs More options Exiting the installer before its finished could corrupt downloaded files, are you sure you want to do that? Cached Installer Home Settings Update available! About Grant Permissions New Discord update available! Warning Download failed Logs Open source libraries Libraries System Light Dark Disabled Quarter hourly Half hourly Hourly Bi-hourly Twice daily Daily Weekly Appearance Change the look and feel of the app Dynamic color Only available on Android 12 and up Theme Check for Discord updates Customization Customize your Bunny install App name Replace app icon Uses the Bunny icon instead of Discord\'s Advanced Modify downloading and installation Release channel Download mirror Clear cache automatically Clears cache when Discord gets an update Developer only These settings may break things if used improperly Module location For use with developing the Xposed module. BE CAREFUL Reset module location Package name Discord version Debuggable Enable debuggable flag Alternate lines Wrap lines Bunny Manager version %1$s is now available! Stable Beta Alpha Github Discord Tim Special thanks Translate Terkini: %1$s Target: %1$s Saat ini: %1$s Install method Default (recommended) Shizuku ================================================ FILE: app/src/main/res/values-it-rIT/strings.xml ================================================ Bunny Manager Author: %1$s License: %1$s Cache pulita con successo Caricamento dei commit non riuscito A breve Versione Discord non valida Altri 7 tocchi Altri 5 tocchi Altri 2 tocchi Caricamento… Impossibile eseguire il downgrade, prova a disinstallare prima Ora sei uno sviluppatore Affinché Bunny Manager possa funzionare, sono richiesti i permessi dei file. Dal momento che i dati condivisi sono memorizzati in ~/Bunny, sono necessari i permessi per accedervi. Failed to obtain Shizuku permissions Vuoi provare di nuovo usando un download alternativo? L\'APK è corrotto, prova a svuotare la cache e poi a reinstallarlo Download was aborted Download failed Could not verify downloaded file, check logs for more details Copied to clipboard Download APKs Patch in corso Installazione Download apk base Download apk librerie Download apk lingua Download apk risorse Download modulo Bunny Cambia icona dell\'app Patch manifest dell\'app Iniettando Bunny Firmando gli APK Installando APK Installato con successo Installazione annullata Installazione non riuscita: Motivo sconosciuto L\'installazione è stata bloccata Uno o più APK non erano validi o corrotti Conflitti con un\'app esistente, di solito a causa di firme non corrispondenti Spazio disponibile insufficiente per l\'installazione L\'applicazione non è compatibile con questo dispositivo Tempo scaduto per l\'installazione In corso Riuscito Non riuscito In coda Minimizza Espandi Indietro Copia i log Svuota cache Conferma Avvia l\'aggiornamento Installa Aggiorna Reinstalla Avvia Disinstalla Info Riprova Aggiorna Apri informazioni Apri impostazioni Tocca per installare Bunny per Discord %1$s Esci comunque Non importa No grazie Riprova See previous theme See next theme View logs Show timestamp Copy log Save logs to file Share logs More options Uscire dal programma prima che abbia finito potrebbe danneggiare i file scaricati, sei sicuro di volerlo fare? Memorizz. nella cache Installer Home Impostazioni Aggiornamento disponibile! Informazioni Concedi permessi Nuovo aggiornamento di Discord disponibile! Attenzione Download non riuscito Logs Open source libraries Libraries Sistema Chiaro Scuro Disabilitato Ogni 15 minuti Ogni 30 minuti Ogni ora Ogni 2 ore Due volte al giorno Giornalmente Settimanalmente Aspetto Change the look and feel of the app Colori dinamici Disponibile solo su Android 12 e versioni successive Tema Controlla aggiornamenti di Discord Customization Customize your Bunny install Nome app Sostituisci l\'icona dell\'app Utilizza l\'icona di Bunny al posto di Discord Avanzate Modify downloading and installation Canale di rilascio Download alternativo Cancella la cache automaticamente Cancella la cache quando Discord riceve un aggiornamento Solo sviluppatori These settings may break things if used improperly Percorso del modulo Da utilizzare con lo sviluppo del modulo Xposed. FA ATTENZIONE Ripristina la posizione del modulo Nome pacchetto Versione Discord Debuggable Abilita flag debug Alternate lines Wrap lines Bunny Manager versione %1$s è disponibile! Stabile Beta Alpha Github Discord Team Ringraziamenti speciali Traduci Ultima versione: %1$s Target: %1$s Versione attuale: %1$s Metodo d\'installazione Predefinito (Consigliato) Shizuku ================================================ FILE: app/src/main/res/values-iw-rIL/strings.xml ================================================ מנהל Bunny Author: %1$s License: %1$s המטמון נוקה בהצלחה טעינת שינוים נכשלה בקרוב גרסת דיסקורד לא נתמכת עוד 7 לחיצות עוד 5 לחיצות עוד 2 לחיצות טוען… לא ניתן לחזור גרסה, נסה קודם להסיר את האפליקציה אתה כעת מפתח על מנת שמנהל Bunny יפעל, נדרשות הרשאות קובץ. מכיוון שהנתונים המשותפים מאוחסנים ב-~/Bunny, נדרשות הרשאות על מנת לגשת אליהם. Failed to obtain Shizuku permissions האם תרצה לנסות שוב באמצעות קישור אחר? APK was corrupted, try clearing cache then reinstalling Download was aborted Download failed Could not verify downloaded file, check logs for more details Copied to clipboard מוריד קבצים מנתח מתקין מוריד את ה-Apk הבסיסי מוריד את ספריית ה-Apk מוריד שפות האפליקציה מוריד את החומרים לאפליקציה מוריד את מודל Bunny שינוי סמל האפליקציה תקן אפליקציית מניפסטים מכניס את Bunny חותם על APKs מתקין אפליקציות הותקן בהצלחה התקנה בוטלה ההתקנה נכשלה: סיבה לא ידועה ההתקנה נחסמה APK אחד או יותר היו לא תקינים או פגומים מתנגשות עם אפליקציה קיימת, בדרך כלל עקב אי-התאמה של חתימות אין מספיק שטח אחסון זמין להתקנה האפליקציה אינה תומכת במכשיר זה ההתקנה איבדה תקשורת מתמשך הצליח נכשל בהמתנה צמצום הרחב חזור אחורה העתק לוגים נקה מטמון אישור התחל עדכון התקן עדכן התקן מחדש הפעל הסר התקנה מידע נסה שוב רענון פתח על פתח הגדרות Tap to install Bunny for Discord %1$s Exit anyways Nevermind No thanks Try again See previous theme See next theme View logs Show timestamp Copy log Save logs to file Share logs More options Exiting the installer before its finished could corrupt downloaded files, are you sure you want to do that? מוטמן מתקין בית הגדרות עדכון זמין! אודות הענקת הרשאות New Discord update available! Warning Download failed Logs Open source libraries Libraries מערכת בהיר כהה Disabled Quarter hourly Half hourly Hourly Bi-hourly Twice daily Daily Weekly מראה Change the look and feel of the app צבע דינמי זמין לאנדרואיד 12 ומעלה בלבד עיצוב Check for Discord updates Customization Customize your Bunny install שם האפליקציה החלף תמונת אפליקציה השתמש בתמונה של Bunny במקום התמונה של דיסקורד הגדרות מתקדמות Modify downloading and installation ערוץ הפצה Download mirror Clear cache automatically Clears cache when Discord gets an update מתכנתים בלבד These settings may break things if used improperly Module location For use with developing the Xposed module. BE CAREFUL Reset module location שם החבילה גרסת דיסקורד ניתן לניפוי באגים האפשרות ניתן לניפוי באגים הופעלה Alternate lines Wrap lines הגרסה %1$s של מנהל Bunny זמינה כעת! יציב בטא אלפא Github דיסקורד צוות תודות מיוחדות תרגם גרסה אחרונה: %1$s מטרה: %1$s נוכחי: %1$s Install method Default (recommended) Shizuku ================================================ FILE: app/src/main/res/values-ja-rJP/strings.xml ================================================ Bunny Manager 開発者: %1$s ライセンス: %1$s キャッシュを消去しました コミットの読み込みに失敗しました 近日公開 Discordのバージョンが無効です あと7回タップしてください あと5回タップしてください あと2回タップしてください 読み込み中… ダウングレードできません。まずアンインストールを試してください これで開発者になりました Bunny Manager を正しく動作させるためには、ファイルへのアクセスを許可する必要があります。 ~/Bunny に共有データが保存されるため、Manager を機能させるためには権限が必要です。 Shizukuの権限を取得できませんでした ダウンロードミラーを使用して再試行しますか? APK が破損しています。キャッシュを消去してから再インストールしてください。 ダウンロードが中止されました ダウンロードに失敗しました ダウンロードされたファイルを検証できませんでした。詳細についてはログを確認してください。 クリップボードにコピーしました APK のダウンロード パッチの適用 インストール ベース APK のダウンロード ライブラリ APK のダウンロード 言語 APK のダウンロード リソース APK のダウンロード Bunny モジュールのダウンロード アプリアイコンの変更 マニフェストにパッチを適用中 Bunny をインジェクト中 APK を署名中 APK をインストール中 正常にインストールされました インストールはキャンセルされました インストールに失敗しました: 不明な理由 インストールがブロックされました 1つまたは複数の APK が無効または破損しています 既存のアプリと競合します。通常、署名が一致しないことが原因です。 空き容量が不足しています。 アプリケーションはこのデバイスと互換性がありません インストールがタイムアウトしました 進行中 成功 失敗 処理待ち 閉じる 展開する 戻る ログをコピー キャッシュを消去 確定 アップデートを開始 インストール アップデート 再インストール 開く アンインストール 情報 再試行 再読み込み 概要を開く 設定を開く タップして Discord %1$s にBunnyをインストールする とにかく終了 やめる いいえ 再試行 前のテーマを表示 次のテーマを表示 ログを表示 タイムスタンプを表示 ログをコピー ログをファイルで保存 ログを共有 その他のオプション インストールが完了する前にインストーラーを終了すると、ダウンロードしたファイルが破損する可能性があります。本当に実行しますか? キャッシュ済み インストーラー ホーム 設定 アップデートがあります 概要 アクセス権限の付与 新しい Discord のアップデートが利用可能です! 警告 ダウンロードに失敗しました ログ オープンソースライブラリ ライブラリ システム ライト ダーク 無効 15分 30分 1時間 2時間 12時間 1日 7日 外観 アプリの外観と操作性を変更します ダイナミック カラー Android 12 以降でのみ利用可能です テーマ Discordのアップデートを確認 カスタマイズ Bunnyのカスタマイズ アプリ名 アプリのアイコンを置き換え Discordアイコンの代わりにBunnyアイコンを使用します 高度な設定 ダウンロードとインストールの変更 リリースチャンネル ダウンロードミラー キャッシュを自動的にクリア Discordの更新時にキャッシュをクリアします 開発者専用 これらの設定を不適切に使用すると、問題が発生する可能性があります モジュールの場所 Xposed モジュールの開発に使用します。!!注意!! モジュールの保存場所をリセット パッケージ名 Discord バージョン デバッグ可能 デバッグ可能フラグを有効化します 代替の行 行を折り返す Bunny Manager の新バージョン %1$s が利用可能です! 安定版 ベータ版 アルファ版 GitHub Discord チーム スペシャルサンクス 翻訳 最新バージョン: %1$s ターゲット: %1$s インストール済み: %1$s インストール方法 Default (推奨) Shizuku ================================================ FILE: app/src/main/res/values-ko-rKR/strings.xml ================================================ Bunny Manager 제작자: %1$s 라이센스: %1$s 캐시를 비웠어요 커밋을 불러오는 데 실패했어요 출시 예정 잘못된 Discord 버전 7번 더 누르세요 5번 더 누르세요 2번 더 누르세요 불러오는 중… 다운그레이드하지 못했어요. 먼저 삭제해 보세요 개발자 모드가 활성화되었어요 Bunny Manager가 정상적으로 작동하려면, 파일 권한을 부여해 주셔야 해요. 공유된 데이터가 ~/Bunny에 저장되기 때문에, Manager가 작동하려면 권한이 필요해요. Shizuku 권한을 얻지 못했어요. 미러 버전을 다운로드해서 다시 시도할까요? APK가 손상되었습니다. 캐시를 지운 후 다시 설치해 보세요. 다운로드 중단 다운로드 실패 다운로드한 파일을 검증할 수 없어요. 자세한 내용은 로그를 확인하세요. 클립보드에 복사됨. APK 다운로드 패치 중 설치 중 기본 APK 다운로드 중 라이브러리 APK 다운로드 중 언어 APK 다운로드 중 리소스 APK 다운로드 중 Bunny 모듈 다운로드 중 앱 아이콘 변경 중 앱 manifests 패치 중 Bunny 인젝팅 중 APK 서명 중 APK 설치 중 설치가 완료되었어요 설치가 취소되었어요 알 수 없는 이유로 설치에 실패했어요 설치가 차단되었어요 하나 이상의 APK가 잘못되었거나 손상되었어요 기존 앱과 충돌이 일어났어요. 주로 서명이 일치하지 않아서 발생해요. 설치 가능한 저장 공간이 부족해요 앱이 이 기기와 호환되지 않아요 설치 시간이 초과되었어요 진행 중 성공 실패 대기 중 접기 펼치기 돌아가기 로그 복사 캐시 비우기 확인 업데이트 시작하기 설치 업데이트 재설치 실행 삭제 정보 재시도 새로고침 정보 보기 설정 열기 Discord %1$s의 Bunny를 설치하려면 누르세요 그래도 종료하기 괜찮아요 괜찮아요 다시 시도 이전 테마 보기 다음 테마 보기 로그 보기 Show timestamp 로그 복사 파일에 로그 저장 로그 공유 More options 완료되기 전 설치기를 벗어나면 다운로드한 파일이 손상될 수 있어요. 그래도 계속할까요? 캐시됨 설치하기 설정 업데이트 가능! 정보 권한 부여 새 Discord 업데이트가 있어요! 경고 다운로드 실패 로그 오픈 소스 라이브러리 라이브러리 시스템 라이트 모드 다크 모드 사용 안 함 15분마다 30분마다 1시간마다 2시간마다 2일마다 1일마다 7일마다 외관 앱의 외형과 분위기를 변경합니다. 동적 색상 안드로이드 12 버전 이상에서만 사용 가능해요 테마 Discord 업데이트 확인 사용자 지정 Customize your Bunny install 앱 이름 앱 아이콘 변경 Discord 아이콘 대신 Bunny 아이콘을 사용해요 고급 Modify downloading and installation 릴리스 채널 미러 버전 다운로드 자동으로 캐시 비우기 Discord 업데이트가 있을 경우 자동으로 캐시를 비워요 개발자 전용 이러한 설정을 잘못 사용하면 문제가 발생할 수 있어요. 모듈 위치 Xposed 모듈를 사용하여 개발하는 경우 조심하세요 모듈 위치 초기화 패키지 이름 Discord 버전 디버그 가능 디버그 가능 플래그를 사용해요 Alternate lines Wrap lines 지금 Bunny Manager 버전 %1$s이 사용 가능해요! 안정화 버전 베타 버전 알파 버전 Github Discord 도움 주신 분들 번역 최신 버전: %1$s 대상 버전: %1$s 현재 버전: %1$s 설치 방법 Default (recommended) Shizuku ================================================ FILE: app/src/main/res/values-nl-rNL/strings.xml ================================================ Bunny Manager Author: %1$s License: %1$s Cache succesvol geleegd Kan commits niet laden Binnenkort beschikbaar Ongeldige Discord-versie Nog 7 tikken Nog 5 tikken Nog 2 tikken Bezig met laden… Kan niet downgraden, probeer eerst te verwijderen Je bent nu een ontwikkelaar Om Bunny Manager te laten werken, zijn bestandsrechten vereist. Aangezien gedeelde gegevens worden opgeslagen in ~/Bunny, zijn machtigingen vereist om er toegang toe te krijgen. Failed to obtain Shizuku permissions Would you like to try again using a download mirror? APK was corrupted, try clearing cache then reinstalling Download was aborted Download failed Could not verify downloaded file, check logs for more details Copied to clipboard Download APKs Patchen Installeren Basis-APK downloaden Bibliotheken-APK downloaden Taal-APK downloaden Resources-APK downloaden Bunny-module downloaden App-pictogram veranderen App-manifesten patchen Bunny injecteren APK\'s ondertekenen APK\'s installeren Succesvol geïnstalleerd Installatie geannuleerd Kan niet installeren: onbekende reden Installatie geblokkeerd Een of meer APK\'s waren ongeldig of corrupt Conflict met een bestaande app, meestal door verkeerde ondertekeningen Niet genoeg opslagruimte beschikbaar om te installeren Applicatie is niet compatibel met dit apparaat Installatie time-out Lopende Succesvol Mislukt In wachtrij Inklappen Uitklappen Ga terug Logs kopiëren Cache wissen Bevestigen Update starten Installeren Updaten Opnieuw installeren Starten Verwijder Info Opnieuw proberen Opnieuw laden \"About\" openen Instellingen openen Tap to install Bunny for Discord %1$s Exit anyways Nevermind No thanks Try again See previous theme See next theme View logs Show timestamp Copy log Save logs to file Share logs More options Exiting the installer before its finished could corrupt downloaded files, are you sure you want to do that? Gecached Installateur Home Instellingen Update beschikbaar! Over Toestemmingen verlenen New Discord update available! Warning Download failed Logs Open source libraries Libraries Systeem Licht Donker Disabled Quarter hourly Half hourly Hourly Bi-hourly Twice daily Daily Weekly Uiterlijk Change the look and feel of the app Dynamische kleuren Alleen beschikbaar op Android 12 en hoger Thema Check for Discord updates Customization Customize your Bunny install App naam App-pictogram vervangen Gebruik het Bunny pictogram in plaats van Discord\'s Geavanceerd Modify downloading and installation Releasekanaal Download mirror Clear cache automatically Clears cache when Discord gets an update Alleen voor ontwikkelaars These settings may break things if used improperly Module location For use with developing the Xposed module. BE CAREFUL Reset module location Pakketnaam Discord-versie Foutopspoorbaar Foutopspoorbaar-vlag inschakelen Alternate lines Wrap lines Bunny Manager versie %1$s is nu beschikbaar! Stabiel Bèta Alpha GitHub Discord Team Bijzondere dank aan: Vertalen Nieuwste: %1$s Doel: %1$s Huidig: %1$s Install method Default (recommended) Shizuku ================================================ FILE: app/src/main/res/values-no-rNO/strings.xml ================================================ Bunny Manager Author: %1$s License: %1$s Hurtigminnet tømt Failed to load commits Kommer snart Ugyldig Discord-versjon 7 flere trykk 5 flere trykk 2 flere trykk Laster… Kan ikke nedgradere, prøv å avinstallere først Du er utvikler nå For at Bunny Manager skal fungere, er filtillatelser påkrevd. Siden delte data lagres i ~/Bunny, kreves tillatelser for å få tilgang til den. Failed to obtain Shizuku permissions Would you like to try again using a download mirror? APK was corrupted, try clearing cache then reinstalling Download was aborted Download failed Could not verify downloaded file, check logs for more details Copied to clipboard Last ned APK Oppdaterer Installing Downloading base apk Downloading libraries apk Downloading language apk Downloading resources apk Downloading Bunny module Changing app icon Patching app manifests Injecting Bunny Signing APKs Installing APKs Installed successfully Install canceled Failed to install: Unknown reason Installation was blocked One or more APKs were invalid or corrupt Conflicts with an existing app, usually due to mismatched signatures Not enough available storage to install Application is incompatible with this device Installation timed out Ongoing Successful Failed Queued Collapse Expand Go back Copy logs Clear cache Confirm Start update Install Update Reinstall Launch Uninstall Info Retry Reload Open about Open settings Tap to install Bunny for Discord %1$s Exit anyways Nevermind No thanks Try again See previous theme See next theme View logs Show timestamp Copy log Save logs to file Share logs More options Exiting the installer before its finished could corrupt downloaded files, are you sure you want to do that? Cached Installer Home Settings Update available! About Grant Permissions New Discord update available! Warning Download failed Logs Open source libraries Libraries System Light Dark Disabled Quarter hourly Half hourly Hourly Bi-hourly Twice daily Daily Weekly Appearance Change the look and feel of the app Dynamic color Only available on Android 12 and up Theme Check for Discord updates Customization Customize your Bunny install App name Replace app icon Uses the Bunny icon instead of Discord\'s Advanced Modify downloading and installation Release channel Download mirror Clear cache automatically Clears cache when Discord gets an update Developer only These settings may break things if used improperly Module location For use with developing the Xposed module. BE CAREFUL Reset module location Package name Discord version Debuggable Enable debuggable flag Alternate lines Wrap lines Bunny Manager version %1$s is now available! Stable Beta Alpha Github Discord Team Special thanks Translate Latest: %1$s Target: %1$s Current: %1$s Install method Default (recommended) Shizuku ================================================ FILE: app/src/main/res/values-or-rIN/strings.xml ================================================ Bunny Manager Author: %1$s License: %1$s Cache cleared successfully Failed to load commits ଯଥାଶୀଘ୍ର ଆସୁଅଛି ଅଵୈଧ Discord ସଂସ୍କରଣ ଆଉ ୭ଟି ଟ୍ୟାପ୍ ଆଉ ୫ଟି ଟ୍ୟାପ୍ ଆଉ ୨ଟି ଟ୍ୟାପ୍ Loading… Cannot downgrade, try uninstalling first You are now a developer In order for Bunny Manager to function, file permissions are required. Since shared data is stored in ~/Bunny, permissions are required in order to access it. Failed to obtain Shizuku permissions Would you like to try again using a download mirror? APK was corrupted, try clearing cache then reinstalling Download was aborted Download failed Could not verify downloaded file, check logs for more details Copied to clipboard Download APKs Patching Installing Downloading base apk Downloading libraries apk Downloading language apk Downloading resources apk Downloading Bunny module Changing app icon Patching app manifests Injecting Bunny Signing APKs Installing APKs Installed successfully Install canceled Failed to install: Unknown reason Installation was blocked One or more APKs were invalid or corrupt Conflicts with an existing app, usually due to mismatched signatures Not enough available storage to install Application is incompatible with this device Installation timed out Ongoing Successful Failed Queued Collapse Expand Go back Copy logs Clear cache Confirm Start update Install Update Reinstall Launch Uninstall Info Retry Reload Open about Open settings Tap to install Bunny for Discord %1$s Exit anyways Nevermind No thanks Try again See previous theme See next theme View logs Show timestamp Copy log Save logs to file Share logs More options Exiting the installer before its finished could corrupt downloaded files, are you sure you want to do that? Cached Installer Home Settings Update available! About Grant Permissions New Discord update available! Warning Download failed Logs Open source libraries Libraries System Light Dark Disabled Quarter hourly Half hourly Hourly Bi-hourly Twice daily Daily Weekly Appearance Change the look and feel of the app Dynamic color Only available on Android 12 and up Theme Check for Discord updates Customization Customize your Bunny install ଆପ୍ ନାମ Replace app icon Uses the Bunny icon instead of Discord\'s Advanced Modify downloading and installation Release channel Download mirror Clear cache automatically Clears cache when Discord gets an update କେଵଳ ଵିକାଶକ These settings may break things if used improperly Module location For use with developing the Xposed module. BE CAREFUL Reset module location Package name Discord version Debuggable Enable debuggable flag Alternate lines Wrap lines Bunny Manager version %1$s is now available! ସ୍ଥିର Beta Alpha Github Discord ଦଳ ଵିଶେଷ ଧନ୍ୟଵାଦ ଅନୁଵାଦ କରନ୍ତୁ ସଦ୍ୟତମ: %1$s ଲକ୍ଷ୍ୟ: %1$s ଚଳିତ: %1$s Install method Default (recommended) Shizuku ================================================ FILE: app/src/main/res/values-pl-rPL/strings.xml ================================================ Menedżer Bunny Author: %1$s License: %1$s Pamięć podręczna pomyślnie wyczyszczona Nie udało się załadować commitów Już niedługo Nieprawidłowa wersja Discorda Jeszcze 7 kliknięć Jeszcze 5 kliknięć Jeszcze 2 kliknięcia Ładowanie… Nie można zainstalować starszej wersji, spróbuj najpierw odinstalować aktualnie zainstalowaną Jesteś teraz deweloperem Aby Menadżer Bunny działał, wymagane są uprawnienia do plików. Ponieważ udostępnione dane są przechowywane w ~/Bunny, dostęp do nich wymaga uprawnień. Nie udało się uzyskać uprawnień Shizuku Czy chcesz spróbować ponownie, używając serwera lustrzanego pobierania? Plik APK został uszkodzony, spróbuj wyczyścić pamięć podręczną i ponownie zainstalować Bunny Pobieranie zostało przerwane Pobieranie nieudane Nie można zweryfikować pobranego pliku, sprawdź logi po więcej szczegółów Skopiowane do schowka Pobieranie plików APK Łatanie Instalowanie Pobieranie base apk Pobieranie libraries apk Pobieranie language apk Pobieranie resources apk Pobieranie modułu Bunny Zmienianie ikony aplikacji Patchowanie manifestów aplikacji Wgrywanie Bunny Podpisywanie plików APK Instalowanie plików APK Pomyślnie zainstalowano Instalacja przerwana Błąd przy instalacji: nieznany powód Instalacja została zablokowana Co najmniej jeden lub więcej plików APK są nieprawidłowe lub uszkodzone Konflikty z istniejącą aplikacją, zazwyczaj z powodu niedopasowanych podpisów Brak dostępnej pamięci do zainstalowania Aplikacja jest niekompatybilna z tym urządzeniem Upłynął limit czasu instalacji W toku Sukces Niepowodzenie W kolejce Zwiń Rozwiń Powrót Kopiuj logi Wyczyść pamięć podręczną Potwierdź Rozpocznij aktualizację Zainstaluj Aktualizuj Reinstaluj Uruchom Odinstaluj Informacje Ponów Odśwież Otwórz o aplikacji Otwórz ustawienia Dotknij, aby zainstalować Bunny dla Discorda %1$s Wyjdź mimo to Nieważne Nie, dziękuję Spróbuj ponownie Zobacz poprzedni motyw Zobacz następny motyw Pokaż logi Pokaż znacznik czasu Kopiuj logi Zapisz logi do pliku Udostępnij logi Więcej opcji Wyjście z instalatora przed zakończeniem instalacji może uszkodzić pobierane pliki, czy na pewno chcesz to zrobić? w pamięci podręcznej Instalator Strona główna Ustawienia Dostępna aktualizacja! O Aplikacji Przyznaj uprawnienia Dostępna jest nowa aktualizacja Discorda! Ostrzeżenie Pobieranie nieudane Logi Open source libraries Libraries System Jasny Ciemny Wyłączony Co kwartał Półgodzinny Raz na godzinę Co dwie godziny Dwa razy dziennie Raz dziennie Raz w tygodniu Wygląd Zmień wygląd i styl aplikacji Dynamiczny kolor Dostępne tylko na Androidzie 12 i nowszym Motyw Sprawdź dostępność aktualizacji Discorda Personalizacja Dostosuj instalację Bunny Nazwa aplikacji Zastąp ikonę aplikacji Używa ikony Bunny zamiast Discorda Zaawansowane Modyfikuj pobieranie i instalację Kanał wydania Serwer lustrzany pobierania Automatycznie czyść pamięć podręczną Czyści pamięć podręczną, kiedy Discord otrzymuje aktualizację Tylko dla deweloperów Te ustawienia mogą zaszkodzić, jeśli używane są nieprawidłowo Lokalizacja modułów Do użycia przy tworzeniu modułu Xposed. BĄDŹ OSTROŻNY Zresetuj lokalizację modułu Nazwa pakietu Wersja Discorda Debugowalne Włącz flagę debugowania Alternatywne linie Zawijaj wiersze Menadżer Bunny w wersji %1$s jest teraz dostępny! Stabilna Beta Alpha Github Discord Zespół Specjalne podziękowania Przetłumacz Najnowsza: %1$s Docelowa: %1$s Zainstalowana: %1$s Metoda instalacji Default (recommended) Shizuku ================================================ FILE: app/src/main/res/values-pt-rBR/strings.xml ================================================ Bunny Manager Autor: %1$s Licença: %1$s Cache limpo com êxito Falha ao carregar commits Disponível em breve Versão inválida do Discord Mais 7 toques Mais 5 toques Mais 2 toques Carregando… Não é possível desatualizar, tente desinstalar primeiro Você agora é um desenvolvedor Para o Gerenciador do Bunny funcionar corretamente, você precisa conceder as permissões de arquivos. Como os dados compartilhados são armazenados em ~/Bunny, as permissões são necessárias para acessa-los. Falha ao obter permissões Shizuku Gostaria de tentar novamente usando uma forma alternativa de download? O APK estava corrompido, tente limpar o cache e reinstale-o Download foi abortado Falha no download Não foi possível verificar o arquivo baixado, verifique os logs para mais detalhes Copiado para área de transferência Baixar APKs Modificando Instalando Baixando apk base Baixando bibliotecas apk Baixando apk de idioma Baixando recursos de apk Baixando módulo Bunny Alterando ícone do aplicativo Modificando manifestos do aplicativo Injetando Bunny Assinando APKs Instalando APKs Instalado com sucesso Instalação cancelada Falha ao instalar: Razão desconhecida A instalação foi bloqueada Um ou mais APKs eram inválidos ou corrompidos Conflitos com um app existente, geralmente devido a assinaturas incompatíveis Não há espaço disponível para instalar O aplicativo é incompatível com esse dispositivo A instalação expirou Em andamento Concluído Falhou Na fila Minimizar Expandir Retornar Copiar logs Limpar cache Confirmar Iniciar atualização Instalar Atualizar Reinstalar Executar Desinstalar Info Repetir Recarregar Abrir sobre Abrir as configurações Toque para instalar o Bunny para o Discord %1$s Sair de qualquer jeito Deixa para lá Não, obrigado Tente novamente Ver tema anterior Ver o próximo tema Ver logs Exibir marcações de tempo Copiar log Salvar logs como arquivo Compartilhar logs Mais opções Sair do instalador antes do seu término pode corromper os arquivos, você tem certeza que deseja fazer isso? Em cache Instalador Início Configurações Atualização disponível! Sobre Conceder Permissões Nova atualização do Discord disponível! Atenção Falha no download Logs Bibliotecas de código aberto Bibliotecas Sistema Claro Escuro Desativado Trimestralmente Meia hora A cada hora A cada duas horas Duas vezes ao dia Diaramente Semanalmente Aparência Altere a aparência do aplicativo Cor dinâmica Disponível somente no Android 12 ou superior Tema Verificar se há atualizações do Discord Customização Personalize sua instalação do Bunny Nome do aplicativo Mudar ícone do aplicativo Usa o ícone do Bunny ao invés do Discord Avançado Modificar download e instalação Canal de lançamento Download alternativo Limpar cache automaticamente Limpa o cache quando o Discord receber uma atualização Somente para Desenvolvedores Estas configurações podem quebrar coisas se forem usadas incorretamente Localização do módulo Para uso com o desenvolvimento do módulo Xposed. TOME CUIDADO Redefinir local do módulo Nome do pacote Versão do Discord Depurável Habilitar modo de depuração Linhas alternativas Quebra automática de linha A versão %1$s do Bunny Manager já está disponível! Estável Beta Alfa GitHub Discord Nossa Equipe Agradecimentos especiais Traduzir Mais recente: %1$s Alvo: %1$s Atual: %1$s Método de instalação Default (recommended) Shizuku ================================================ FILE: app/src/main/res/values-pt-rPT/strings.xml ================================================ Author: %1$s License: %1$s Failed to load commits Coming soon Invalid Discord version 7 more taps 5 more taps 2 more taps Loading… Cannot downgrade, try uninstalling first You are now a developer In order for Bunny Manager to function, file permissions are required. Since shared data is stored in ~/Bunny, permissions are required in order to access it. Failed to obtain Shizuku permissions Would you like to try again using a download mirror? APK was corrupted, try clearing cache then reinstalling Download was aborted Download failed Could not verify downloaded file, check logs for more details Copied to clipboard Download APKs Patching Installing Downloading base apk Downloading libraries apk Downloading language apk Downloading resources apk Downloading Bunny module Changing app icon Patching app manifests Injecting Bunny Signing APKs Installing APKs Installed successfully Install canceled Failed to install: Unknown reason Installation was blocked One or more APKs were invalid or corrupt Conflicts with an existing app, usually due to mismatched signatures Not enough available storage to install Application is incompatible with this device Installation timed out Ongoing Successful Failed Queued Collapse Expand Go back Copy logs Clear cache Confirm Start update Install Update Reinstall Launch Uninstall Info Retry Reload Open about Open settings Tap to install Bunny for Discord %1$s Exit anyways Nevermind No thanks Try again See previous theme See next theme View logs Show timestamp Copy log Save logs to file Share logs More options Exiting the installer before its finished could corrupt downloaded files, are you sure you want to do that? Cached Installer Home Settings Update available! About Grant Permissions New Discord update available! Warning Download failed Logs Open source libraries Libraries System Light Dark Disabled Quarter hourly Half hourly Hourly Bi-hourly Twice daily Daily Weekly Appearance Change the look and feel of the app Dynamic color Only available on Android 12 and up Theme Check for Discord updates Customization Customize your Bunny install App name Replace app icon Uses the Bunny icon instead of Discord\'s Advanced Modify downloading and installation Release channel Download mirror Clear cache automatically Clears cache when Discord gets an update Developer only These settings may break things if used improperly Module location For use with developing the Xposed module. BE CAREFUL Reset module location Package name Discord version Debuggable Enable debuggable flag Alternate lines Wrap lines Bunny Manager version %1$s is now available! Stable Beta Alpha Github Discord Team Special thanks Translate Latest: %1$s Target: %1$s Current: %1$s Install method Default (recommended) Shizuku ================================================ FILE: app/src/main/res/values-ro-rRO/strings.xml ================================================ Managerul Bunny Author: %1$s License: %1$s Memoria cache a fost golita Încărcarea setărilor eșuată În curând Versiune filtru nevalidă încă 7 atingeri Încă 5 atingeri încă 2 atingeri Încărcare… Imposibil de retrogradat, încercați să dezinstalați mai întâi Ați devenit dezvoltator Pentru ca Bunny Manager să funcţioneze, sunt necesare permisiunile fişierelor. Deoarece datele partajate sunt stocate în ~/Bunny, sunt necesare permisiuni pentru a le accesa. Failed to obtain Shizuku permissions Would you like to try again using a download mirror? APK was corrupted, try clearing cache then reinstalling Download was aborted Download failed Could not verify downloaded file, check logs for more details Copied to clipboard Descarcaţi fișier APK Patching Se instalează Descărcarea apk-ului de bază Se descarcă biblioteca %s Se descarcă limba Se descarcă biblioteca %s Se descarca modulul Bunny Schimbați pictograma aplicației Patcheaza manifestele aplicatiei Injectarea Bunny Semnare APK Se instalează APK Instalat cu succes Instalarea a fost anulată Instalarea a eșuat: motiv necunoscut Instalarea a fost blocată Unul sau mai multe APK-uri au fost invalide sau corupte Conflicte cu o aplicație existentă, de obicei din cauza semnăturilor necorespunzătoare Nu este suficient spațiu de stocare disponibil pentru instalare Aplicația este incompatibilă cu acest dispozitiv Instalarea a expirat În desfășurare Succes Eșuat În așteptare Restrânge Extinde Mergi înapoi Copiază jurnalele Golește cache-ul A confirma Începe actualizarea Instalare Actualizare Reinstalare Lansați Dezinstalează Informații Reîncearcă Reload Open about Deschideți setările Tap to install Bunny for Discord %1$s Exit anyways Nevermind No thanks Try again See previous theme See next theme View logs Show timestamp Copy log Save logs to file Share logs More options Exiting the installer before its finished could corrupt downloaded files, are you sure you want to do that? Memorie cache Instalare Acasă Setări Actualizare disponibilă! Despre Acordă permisiuni New Discord update available! Warning Download failed Logs Open source libraries Libraries Sistem Luminos Întunecată Disabled Quarter hourly Half hourly Hourly Bi-hourly Twice daily Daily Weekly Aspect Change the look and feel of the app Culoare dinamică Disponibil doar pe Android 12 și sus Temă Check for Discord updates Customization Customize your Bunny install Numele aplicației Înlocuiţi pictograma aplicaţiei Folosește icoana Bunny în loc de Discord Avansat Modify downloading and installation Canal de lansare Download mirror Clear cache automatically Clears cache when Discord gets an update Doar dezvoltatori These settings may break things if used improperly Module location For use with developing the Xposed module. BE CAREFUL Reset module location Numele pachetului Versiune Discord Depanare Activează marcajul depanabil Alternate lines Wrap lines Versiunea Bunny Manager %1$s este acum disponibilă! Stabil Beta Alpha GitHub Discord Echipă Mulțumiri speciale Traduce Cel mai recent: %1$s Tinta %1$s Curent: %1$s Install method Default (recommended) Shizuku ================================================ FILE: app/src/main/res/values-ru-rRU/strings.xml ================================================ Bunny Manager Автор: %1$s Лицензия: %1$s Кеш успешно очищен Не удалось загрузить фиксации Скоро Некорректная Discord версия Ещё 7 нажатий Ещё 5 нажатий Ещё 2 нажатия Загрузка… Не удалось понизить версию, сначала попробуйте удалить Вы стали разработчиком Чтобы Bunny Manager работал правильно, разрешения на доступ к файлам обязательны. Так как данные хранятся в ~/Bunny, выдача разрешений необходима для доступа к этому пути. Не удалось получить разрешения Shizuku Хотели бы вы попробовать еще раз, используя зеркало для скачивания? Пакет был повреждён, попробуйте очистить кеш, а потом повторить установку Скачивание было прервано Скачивание не удалось Не удалось проверить загруженный файл, проверьте журналы для получения более подробной информации Скопировано в буфер обмена Скачивание пакетов Патчинг Установка Скачивание базового пакета Скачивание библиотек пакета Скачивание языкового пакета Скачивание ресурсных пакетов Скачивание Bunny модуля Изменение значка приложения Патчинг манифестов приложения Внедрение Bunny Подписывание пакетов Установка пакетов Успешно установлено Установка отменена Не удалось установить: Неизвестная причина Установка была заблокирована Один или несколько пакетов были недействительны или повреждены Конфликтует с уже установленным приложением, обычно из-за несоответствующих подписей Недостаточно свободного места для установки Приложение несовместимо с данным устройством Время ожидания установки истекло В процессе Успешно Не удалось В очереди Свернуть Развернуть Назад Скопировать логи Очистить кеш Подтвердить Начать обновление Установить Обновить Переустановить Запустить Удалить Информация Повторить Перезагрузить Открыть О приложении Открыть настройки Нажмите, чтобы установить Bunny для Discord %1$s Всё равно выйти Не сейчас Нет, спасибо Попробовать снова Увидеть предыдущую тему Увидеть следующую тему Посмотреть логи Показать временную метку Скопировать лог Сохранить логи в файл Поделиться логами Дополнительные параметры Выход из программы установки до её завершения может привести к повреждению загруженных файлов, вы уверены, что хотите это сделать? Кешировано Установщик Главная Настройки Доступно обновление! О приложении Предоставить разрешения Доступно новое обновление Discord! Предупреждение Скачивание не удалось Логи Библиотеки с открытым исходным кодом Библиотеки Системная Светлая Тёмная Отключено Каждую четверть часа Каждые полчаса Каждый час Каждый второй час Дважды в день Ежедневно Еженедельно Внешний вид Изменить внешний вид и ощущения от приложения Динамический цвет Доступно только на Android 12 и выше Тема Проверять на обновления Discord Кастомизация Кастомизируйте вашу Bunny установку Название приложения Заменить значок приложения Использует иконку Bunny вместо Discord Дополнительные Изменить скачивание и установку Канал выпуска Зеркало скачивания Очищать кеш автоматически Очищает кеш, когда Discord получает обновление Только для разработчика Эти настройки могут сломать некоторые вещи, если их неправильно использовать Расположение модуля Для использования при разработке Xposed модуля. БУДЬТЕ ОСТОРОЖНЫ Сбросить расположение модуля Название пакета Версия Discord Отлаживаемый Включить отладочный флаг Альтернативные строки Перенос строк Доступна версия %1$s для Bunny Manager! Стабильный Бета Альфа Github Discord Команда Особая благодарность Перевести Последняя: %1$s Выбранная: %1$s Текущая: %1$s Метод установки По умолчанию (рекомендуется) Shizuku ================================================ FILE: app/src/main/res/values-sr-rSP/strings.xml ================================================ Bunny Manager Author: %1$s License: %1$s Cache cleared successfully Failed to load commits Coming soon Invalid Discord version 7 more taps 5 more taps 2 more taps Loading… Cannot downgrade, try uninstalling first You are now a developer In order for Bunny Manager to function, file permissions are required. Since shared data is stored in ~/Bunny, permissions are required in order to access it. Failed to obtain Shizuku permissions Would you like to try again using a download mirror? APK was corrupted, try clearing cache then reinstalling Download was aborted Download failed Could not verify downloaded file, check logs for more details Copied to clipboard Download APKs Patching Installing Downloading base apk Downloading libraries apk Downloading language apk Downloading resources apk Downloading Bunny module Changing app icon Patching app manifests Injecting Bunny Signing APKs Installing APKs Installed successfully Install canceled Failed to install: Unknown reason Installation was blocked One or more APKs were invalid or corrupt Conflicts with an existing app, usually due to mismatched signatures Not enough available storage to install Application is incompatible with this device Installation timed out Ongoing Successful Failed Queued Collapse Expand Go back Copy logs Clear cache Confirm Start update Install Update Reinstall Launch Uninstall Info Retry Reload Open about Open settings Tap to install Bunny for Discord %1$s Exit anyways Nevermind No thanks Try again See previous theme See next theme View logs Show timestamp Copy log Save logs to file Share logs More options Exiting the installer before its finished could corrupt downloaded files, are you sure you want to do that? Cached Installer Home Settings Update available! About Grant Permissions New Discord update available! Warning Download failed Logs Open source libraries Libraries System Light Dark Disabled Quarter hourly Half hourly Hourly Bi-hourly Twice daily Daily Weekly Appearance Change the look and feel of the app Dynamic color Only available on Android 12 and up Theme Check for Discord updates Customization Customize your Bunny install App name Replace app icon Uses the Bunny icon instead of Discord\'s Advanced Modify downloading and installation Release channel Download mirror Clear cache automatically Clears cache when Discord gets an update Developer only These settings may break things if used improperly Module location For use with developing the Xposed module. BE CAREFUL Reset module location Package name Discord version Debuggable Enable debuggable flag Alternate lines Wrap lines Bunny Manager version %1$s is now available! Stable Beta Alpha Github Discord Team Special thanks Translate Latest: %1$s Target: %1$s Current: %1$s Install method Default (recommended) Shizuku ================================================ FILE: app/src/main/res/values-sv-rSE/strings.xml ================================================ Bunny Hanteraren Author: %1$s License: %1$s Cachen har lyckats rensats Misslyckades att ladda comitter Kommer snart Ogiltig Discord version 7 tryck till 5 tryck till 2 tryck till Laddar… Kan inte nedgradera, försök att avinstallera först Du är nu en utvecklare För att Bunny Hanteraren ska kunna fungera så krävs filbehörigheter. Eftersom delad data lagras i ~/Bunny så krävs behörigheter för att komma åt den. Misslyckades med att få Shizuku-behörigheter Vill du försöka igen med hjälp av en speglad nedladdning? APK:n blev korrupterad, försök att rensa cachen och sedan ominstallera Nedladdningen avbröts Nedladdning misslyckades Could not verify downloaded file, check logs for more details Copied to clipboard Ladda ner APKer Patchar Installerar Laddar ner bas apk Laddar ner programbiblioteks apk Laddar ner språk apk Laddar ner resurs apk Laddar ner Bunny modul Ändrar appikonen Patchar app manifester Injekterar Bunny Signerar APK:er Installerar APKer Installation klar Installeringen avbröts Misslyckades att installera: Okänd orsak Installationen var blockerad En eller flera APK:er var ogiltiga eller korrupta Konflikter med en befintlig app, vanligtvis på grund av missmatchade signaturer Inte tillräckligt med lagringsutrymme för att installera Applikationen är inkompatibel med den här enheten Installationen tog för lång tid Pågående Lyckades Misslyckades Köad Kollapsa Expandera Gå tillbaka Kopiera loggar Rensa cache Bekräfta Starta uppdatering Installera Uppdatera Ominstallera Kör Avinstallera Info Försök igen Ladda om Öppna \"om\" Öppna inställningar Tryck för att installera Bunny för Discord %1$s Gå ut ändå Glöm det Nej tack Försök igen Visa föregående tema Visa nästa tema View logs Show timestamp Copy log Save logs to file Share logs More options Att gå ur installationen innan den är klar kan korruptera nerladdade filer, är du säker på att du vill göra detta? Cachad Installerare Hem Inställningar Uppdatering tillgänglig! Om Ge Behörigheter Ny Discord-uppdatering tillgänglig! Varning Nedladdning misslyckades Logs Open source libraries Libraries System Ljust Mörkt Inaktiverad Varje kvart Varje halvtimme Varje timme Varannan timme Två gånger om dagen Dagligen Varje vecka Utseende Ändra appens utseende och känsla Dynamisk färg Endast tillgänglig på Android 12 och uppåt Tema Sök efter Discord-uppdateringar Anpassningar Skräddarsy din Bunny installation Namn på appen Ersätt appikonen Använder Bunnys ikon istället för Discords Avancerat Modify downloading and installation Stabil kanal Speglad nedladdning Rensa cache automatiskt Rensar cachen när Discord får en uppdatering Endast utvecklare These settings may break things if used improperly Modulens plats För utveckling av Xposed modulen. VAR FÖRSIKTIG Återställ modulens plats Namn på paket Discord-version Felsökbar Aktivera felsökbar flagga Alternate lines Wrap lines Bunny Manager version %1$s är nu tillgänglig! Stabil Beta Alpha GitHub Discord Team Särskilt tack Översätt Senaste: %1$s Mål: %1$s Nuvarande: %1$s Installations metoder Standard (rekommenderad) Shizuku ================================================ FILE: app/src/main/res/values-tr-rTR/strings.xml ================================================ Bunny Manager Yapan: %1$s Lisans: %1$s Önbellek başarıyla temizlendi İçerik yüklenemedi Yakında Geçersiz Discord sürümü 7 kez daha basın 5 kez daha basın 2 kez daha basın Yükleniyor… Sürüm düşürülemiyor, önce kaldırmayı deneyin Artık bir geliştiricisiniz Bunny Manager\'ın düzgün çalışması için dosya izinleri gereklidir. Paylaşılan uygulama verileri ~/Bunny\'da depolandığından, dosyalara erişim için izin gerekmektedir. Shizuku izinleri alınamadı Alternatif bir kaynak kullanarak tekrar denemek ister misiniz? APK bozuk. Önbelleği temizleyip yeniden yüklemeyi deneyin İndirme işlemi iptal edildi İndirme işlemi başarısız oldu İndirilen dosya doğrulanamadı, daha fazla detay için günlükleri kontrol edin Panoya kopyalandı APK\'ları indir Yamalanıyor Kuruluyor Temel apk indiriliyor Kütüphane apk\'ları indiriliyor Dil apk\'sı indiriliyor Kaynak apk\'sı indiriliyor Bunny modülü indiriliyor Uygulama simgesi değiştiriliyor Uygulama manifest\'i yamalanıyor Bunny enjekte ediliyor APK\'lar imzalanıyor APK\'lar kuruluyor Başarıyla kuruldu Kurulum iptal edildi Kurulum başarısız oldu: Bilinmeyen sebep Kurulum engellendi Bir ya da daha fazla APK geçersiz ya da bozuk Mevcut bir uygulamayla çakışıyor, genellikle uyumsuz imzalar sebep olur Kurmak için yeterli depolama alanı yok Uygulama bu cihaz ile uyumsuz Kurulum zaman aşımına uğradı Devam ediyor Başarılı Başarısız oldu Sıraya alındı Daralt Genişlet Geri dön Günlükleri kopyala Önbelleği temizle Onayla Güncellemeyi başlat Kur Güncelle Yeniden kur Başlat Kaldır Bilgi Yeniden dene Yenile Hakkındayı aç Ayarları aç Discord %1$s için Bunny\'yı kurmak için dokunun Yine de çık Boş ver Hayır teşekkürler Tekrar deneyin Önceki temayı göster Sonraki temayı göster Günlükleri göster Zamanı göster Günlüğü kopyala Günlükleri dosyaya kaydet Günlükleri paylaş Diğer seçenekler Yükleme bitmeden çıkmak indirilen dosyaları bozabilir, bunu yapmak istediğinizden emin misiniz? Önbelleğe alındı Yükleyici Ana Sayfa Ayarlar Güncelleme mevcut! Hakkında İzinleri Ver Yeni Discord güncellemesi mevcut! Uyarı İndirme işlemi başarısız Günlükler Açık kaynak kütüphaneleri Kütüphaneler Sistem Açık Koyu Devre dışı 15 dakikada bir Yarım saatte bir Saatte bir İki saatte bir Günde iki Günde bir Haftada bir Görünüm Uygulamanın görünümünü ve havasını değiştirin Dinamik renk Sadece Android 12 ve üzerinde çalışır Tema Discord güncellemelerini denetle Özelleştirme Bunny kurulumunuzu özelleştirin Uygulama ismi Uygulama simgesini değiştir Discord\'unki yerine Bunny\'nın simgesini kullanır Gelişmiş İndirmeyi ve kurulumu ayarlayın Yayın kanalı Alternatif indirme Önbelleği otomatik olarak temizle Discord güncelleme aldığında önbelleği temizler Yalnızca geliştiriciler için Bu ayarlar yanlış kullanıldığında bir şeyleri bozabilir Modül konumu Xposed modülünü geliştirmek içindir. DİKKATLİ OLUN Modül konumunu sıfırla Paket ismi Discord sürümü Hata ayıklanabilir Hata ayıklanabilir yap Satırları ayır Satırları kaydır Bunny Manager sürüm %1$s yayınlandı! Kararlı Beta Alfa GitHub Discord Ekip Özel teşekkürler Tercüme et En son: %1$s Hedef: %1$s Şu anki: %1$s Kurulum yöntemi Varsayılan (önerilen) Shizuku ================================================ FILE: app/src/main/res/values-uk-rUA/strings.xml ================================================ Менеджер Bunny Author: %1$s License: %1$s Кеш успішно очищено Не вдалося завантажити коміти Незабаром Невірна версія Discord Ще 7 натисків Ще 5 натисків Ще 2 натиску Завантаження… Неможливо понизити версію, спробуйте спочатку видалити Тепер ви – розробник Для того, щоб Bunny Менеджер функціонував, потрібен доступ до файлів. Оскільки спільні дані зберігаються в ~/Bunny, для доступу необхіден дозвіл. Failed to obtain Shizuku permissions Бажаєте спробувати ще раз, використовуючи дзеркало завантаження? APK-файл пошкоджений, спробуйте очистити кеш, а потім перевстановити Download was aborted Download failed Could not verify downloaded file, check logs for more details Copied to clipboard Завантаження APK-файлів Патчінг Встановлення Завантаження основного apk Завантаження apk бібліотек Завантаження мовних apk Завантаження ресурсних apk Завантаження модуля Bunny Змінення іконки додатку Пропатчування маніфестів додатку Ін\'єкція Bunny Сертифікація APK-файлів Встановлення APK-файлів Успішно встановлено Встановлення скасовано Не вдалося встановити: невідома причина Встановлення було заблоковано Один чи декілька APK-файлів були невірні або пошкоджені Конфліктує з вже встановленою програмою, зазвичай з-за невідповідних сертифікацій Недостатньо вільної пам\'яті для встановлення Додаток несумісний з цим пристроєм Час очікування встановлення закінчився Поточний Успішний Невдалий У черзі Згорнути Розгорнути Повернутися Копіювати логи Очистити кеш Підтвердити Почати оновлення Встановити Оновити Перевстановити Запустити Видалити Інфо Повторити Оновити Відкрити про Відкрити налаштування Натисни, щоб встановити Bunny до Discord %1$s Вийти у будь-якому разі Не зараз Ні дякую Спробувати знову See previous theme See next theme View logs Show timestamp Copy log Save logs to file Share logs More options Вихід з програми установки перед її завершенням може зіпсуватися завантажені файли. Ви впевнені, що хочете це зробити? Кешовано Встановлювач Головна Налаштування Доступне оновлення! Про Надати дозволи Доступне нове оновлення Discord! Попередження Помилка завантаження Logs Open source libraries Libraries Системна Світла Темна Вимкнено Кожну чверть години Кожні пів години Кожну годину Кожну другу годину Двічі на день Щодня Щотижня Оформлення Change the look and feel of the app Динамічні кольори Доступно лише для Android 12 та вище Тема Перевіряти на оновлення Discord Customization Customize your Bunny install Назва додатку Замінити іконку додатку Використовує іконку Bunny замість Discord Розширені Modify downloading and installation Канал випуску Дзеркальне завантаження Очищати кеш автоматично Очищає кеш, коли Discord отримує оновлення Тільки для розробників These settings may break things if used improperly Розташування модуля Використовується для розробки Xposed модуля. БУДЬТЕ ОБЕРЕЖНІ Відновити розташування модуля Назва пакету Версія Discord Налагоджуваний Увімкнути налагоджувальний прапорець Alternate lines Wrap lines Версія Bunny Менеджер %1$s вже доступна! Стабільна Бета Альфа Github Discord Команда Окрема подяка Перекласти Остання: %1$s Цільова: %1$s Поточна: %1$s Install method Default (recommended) Shizuku ================================================ FILE: app/src/main/res/values-vi-rVN/strings.xml ================================================ Trình quản lý Bunny Author: %1$s License: %1$s Đã xóa bộ nhớ đệm thành công Tải các cập nhật phiên bản thất bại Sắp ra mắt Phiên bản Discord không hợp lệ Nhấn thêm 7 lần nữa Nhấn thêm 5 lần nữa Nhấn thêm 2 lần nữa Đang tải… Không thể hạ cấp, hãy thử gỡ cài đặt trước Bạn hiện là nhà phát triển Để Trình quản lý Bunny hoạt động, bạn cần cấp quyền truy cập tệp. Do dữ liệu chia sẻ được lưu trữ trong ~/Bunny, nên cần có quyền để truy cập dữ liệu đó. Nhận quyền của Shizuku thất bại Bạn có muốn thử lại bằng cách sử dụng máy chủ tải xuống khác không? APK bị hỏng, hãy thử xóa bộ nhớ đệm rồi cài đặt lại Tải xuống đã bị hủy bỏ Tải xuống thất bại Không thể xác minh tệp đã tải, hãy kiểm tra nhật ký để biết thêm chi tiết Copied to clipboard Tải xuống APKs Đang vá Đang cài đặt Đang tải xuống base.apk Đang tải xuống các thư viện apk Đang tải xuống ngôn ngữ apk Đang tải xuống tài nguyên apk Đang tải xuống mô-đun Bunny Đang thay đổi biểu tượng app Đang vá tệp kê khai ứng dụng Đang triển khai Bunny Đang ký tệp APK Đang cài đặt APKs Đã cài đặt thành công Đã hủy cài đặt Cài đặt thất bại: Không rõ lý do Cài đặt đã bị chặn Một hoặc nhiều tệp APK không hợp lệ hoặc bị hỏng Xung đột với ứng dụng hiện có, thường là do chữ ký không trùng khớp Không đủ bộ nhớ khả dụng để cài đặt Ứng dụng không tương thích với thiết bị này Hết thời gian chờ cho cài đặt Đang thực hiện Thành công Đã thất bại Trong hàng chờ Thu gọn Mở rộng Quay lại Sao chép nhật ký Xóa bộ nhớ đệm Xác nhận Bắt đầu cập nhật Cài đặt Cập nhật Cài đặt lại Khởi chạy Gỡ cài đặt Thông tin Thử lại Tải lại Mở về Mở cài đặt Nhấn để cài đặt Bunny cho Discord %1$s Vẫn thoát Bỏ qua Không, cảm ơn Thử lại Xem chủ đề trước Xem chủ đề tiếp theo View logs Show timestamp Copy log Save logs to file Share logs More options Việc thoát khỏi trình cài đặt trước khi hoàn tất có thể làm hỏng các tệp đã tải, bạn có chắc muốn làm điều đó? Đã lưu trong bộ nhớ đệm Trình cài đặt Trang chủ Cài đặt Đã có bản cập nhật mới! Về chúng tôi Cấp quyền Discord đã có bản cập nhật mới! Cảnh báo Tải xuống thất bại Logs Open source libraries Libraries Hệ thống Sáng Tối Tắt Mỗi 15 phút Thời gian nửa giờ Mỗi giờ Mỗi 2 giờ Hai lần mỗi ngày Hàng ngày Hằng tuần Giao diện Thay đổi cái nhìn và cảm nhận của ứng dụng Màu động Chỉ khả dụng trên Android 12 trở lên Chủ đề Kiểm tra bản cập nhật Discord Tuỳ biến Tùy chỉnh cài đặt Bunny của bạn Tên ứng dụng Thay thế biểu tượng ứng dụng Sử dụng biểu tượng Bunny thay vì của Discord Nâng cao Sửa đổi tải xuống và cài đặt Kênh phát hành Máy chủ tải xuống Xóa bộ nhớ đệm tự động Xóa bộ đệm khi Discord có bản cập nhật Chỉ dành cho nhà phát triển Những cài đặt này có thể làm hỏng mọi thứ nếu dùng không đúng cách Vị trí mô-đun Để sử dụng để phát triển mô-đun Xposed. HÃY CẨN THẬN Đặt lại vị trí mô-đun Tên gói Phiên bản Discord Gỡ lỗi Bật tùy chọn gỡ lỗi Alternate lines Wrap lines Trình quản lý Bunny phiên bản %1$s đang khả dụng! Ổn định Beta Alpha Github Discord Nhóm Đặc biệt cảm ơn Dịch bởi Mới nhất: %1$s Mục tiêu: %1$s Hiện tại: %1$s Cách thức cài đặt Default (recommended) Shizuku ================================================ FILE: app/src/main/res/values-zh-rCN/strings.xml ================================================ Bunny 管理器 作者:%1$s 许可证:%1$s 缓存清除成功 无法加载提交 敬请期待 无效的 Discord 版本 请再点击 7 次 请再点击 5 次 请再点击 2 次 载入中… 无法降级,请先尝试卸载 您现在已成为开发者 为了使 Bunny Manager 正常运作,您需要授予「管理所有文件」权限。由于数据存储在 ~/Bunny 中,因此需要权限才能读取它。 无法获取Shizuku权限 您想使用下载镜像再试一次吗? APK已损坏,请尝试清除缓存后再重新安装 下载已中止 下载失败 无法校验下载的文件,请检查日志以了解详细信息 复制到剪贴板 下载 APK 补丁中 安装中 正在下载基础 apk 正在下载函式库 apk 正在下载语言 apk 正在下载资源 apk 正在下载 Bunny 模块 正在变更应用程序图标 正在安装补丁应用程序清单 正在注入 Bunny 正在签署 apk 正在安装 apk 安装成功 已取消安装 安装失败:未知的原因 安装已被阻止 一个或多个 apk 损坏或无效 与现有应用程序冲突,通常是因为签署名不相符造成的 您没有足够的存储空间来安装 此应用程序与您的设备不兼容 安装超时 进行中 成功 失败 待处理 折迭 展开 返回 复制日志 清除缓存 确认 开始更新 安装 更新 重新安装 启动 卸载 信息 重试 刷新 打开关于 打开设置 点击为 Discord 安装 Bunny Manager %1$s 坚持推出 没关系 残忍拒绝 重试 查看上一个主题 查看下一个主题 查看日志 显示时间戳 复制日志 将日志保存到文件目录 分享日志 更多选项 在安装程序完成之前退出可能会损坏下载的文件,您确定要这样做吗? 已缓存 安装器 首页 设置 更新可供使用! 关于 授予权限 有新的 Discord 更新可用! 警告 下载失败 日志 开源库 系统 浅色 深色 停用 每 15 分钟 每半小时 每小时 每两小时 每天两次 每天 每周 外观 更改应用程序的外观。 动态颜色 仅适用于 Android 12 及更高的版本 主题 检测 Discord 更新 个性化 自定义您的 Bunny 安装 应用名称 更换应用程示图标 使用 Bunny 图标代替 Discord 图标 高级 修改下载和安装 发布通道 下载镜像 自动清除缓存 当 Discord 更新时清除缓存 仅限开发者 这些设置如果使用不当可能会破坏内容 模块位置 用于开发 Xposed 模块, 请小心 重置模块位置 包名 Discord 版本 可调试 启用调试标志 备用路线 Wrap路线 Bunny Manager 版本 %1$s 已可供使用! 稳定版 测试版 早期开发版 GitHub Discord 我们的团队 特别感谢 协助翻译 最新版本:%1$s 目标:%1$s 目前版本:%1$s 选择安装方法 软件包安装程序 (推荐) Shizuku ================================================ FILE: app/src/main/res/values-zh-rTW/strings.xml ================================================ Bunny Manager 作者:%1$s 協議:%1$s 快取清除完成 無法載入提交 敬請期待 無效的 Discord 版本 再點擊 7 次 再點擊 5 次 再點擊 2 次 載入中… 無法降級,請先嘗試解除安裝 您現在已成為開發人員 為了使 Bunny Manager 正常運作,您需要授予「管理所有檔案」權限。由於資料儲存在 ~/Bunny 中,因此需要權限才能讀取它。 取得 Shizuku 權限失敗 您想使用下載鏡像再試一次嗎? APK 已損壞,請嘗試清除快取然後重新安裝 下載中斷 下載失敗 無法驗證已下載的檔案,檢視記錄檔以獲得更多詳細資訊 已複製到剪貼簿 下載 APK 安裝修補工具中 安裝中 正在下載基礎 apk 正在下載函式庫 apk 正在下載語言 apk 正在下載資源 apk 正在下載 Bunny 模組 正在變更應用程式圖示 正在安裝應用程式修補工具 正在注入 Bunny 正在簽署 apk 正在安裝 apk 成功安裝 已取消安裝 安裝失敗:未知的原因 安裝已被阻止 一個或多個 apk 損壞或無效 與現有應用程式衝突,通常是因為簽署名不相符造成的 您沒有足夠的儲存空間來安裝 此應用程式與您的裝置不相容 安裝超時 進行中 成功 失敗 待處理 折疊 展開 返回 複製記錄檔 清除快取 確定 開始更新 安裝 更新 重新安裝 啟動 解除安裝 資訊 重試 重新整理 開啟關於 開啟設定 點擊為 Discord 安裝 Bunny Manager %1$s 無論如何都要退出 沒關係 不用 重試 看上一個主題 看下一個主題 檢視記錄檔 顯示更新時間 複製記錄檔 儲存記錄檔到檔案 分享記錄檔 更多選項 在安裝程序完成之前退出可能會損壞下載的檔案,您確定要這樣做嗎? 已快取 安裝程式 首頁 設定 有可供使用的更新! 關於 授予權限 有可供使用的新 Discord 更新! 警告 下載失敗 記錄檔 開放原始碼函式庫 函式庫 系統 淺色 深色 停用 每 15 分鐘 每半小時 每小時 每兩小時 每天兩次 每天 每週 外觀 變更應用程式的外觀 動態顏色 僅適用於 Android 12 及更高的版本 主題 檢查 Discord 更新 客製化 客製化安裝您的 Bunny 應用程式名稱 更換應用程示圖示 使用 Bunny 圖示代替 Discord 圖示 進階 修改下載與安裝 發布通道 下載鏡像 自動清除快取 當 Discord 更新時清除快取 僅限開發人員 如果使用不當,可能會損壞這些設定 模組位置 請小心,此用於開發 Xposed 模組。 重設模組位置 套件名稱 Discord 版本 除錯模式 啟用除錯模式 取代行線 換行線 Bunny Manager 版本 %1$s 已可供使用! 穩定版(Stable) 測試版(Beta) 早期開發版(Alpha) GitHub Discord 團隊 特別感謝 協助翻譯 最新版本:%1$s 目標:%1$s 目前版本:%1$s 安裝方法 Default (recommended) Shizuku ================================================ FILE: app/src/main/res/xml/backup_rules.xml ================================================ ================================================ FILE: app/src/main/res/xml/data_extraction_rules.xml ================================================ ================================================ FILE: app/src/main/res/xml/locales_config.xml ================================================ ================================================ FILE: app/src/main/res/xml/provider_paths.xml ================================================ ================================================ FILE: build.gradle.kts ================================================ // Top-level build file where you can add configuration options common to all sub-projects/modules. plugins { alias(libs.plugins.aboutlibraries) apply false alias(libs.plugins.android.application) apply false alias(libs.plugins.android.library) apply false alias(libs.plugins.kotlin.android) apply false alias(libs.plugins.compose.compiler) apply false } tasks.withType().all { duplicatesStrategy = DuplicatesStrategy.INCLUDE } allprojects { repositories { google() mavenCentral() maven("https://maven.aliucord.com/snapshots") maven("https://jitpack.io") } } ================================================ FILE: gradle/libs.versions.toml ================================================ [versions] # Plugins agp = "8.8.1" kotlin = "2.0.21" # AndroidX androidx-core-ktx = "1.15.0" androidx-core-splashscreen = "1.0.1" androidx-lifecycle-runtime = "2.8.7" androidx-paging-compose = "3.3.4" androidx-work-runtime = "2.10.0" # Compose compose-activity = "1.9.3" compose-bom = "2024.11.00" # Misc. aboutlibraries = "11.1.3" accompanist = "0.36.0" binary-resources = "2.0.0" coil = "2.7.0" koin = "4.0.0" kotlinx-datetime = "0.6.1" kotlinx-collections = "0.3.8" ktor = "3.0.1" shizuku = "13.1.0" voyager = "1.1.0-beta03" zip-android = "2.1.1" [libraries] # Accompanist accompanist-pager = { group = "com.google.accompanist", name = "accompanist-pager", version.ref = "accompanist" } accompanist-permissions = { group = "com.google.accompanist", name = "accompanist-permissions", version.ref = "accompanist" } accompanist-systemuicontroller = { group = "com.google.accompanist", name = "accompanist-systemuicontroller", version.ref = "accompanist" } # AndroidX androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "androidx-core-ktx" } androidx-core-splashscreen = { group = "androidx.core", name = "core-splashscreen", version.ref = "androidx-core-splashscreen" } androidx-lifecycle-runtime = { group = "androidx.lifecycle", name = "lifecycle-runtime-ktx", version.ref = "androidx-lifecycle-runtime" } androidx-paging-compose = { group = "androidx.paging", name = "paging-compose", version.ref = "androidx-paging-compose" } androidx-work-runtime = { group = "androidx.work", name = "work-runtime-ktx", version.ref = "androidx-work-runtime" } # Coil coil = { group = "io.coil-kt", name = "coil", version.ref = "coil" } coil-compose = { group = "io.coil-kt", name = "coil-compose", version.ref = "coil" } # Compose compose-activity = { group = "androidx.activity", name = "activity-compose", version.ref = "compose-activity" } compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "compose-bom" } compose-material3 = { group = "androidx.compose.material3", name = "material3" } compose-material-icons-extended = { group = "androidx.compose.material", name = "material-icons-extended" } compose-ui = { group = "androidx.compose.ui", name = "ui" } compose-ui-graphics = { group = "androidx.compose.ui", name = "ui-graphics" } compose-ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-tooling-preview" } # Koin koin-android = { group = "io.insert-koin", name = "koin-android", version.ref = "koin" } koin-androidx-compose = { group = "io.insert-koin", name = "koin-androidx-compose", version.ref = "koin" } koin-core = { group = "io.insert-koin", name = "koin-core", version.ref = "koin" } # Ktor ktor-client-core = { group = "io.ktor", name = "ktor-client-core", version.ref = "ktor" } ktor-client-cio = { group = "io.ktor", name = "ktor-client-cio", version.ref = "ktor" } ktor-client-content-negotiation = { group = "io.ktor", name = "ktor-client-content-negotiation", version.ref = "ktor" } ktor-serialization-kotlinx-json = { group = "io.ktor", name = "ktor-serialization-kotlinx-json", version.ref = "ktor" } ktor-client-logging = { group = "io.ktor", name = "ktor-client-logging", version.ref = "ktor" } # Shizuku shizuku-api = { group = "dev.rikka.shizuku", name = "api", version.ref = "shizuku" } shizuku-provider = { group = "dev.rikka.shizuku", name = "provider", version.ref = "shizuku" } # Voyager voyager-navigator = { group = "cafe.adriel.voyager", name = "voyager-navigator", version.ref = "voyager" } voyager-screenmodel = { group = "cafe.adriel.voyager", name = "voyager-screenmodel", version.ref = "voyager" } voyager-tab-navigator = { group = "cafe.adriel.voyager", name = "voyager-tab-navigator", version.ref = "voyager" } voyager-transitions = { group = "cafe.adriel.voyager", name = "voyager-transitions", version.ref = "voyager" } voyager-koin = { group = "cafe.adriel.voyager", name = "voyager-koin", version.ref = "voyager" } # Misc. aboutlibraries-core = { group = "com.mikepenz", name = "aboutlibraries", version.ref = "aboutlibraries" } binaryResources = { module = "com.aliucord:binary-resources", version.ref = "binary-resources" } kotlinx-datetime = { group = "org.jetbrains.kotlinx", name = "kotlinx-datetime", version.ref = "kotlinx-datetime" } kotlinx-collections = { group = "org.jetbrains.kotlinx", name = "kotlinx-collections-immutable", version.ref = "kotlinx-collections" } zip-android = { group = "io.github.diamondminer88", name = "zip-android", version.ref = "zip-android" } [bundles] accompanist = ["accompanist-pager", "accompanist-permissions", "accompanist-systemuicontroller"] androidx = ["androidx-work-runtime", "androidx-paging-compose", "androidx-lifecycle-runtime", "androidx-core-splashscreen", "androidx-core-ktx"] coil = ["coil", "coil-compose"] compose = ["compose-activity", "compose-material-icons-extended", "compose-material3", "compose-ui", "compose-ui-graphics", "compose-ui-tooling-preview"] koin = ["koin-android", "koin-androidx-compose", "koin-core"] ktor = ["ktor-client-cio", "ktor-client-content-negotiation", "ktor-client-core", "ktor-client-logging", "ktor-serialization-kotlinx-json"] shizuku = ["shizuku-api", "shizuku-provider"] voyager = ["voyager-koin", "voyager-navigator", "voyager-tab-navigator", "voyager-transitions", "voyager-screenmodel"] [plugins] aboutlibraries = { id = "com.mikepenz.aboutlibraries.plugin", version.ref = "aboutlibraries"} android-application = { id = "com.android.application", version.ref = "agp" } android-library = { id = "com.android.library", version.ref = "agp" } kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } compose-compiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" } kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" } ================================================ FILE: gradle/wrapper/gradle-wrapper.properties ================================================ #Wed Aug 09 19:03:47 EDT 2023 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists ================================================ FILE: gradle.properties ================================================ # 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 # Enable R8 full mode. android.enableR8.fullMode=true ================================================ FILE: gradlew ================================================ #!/usr/bin/env sh # # Copyright 2015 the original author or authors. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # ############################################################################## ## ## Gradle start up script for UN*X ## ############################################################################## # Attempt to set APP_HOME # Resolve links: $0 may be a link PRG="$0" # Need this for relative symlinks. while [ -h "$PRG" ] ; do ls=`ls -ld "$PRG"` link=`expr "$ls" : '.*-> \(.*\)$'` if expr "$link" : '/.*' > /dev/null; then PRG="$link" else PRG=`dirname "$PRG"`"/$link" fi done SAVED="`pwd`" cd "`dirname \"$PRG\"`/" >/dev/null APP_HOME="`pwd -P`" cd "$SAVED" >/dev/null APP_NAME="Gradle" APP_BASE_NAME=`basename "$0"` # 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"' # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD="maximum" warn () { echo "$*" } die () { echo echo "$*" echo exit 1 } # 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 ;; 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" which java >/dev/null 2>&1 || 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 # Increase the maximum file descriptors if we can. if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then MAX_FD_LIMIT=`ulimit -H -n` if [ $? -eq 0 ] ; then if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then MAX_FD="$MAX_FD_LIMIT" fi ulimit -n $MAX_FD if [ $? -ne 0 ] ; then warn "Could not set maximum file descriptor limit: $MAX_FD" fi else warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" fi fi # For Darwin, add options to specify how the application appears in the dock if $darwin; then GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" fi # For Cygwin or MSYS, switch paths to Windows format before running java if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then APP_HOME=`cygpath --path --mixed "$APP_HOME"` CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` JAVACMD=`cygpath --unix "$JAVACMD"` # We build the pattern for arguments to be converted via cygpath ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` SEP="" for dir in $ROOTDIRSRAW ; do ROOTDIRS="$ROOTDIRS$SEP$dir" SEP="|" done OURCYGPATTERN="(^($ROOTDIRS))" # Add a user-defined pattern to the cygpath arguments if [ "$GRADLE_CYGPATTERN" != "" ] ; then OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" fi # Now convert the arguments - kludge to limit ourselves to /bin/sh i=0 for arg in "$@" ; do CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` else eval `echo args$i`="\"$arg\"" fi i=`expr $i + 1` done case $i in 0) set -- ;; 1) set -- "$args0" ;; 2) set -- "$args0" "$args1" ;; 3) set -- "$args0" "$args1" "$args2" ;; 4) set -- "$args0" "$args1" "$args2" "$args3" ;; 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; esac fi # Escape application args save () { for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done echo " " } APP_ARGS=`save "$@"` # Collect all arguments for the java command, following the shell quoting and substitution rules eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" exec "$JAVACMD" "$@" ================================================ FILE: gradlew.bat ================================================ @rem @rem Copyright 2015 the original author or authors. @rem @rem Licensed under the Apache License, Version 2.0 (the "License"); @rem you may not use this file except in compliance with the License. @rem You may obtain a copy of the License at @rem @rem https://www.apache.org/licenses/LICENSE-2.0 @rem @rem Unless required by applicable law or agreed to in writing, software @rem distributed under the License is distributed on an "AS IS" BASIS, @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @rem See the License for the specific language governing permissions and @rem limitations under the License. @rem @if "%DEBUG%" == "" @echo off @rem ########################################################################## @rem @rem Gradle startup script for Windows @rem @rem ########################################################################## @rem Set local scope for the variables with windows NT shell if "%OS%"=="Windows_NT" setlocal set DIRNAME=%~dp0 if "%DIRNAME%" == "" set DIRNAME=. 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%" == "0" goto execute echo. echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. echo. echo Please set the JAVA_HOME variable in your environment to match the echo location of your Java installation. goto fail :findJavaFromJavaHome set JAVA_HOME=%JAVA_HOME:"=% set JAVA_EXE=%JAVA_HOME%/bin/java.exe if exist "%JAVA_EXE%" goto execute echo. echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% echo. echo Please set the JAVA_HOME variable in your environment to match the echo location of your Java installation. goto fail :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%"=="0" goto mainEnd :fail rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of rem the _cmd.exe /c_ return code! if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 exit /b 1 :mainEnd if "%OS%"=="Windows_NT" endlocal :omega ================================================ FILE: settings.gradle.kts ================================================ pluginManagement { repositories { google() mavenCentral() gradlePluginPortal() } } rootProject.name = "Bunny Manager" include(":app")