diff --git a/.github/workflows/build_pull_request.yml b/.github/workflows/build_pull_request.yml index e030f62a5..bcb961770 100644 --- a/.github/workflows/build_pull_request.yml +++ b/.github/workflows/build_pull_request.yml @@ -2,10 +2,14 @@ name: Build pull request on: workflow_dispatch: - push: + pull_request: branches: - dev +concurrency: + group: compile-${{ github.ref }} + cancel-in-progress: true + jobs: build: runs-on: ubuntu-latest diff --git a/.github/workflows/compile_pull_request.yml b/.github/workflows/compile_pull_request.yml deleted file mode 100644 index 70750c8d7..000000000 --- a/.github/workflows/compile_pull_request.yml +++ /dev/null @@ -1,35 +0,0 @@ -name: Compile pull request - -on: - pull_request: - branches: - - dev - -concurrency: - group: compile-${{ github.ref }} - cancel-in-progress: true - -jobs: - build: - runs-on: ubuntu-latest - - steps: - - name: Checkout - uses: actions/checkout@v6 - - - name: Setup Java - uses: actions/setup-java@v5 - with: - distribution: 'temurin' - java-version: '17' - - - name: Cache Gradle - uses: burrunan/gradle-cache-action@v3 - - - name: Build Debug APK - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - _JAVA_OPTIONS: "-Djava.awt.headless=true" - run: | - chmod +x ./gradlew - ./gradlew assembleDebug -PnoProguard -PsignAsDebug --stacktrace diff --git a/.github/workflows/crowdin_pull.yml b/.github/workflows/crowdin_pull.yml index 07a4391b7..19494a982 100644 --- a/.github/workflows/crowdin_pull.yml +++ b/.github/workflows/crowdin_pull.yml @@ -33,10 +33,6 @@ jobs: CROWDIN_PROJECT_ID: ${{ secrets.CROWDIN_PROJECT_ID }} CROWDIN_PERSONAL_TOKEN: ${{ secrets.CROWDIN_PERSONAL_TOKEN }} - - name: Push crowdin branch - run: | - git checkout -B crowdin - git push --force origin crowdin merge: name: Squash merge Crowdin into Dev if: github.event_name == 'workflow_dispatch' @@ -53,6 +49,9 @@ jobs: fetch-depth: 0 clean: true + - name: Cache Gradle + uses: burrunan/gradle-cache-action@v3 + # Step 2: Compile Android app to check string resources - name: Compile Android app to check resources env: diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 6916aae3a..68fb888db 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -2,6 +2,11 @@ name: Release on: workflow_dispatch: + inputs: + send_fcm: + description: 'Send FCM push notification' + type: boolean + default: true push: branches: - main @@ -25,7 +30,7 @@ jobs: uses: actions/setup-java@v5 with: distribution: 'temurin' - java-version: '17' + java-version: '21' - name: Cache Gradle uses: burrunan/gradle-cache-action@v3 @@ -60,31 +65,6 @@ jobs: KEYSTORE_ENTRY_ALIAS: ${{ secrets.KEYSTORE_ENTRY_ALIAS }} KEYSTORE_ENTRY_PASSWORD: ${{ secrets.KEYSTORE_ENTRY_PASSWORD }} - - name: Copy release files to legacy path - # TODO: Eventually delete this logic - if: steps.release.outputs.new_release_published == 'true' - run: | - git config user.name "semantic-release-bot" - git config user.email "semantic-release-bot@martynus.net" - - # Make sure we're rebasing onto the latest remote branch - git fetch origin - - # Hard reset to latest remote branch - git checkout ${GITHUB_REF_NAME} - git reset --hard origin/${GITHUB_REF_NAME} - - cp app-release.json app/app-release.json - cp CHANGELOG.md app/CHANGELOG.md - - git add app/app-release.json - git add app/CHANGELOG.md - - git commit -m "chore: Update legacy release files" - - # Push explicitly to the branch that triggered the workflow - git push origin HEAD:${GITHUB_REF_NAME} - - name: Attest if: steps.release.outputs.new_release_published == 'true' uses: actions/attest-build-provenance@v2 @@ -114,17 +94,17 @@ jobs: }) - name: Wait before sending FCM - if: steps.release.outputs.new_release_published == 'true' + if: steps.release.outputs.new_release_published == 'true' && inputs.send_fcm != false run: sleep 480 - name: Setup Python for FCM - if: steps.release.outputs.new_release_published == 'true' + if: steps.release.outputs.new_release_published == 'true' && inputs.send_fcm != false uses: actions/setup-python@v5 with: python-version: '3.12' - name: Send FCM push notification - if: steps.release.outputs.new_release_published == 'true' + if: steps.release.outputs.new_release_published == 'true' && inputs.send_fcm != false env: FCM_PROJECT_ID: ${{ secrets.FCM_PROJECT_ID }} FCM_SERVICE_ACCOUNT_JSON: ${{ secrets.FCM_SERVICE_ACCOUNT_JSON }} diff --git a/CHANGELOG.md b/CHANGELOG.md index bfe58de96..7db5c4c04 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,177 @@ +# [1.16.0-dev.16](https://github.com/MorpheApp/morphe-manager/compare/v1.16.0-dev.15...v1.16.0-dev.16) (2026-04-30) + + +### Bug Fixes + +* "SessionBasedInstallConfirmationActivity was finished by user" install error on some devices ([3e74857](https://github.com/MorpheApp/morphe-manager/commit/3e74857bdb6fab5815f04c52b137024188a60dc3)) +* Scope `InstalledAppInfoViewModel` to dialog instance via dialog token ([0cf2f6a](https://github.com/MorpheApp/morphe-manager/commit/0cf2f6a7c451fe1bd79538684babc3e1376b7f41)) + +# [1.16.0-dev.15](https://github.com/MorpheApp/morphe-manager/compare/v1.16.0-dev.14...v1.16.0-dev.15) (2026-04-29) + + +### Bug Fixes + +* Show reinstall button and installer dialog for deleted apps ([472d046](https://github.com/MorpheApp/morphe-manager/commit/472d0462959235a90c0033129f87dd22a7af621d)) +* Update home screen cards immediately after install/uninstall ([8f671bd](https://github.com/MorpheApp/morphe-manager/commit/8f671bdc350e1daa576640e13bbd778d78382f73)) + +# [1.16.0-dev.14](https://github.com/MorpheApp/morphe-manager/compare/v1.16.0-dev.13...v1.16.0-dev.14) (2026-04-28) + + +### Bug Fixes + +* File picker and export for Android TV ([#491](https://github.com/MorpheApp/morphe-manager/issues/491)) ([7c1cfba](https://github.com/MorpheApp/morphe-manager/commit/7c1cfba98ff4b2eb39aa0e8a6f14e028978f1f60)) + + +### Features + +* Add manual `JKS` parser for keystore import without BC provider dependency ([#494](https://github.com/MorpheApp/morphe-manager/issues/494)) ([ccc99a2](https://github.com/MorpheApp/morphe-manager/commit/ccc99a24b410dff2f04d8c74b8d0916b64d7d762)) + +# [1.16.0-dev.13](https://github.com/MorpheApp/morphe-manager/compare/v1.16.0-dev.12...v1.16.0-dev.13) (2026-04-27) + + +### Bug Fixes + +* Replace `isLoaded` flag with `BundleState` sealed class and simplify `homeAppState` ([72976d3](https://github.com/MorpheApp/morphe-manager/commit/72976d33aea02e3aad6639fe7ff1ec1f4d330a0c)) + + +### Features + +* Add random background mode with rotation interval ([2d12fbb](https://github.com/MorpheApp/morphe-manager/commit/2d12fbbb052c3fe1fae979db7ad1d13b83918da9)) +* Import keystore from `PKCS12`, `BKS` and `JKS` formats ([3f38387](https://github.com/MorpheApp/morphe-manager/commit/3f38387f515c351b5c4c248a9f682b46af03de85)) + +# [1.16.0-dev.12](https://github.com/MorpheApp/morphe-manager/compare/v1.16.0-dev.11...v1.16.0-dev.12) (2026-04-26) + + +### Bug Fixes + +* Show SDK-incompatible versions as disabled, block patching when no versions are compatible with device SDK ([f90d5ba](https://github.com/MorpheApp/morphe-manager/commit/f90d5bacc7198f2c0a3c7cf48c7745415d89e141)) + + +### Features + +* Open `.mpp` patch sources directly from file manager ([#483](https://github.com/MorpheApp/morphe-manager/issues/483)) ([f46a11f](https://github.com/MorpheApp/morphe-manager/commit/f46a11f91e88d948ecbab8e71200cec543ce48ec)) + +# [1.16.0-dev.11](https://github.com/MorpheApp/morphe-manager/compare/v1.16.0-dev.10...v1.16.0-dev.11) (2026-04-24) + + +### Bug Fixes + +* Patch bundles do not load on Android 8.0 devices ([3116619](https://github.com/MorpheApp/morphe-manager/commit/3116619cee63697dd71d1c22b95be11cec78384e)) +* Resolve display name from bundle metadata over patched APK label ([6c6e065](https://github.com/MorpheApp/morphe-manager/commit/6c6e0658d4d4e2a415db1342764de45bd5595c04)) + + +### Features + +* Live patching progress in foreground notification ([c25af8f](https://github.com/MorpheApp/morphe-manager/commit/c25af8f28e7f33d90f8db62b74f5eb1128680e95)) + +# [1.16.0-dev.10](https://github.com/MorpheApp/morphe-manager/compare/v1.16.0-dev.9...v1.16.0-dev.10) (2026-04-24) + + +### Bug Fixes + +* Resolve app icon from saved APK when app is not installed ([fe3ef6c](https://github.com/MorpheApp/morphe-manager/commit/fe3ef6cfaea6bb68c11bd89a127a1122d3cdb943)) + + +### Features + +* Add `BundleAppMetadata` as a data source for `AppDataResolver` ([3bdc1f5](https://github.com/MorpheApp/morphe-manager/commit/3bdc1f5299b7429444c7266559806aa1ad6aa8d1)) +* Open patches dialog on hidden app tap in search ([1898e74](https://github.com/MorpheApp/morphe-manager/commit/1898e74fe8240961ddda118c2f7fac7f48bacb76)) + +# [1.16.0-dev.9](https://github.com/MorpheApp/morphe-manager/compare/v1.16.0-dev.8...v1.16.0-dev.9) (2026-04-24) + + +### Bug Fixes + +* Check primary ABI only in `isArmV7` to avoid false positives on `ArmV8` devices ([14729c2](https://github.com/MorpheApp/morphe-manager/commit/14729c28a73c0294152c3df1aabffdfb79974215)) +* Fall back to `Downloads` export on devices without `DocumentsUI` (Android TV) ([1e21c39](https://github.com/MorpheApp/morphe-manager/commit/1e21c3957e37351d1498b9894d2e4c3ee8154608)) + +# [1.16.0-dev.8](https://github.com/MorpheApp/morphe-manager/compare/v1.16.0-dev.7...v1.16.0-dev.8) (2026-04-24) + + +### Bug Fixes + +* Adapt accent color contrast for extreme black/white values in app info dialog ([c4f883f](https://github.com/MorpheApp/morphe-manager/commit/c4f883ffbdbb1bd3e7b2085939cb050fa747a11b)) +* Always respect manager prerelease preference for update channel ([090ee0c](https://github.com/MorpheApp/morphe-manager/commit/090ee0ca1ed8d076b58dd3abe4ffa8f8f2062203)) +* Show swipe gesture hint on every custom bundle addition ([0c66503](https://github.com/MorpheApp/morphe-manager/commit/0c665038ed00234ed13b5df32b180382ecfa2f12)) + + +### Features + +* Adaptive two-column layout for `InstalledAppInfoDialog` on tablets ([40a29a9](https://github.com/MorpheApp/morphe-manager/commit/40a29a96967ef06eca27cb5f62db8d821f93c4aa)) +* Add swipe gestures to hidden apps dialog and search results ([8cf1f13](https://github.com/MorpheApp/morphe-manager/commit/8cf1f137e8cf346a5628c80098e04aecf86f2441)) + +# [1.16.0-dev.7](https://github.com/MorpheApp/morphe-manager/compare/v1.16.0-dev.6...v1.16.0-dev.7) (2026-04-24) + + +### Bug Fixes + +* `Session is dead` error on Pixels devices when installing apps ([#458](https://github.com/MorpheApp/morphe-manager/issues/458)) ([ce1ce6e](https://github.com/MorpheApp/morphe-manager/commit/ce1ce6e195a4138c7ca18f5a9dd018db8591fddc)) + + +### Features + +* Improve patch visibility in bundle and app patch dialogs ([#457](https://github.com/MorpheApp/morphe-manager/issues/457)) ([1881991](https://github.com/MorpheApp/morphe-manager/commit/188199176dba9fe41115e2376fe7ade3138c8068)) + +# [1.16.0-dev.6](https://github.com/MorpheApp/morphe-manager/compare/v1.16.0-dev.5...v1.16.0-dev.6) (2026-04-20) + + +### Bug Fixes + +* System installer couldn't update an already installed app ([#455](https://github.com/MorpheApp/morphe-manager/issues/455)) ([adc93e4](https://github.com/MorpheApp/morphe-manager/commit/adc93e4bab2d9b153b2aecd9f0671b393e42d309)) +* When greeting message is disabled, show a small top spacer so the app cards don't sit flush against the top of the screen ([6900cc2](https://github.com/MorpheApp/morphe-manager/commit/6900cc226e189b97c747d6d53f5fba98ade6ccf0)) + + +### Features + +* Sort universal patches to bottom of each bundle in patches dialog ([eac672e](https://github.com/MorpheApp/morphe-manager/commit/eac672e51f1fe3007bba46af6321d833ac20fb4b)) + +# [1.16.0-dev.5](https://github.com/MorpheApp/morphe-manager/compare/v1.16.0-dev.4...v1.16.0-dev.5) (2026-04-19) + + +### Bug Fixes + +* Shizuku installer couldn't update an already installed app ([#454](https://github.com/MorpheApp/morphe-manager/issues/454)) ([d4e74e3](https://github.com/MorpheApp/morphe-manager/commit/d4e74e3a84ca34529f8d20005c19b2b0e7c9136f)) + +# [1.16.0-dev.4](https://github.com/MorpheApp/morphe-manager/compare/v1.16.0-dev.3...v1.16.0-dev.4) (2026-04-19) + + +### Bug Fixes + +* Add logging and fix stale installer cache ([78b5aee](https://github.com/MorpheApp/morphe-manager/commit/78b5aee8dca5f1f51fb6bb46c9d9cab8bd1f7c0a)) +* Merge 'Filter split APKs' and `Remove unused native libraries` into 'Optimize for device architecture' setting ([2edb15f](https://github.com/MorpheApp/morphe-manager/commit/2edb15fcfb0fd018ffcafc0f7203930a203ab4d4)) + + +### Features + +* Add swipe gestures and multi-select to app buttons on main screen ([#446](https://github.com/MorpheApp/morphe-manager/issues/446)) ([0330699](https://github.com/MorpheApp/morphe-manager/commit/033069989d24c437798bb5a7bf248afe9f30ae89)) +* Add toggle to disable home screen patching phrases ([#443](https://github.com/MorpheApp/morphe-manager/issues/443)) ([f53ad64](https://github.com/MorpheApp/morphe-manager/commit/f53ad646c90700af17b3d58b0a2651cdfb87aab9)) + +# [1.16.0-dev.3](https://github.com/MorpheApp/morphe-manager/compare/v1.16.0-dev.2...v1.16.0-dev.3) (2026-04-18) + + +### Bug Fixes + +* Handle `InstallFailure` result when installing manager update ([a4d1eb8](https://github.com/MorpheApp/morphe-manager/commit/a4d1eb8a99dc53d59f23df3d7bb8ad3725edd1c4)) + + +### Features + +* Migrate to `Ackpine` for package installation/uninstallation ([#444](https://github.com/MorpheApp/morphe-manager/issues/444)) ([aa7207d](https://github.com/MorpheApp/morphe-manager/commit/aa7207d486427753eb56c18ddd29d481f1a3605e)) + +# [1.16.0-dev.2](https://github.com/MorpheApp/morphe-manager/compare/v1.16.0-dev.1...v1.16.0-dev.2) (2026-04-18) + + +### Features + +* Add fast bytecode mode setting to expert mode ([#403](https://github.com/MorpheApp/morphe-manager/issues/403)) ([e73c63c](https://github.com/MorpheApp/morphe-manager/commit/e73c63c7fcce14d5d38ce8b8cde26feed4a6e5e4)) + +# [1.16.0-dev.1](https://github.com/MorpheApp/morphe-manager/compare/v1.15.0...v1.16.0-dev.1) (2026-04-17) + + +### Features + +* Store merged APK from split archives as original for repatching ([#438](https://github.com/MorpheApp/morphe-manager/issues/438)) ([be0b868](https://github.com/MorpheApp/morphe-manager/commit/be0b86866f5e9f5e8aad4b596158084d03189fa3)) + # [1.15.0](https://github.com/MorpheApp/morphe-manager/compare/v1.14.0...v1.15.0) (2026-04-17) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 000000000..22a32f228 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,16 @@ +# 👋 Contribution guidelines + +This document describes how to contribute to the Morphe Manager. + +## 📝 How to contribute + +1. Before contributing, it is recommended to [open an issue](https://github.com/MorpheApp/morphe-manager/issues/new?labels=Feature+request&projects=&template=feature_request.yml&title=feat%3A+) + to discuss your change. This will help you determine whether your change is worth your time to implement it. +2. Development happens on the `dev` branch. Fork the repository and create your branch from `dev`. +3. Commit your changes. +4. Submit a pull request to the `dev` branch of the repository and reference issues + that your pull request closes in the description of your pull request. +5. Our team will review your pull request and provide feedback. Once your pull request is approved, + it will be merged into the `dev` branch and will be included in the next pre-release of Morphe. + +❤️ Thank you for considering contributing to the Morphe Manager. diff --git a/README.md b/README.md index deab66133..b85cef2a8 100644 --- a/README.md +++ b/README.md @@ -46,7 +46,7 @@ The website will guide you to the latest release for your device. No account nee 3. **Choose your mode:** - **Simple mode** - designed for a one-tap experience. Just tap Patch and Morphe handles the rest with sensible defaults. No configuration needed. - **Expert mode** - gives you full control. Choose exactly which of the 100+ patches to apply, configure per-patch options (colors, toggles, and more), and fine-tune everything before patching. -4. **Provide the APK** - Morphe guides you through obtaining the original app file via step-by-step dialogs. The patching itself happens entirely on your device. +4. **Provide the APK** - Morphe guides you through obtaining the original app file via step-by-step dialogs. The patching itself happens entirely on your device. *(If you can't select the APK file, try moving it out of the Downloads folder to the root of your internal storage.)* 5. **Install and enjoy** - once patching is complete, install the result like any normal APK. Everything happens locally. Morphe never uploads your APKs or personal data anywhere. @@ -59,6 +59,8 @@ Everything happens locally. Morphe never uploads your APKs or personal data anyw - Expert mode also shows an expanded patching screen with real-time logs and live RAM usage monitoring during patching - 100+ patches for YouTube, YouTube Music, and Reddit - Support for split APKs +- Skips split APK modules for unsupported CPU architectures, locales, and screen densities during merge +- Strips native libraries for unsupported architectures from plain APKs after patching **Patch options** *(Simple mode: available in the Advanced tab; Expert mode: available on the patch selection screen)* - Custom app display name and header logo per app @@ -91,6 +93,7 @@ Everything happens locally. Morphe never uploads your APKs or personal data anyw - Manage saved patch selections per app - GitHub Personal Access Token support for higher API rate limits - Process runtime - run patching in a separate process for better stability, with configurable memory limit +- Bytecode processing mode - controls how bytecode is processed during patching, affecting patching speed, memory usage, and output APK size - Export debug logs for troubleshooting ## ❓ New to GitHub? @@ -106,6 +109,11 @@ That's it. Once Morphe is installed, everything else happens inside the app. For guides, FAQs, and troubleshooting, visit **[morphe.software](https://morphe.software)** or join the community on **[Reddit](https://www.reddit.com/r/MorpheApp)**. +## 📙 Contributing + +Thank you for considering contributing to Morphe Manager. +You can find the contribution guidelines [here](CONTRIBUTING.md). + ## ❗ About Morphe is built on the foundation of [ReVanced Manager](https://github.com/ReVanced/revanced-manager) and [URV](https://github.com/Jman-Github/Universal-ReVanced-Manager). All changes made by Morphe are documented in the Git history. diff --git a/app-release.json b/app-release.json index 752226b66..d1fbcc747 100644 --- a/app-release.json +++ b/app-release.json @@ -1,7 +1,7 @@ { - "created_at": "2026-04-17T06:32:20.437Z", - "description": "# [1.15.0](https://github.com/MorpheApp/morphe-manager/compare/v1.14.0...v1.15.0) (2026-04-17)\n\n\n### Bug Fixes\n\n* Adjust wording ([482c1d1](https://github.com/MorpheApp/morphe-manager/commit/482c1d18945cfdc63bb54d7d112b0ec7ce4f58ba))\n* Cancel patcher worker immediately on user cancellation ([4f0b312](https://github.com/MorpheApp/morphe-manager/commit/4f0b3124052a0975a94a38f0a519ab8e340ec318))\n* Don't count empty patch selections in package badge ([e073ecf](https://github.com/MorpheApp/morphe-manager/commit/e073ecf279d5d605198a039b6325226f1d3feec2))\n* Improve APK load error messages with distinct failure reasons ([3174f28](https://github.com/MorpheApp/morphe-manager/commit/3174f28480e1857ae689dee26806ed513ad980f9))\n* Interrupt split APK merger immediately on cancellation ([0f7feca](https://github.com/MorpheApp/morphe-manager/commit/0f7fecabfd24ea588755c2ddfc2a4661c0783b83))\n* Re-download bundle if version matches but createdAt differs ([2e77833](https://github.com/MorpheApp/morphe-manager/commit/2e77833cca08fb1c5c52bebe11dd032269099f9c))\n* Refresh patch options only once on bundle load ([bf04846](https://github.com/MorpheApp/morphe-manager/commit/bf0484648ea76dfedbd778716fd75c65f1538f4f))\n* Serialize `StringList` options based on patcher type ([8464f34](https://github.com/MorpheApp/morphe-manager/commit/8464f34f280b02beab4965dd62f3d9cfc3653979))\n* Show failing bundle name in error toast and auto-disable bundles on fetch failure ([1c3a384](https://github.com/MorpheApp/morphe-manager/commit/1c3a3843f3989621764dfb8582e926170e686fa9))\n* Show full patching log in error dialog when no specific error is captured ([f18d826](https://github.com/MorpheApp/morphe-manager/commit/f18d8267cc0b356139ce4a9299548d45097624d5))\n* Show success toast after bundle import completes ([74d05cb](https://github.com/MorpheApp/morphe-manager/commit/74d05cb46d74ea489ad2c454706a9c0a4cf4a1c5))\n* Skip disabled installed apps in AppDataResolver ([8eaa88b](https://github.com/MorpheApp/morphe-manager/commit/8eaa88bda6e4fe657924355eaed1b3fe87f045b1))\n* Use `GetContent` instead of `OpenDocument` for APK/bundle pickers ([cb3551d](https://github.com/MorpheApp/morphe-manager/commit/cb3551d13ac490b2e74eb7ec111369e278e32efe))\n\n\n### Features\n\n* Add Android TV launcher support ([38f2703](https://github.com/MorpheApp/morphe-manager/commit/38f27030d4c80b1873af37c21454206fc86ec372))\n* Add Expert badge to patch bundle viewer ([169ff75](https://github.com/MorpheApp/morphe-manager/commit/169ff751ba839b50aeebb03c07801688a8dd2cbe))\n* Add import/export selection buttons in patch selection dialog ([c5b4ef6](https://github.com/MorpheApp/morphe-manager/commit/c5b4ef658e05a34198233ffe897e05499454ca18))\n* Add saved selection button in expert mode dialog ([ee336d8](https://github.com/MorpheApp/morphe-manager/commit/ee336d865e71e4a597924a302326ef2d5c638805))\n* Export/import third-party bundles with manager settings ([e5c826f](https://github.com/MorpheApp/morphe-manager/commit/e5c826fb81c725cb6ea3b614f2a04a924350f05a))\n* Group compatible versions by bundle in APK availability dialog ([#432](https://github.com/MorpheApp/morphe-manager/issues/432)) ([362d097](https://github.com/MorpheApp/morphe-manager/commit/362d09744c51844774c1e9555580e0ed7fcdbfa1))\n* Show bottom bar labels in main screen ([2d4fd8d](https://github.com/MorpheApp/morphe-manager/commit/2d4fd8d3c2180c9443e65c8d0a9c23bcb2586e13))\n* Show update date for single default bundle in management sheet ([16e81bb](https://github.com/MorpheApp/morphe-manager/commit/16e81bbde5eff647742eee5414033a4bcce4c98d))", - "download_url": "https://github.com/MorpheApp/morphe-manager/releases/download/v1.15.0/morphe-manager-1.15.0.apk", - "signature_download_url": "https://github.com/MorpheApp/morphe-manager/releases/download/v1.15.0/morphe-manager-1.15.0.apk.asc", - "version": "1.15.0" + "created_at": "2026-04-30T21:41:49.760Z", + "description": "# [1.16.0-dev.16](https://github.com/MorpheApp/morphe-manager/compare/v1.16.0-dev.15...v1.16.0-dev.16) (2026-04-30)\n\n\n### Bug Fixes\n\n* \"SessionBasedInstallConfirmationActivity was finished by user\" install error on some devices ([3e74857](https://github.com/MorpheApp/morphe-manager/commit/3e74857bdb6fab5815f04c52b137024188a60dc3))\n* Scope `InstalledAppInfoViewModel` to dialog instance via dialog token ([0cf2f6a](https://github.com/MorpheApp/morphe-manager/commit/0cf2f6a7c451fe1bd79538684babc3e1376b7f41))", + "download_url": "https://github.com/MorpheApp/morphe-manager/releases/download/v1.16.0-dev.16/morphe-manager-1.16.0-dev.16.apk", + "signature_download_url": "https://github.com/MorpheApp/morphe-manager/releases/download/v1.16.0-dev.16/morphe-manager-1.16.0-dev.16.apk.asc", + "version": "1.16.0-dev.16" } diff --git a/app/CHANGELOG.md b/app/CHANGELOG.md index bfe58de96..f444f8809 100644 --- a/app/CHANGELOG.md +++ b/app/CHANGELOG.md @@ -1287,4 +1287,4 @@ ### Features -* Custom Morphe home screen ([515d08c](https://github.com/MorpheApp/morphe-manager/commit/515d08ce741752d06cbabb7be57bac9fe692d8a6)) +* Custom Morphe home screen ([515d08c](https://github.com/MorpheApp/morphe-manager/commit/515d08ce741752d06cbabb7be57bac9fe692d8a6)) \ No newline at end of file diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 8ae02604a..a987b2d20 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -62,11 +62,6 @@ dependencies { implementation(libs.morphe.patcher) implementation(libs.morphe.library) - // Exclude xmlpull as it's included in Android already - configurations.configureEach { - exclude(group = "xmlpull", module = "xmlpull") - } - implementation(libs.androidx.documentfile) // Native processes @@ -80,6 +75,12 @@ dependencies { implementation(libs.shizuku.api) implementation(libs.shizuku.provider) + // Ackpine + implementation(libs.ackpine.core) + implementation(libs.ackpine.ktx) + implementation(libs.ackpine.shizuku) + implementation(libs.ackpine.shizuku.ktx) + // LibSU implementation(libs.libsu.core) implementation(libs.libsu.service) @@ -155,6 +156,7 @@ android { buildTypes { debug { + applicationIdSuffix = ".debug" buildConfigField("long", "BUILD_ID", "${Random.nextLong()}L") } @@ -218,7 +220,6 @@ android { ) jniLibs { - excludes += "/lib/x86/*.so" useLegacyPackaging = true } } @@ -258,6 +259,7 @@ kotlin { freeCompilerArgs.addAll( "-Xexplicit-backing-fields", "-Xcontext-parameters", + "-opt-in=kotlin.time.ExperimentalTime" ) } } diff --git a/app/google-services.json b/app/google-services.json index b39d30939..d78abd348 100644 --- a/app/google-services.json +++ b/app/google-services.json @@ -23,6 +23,25 @@ "other_platform_oauth_client": [] } } + }, + { + "client_info": { + "mobilesdk_app_id": "1:574971415597:android:5acf99d4cc8260eedc7ca4", + "android_client_info": { + "package_name": "app.morphe.manager.debug" + } + }, + "oauth_client": [], + "api_key": [ + { + "current_key": "AIzaSyAICrE-tnEhNKXh1vEbfucYWWxOr7NSr_g" + } + ], + "services": { + "appinvite_service": { + "other_platform_oauth_client": [] + } + } } ], "configuration_version": "1" diff --git a/app/gradle.properties b/app/gradle.properties index 7307fe49d..ac0712d98 100644 --- a/app/gradle.properties +++ b/app/gradle.properties @@ -1 +1 @@ -version = 1.15.0 +version = 1.16.0-dev.16 diff --git a/app/src/debug/res/values/strings.xml b/app/src/debug/res/values/strings.xml new file mode 100644 index 000000000..fdc15b049 --- /dev/null +++ b/app/src/debug/res/values/strings.xml @@ -0,0 +1,4 @@ + + + Morphe Debug + diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 21430b0da..e87340c4f 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -84,6 +84,15 @@ + + + + + + + + + @@ -188,16 +197,6 @@ - - - - - - + + diff --git a/app/src/main/java/app/morphe/manager/MainActivity.kt b/app/src/main/java/app/morphe/manager/MainActivity.kt index e39d714d1..670bde03f 100644 --- a/app/src/main/java/app/morphe/manager/MainActivity.kt +++ b/app/src/main/java/app/morphe/manager/MainActivity.kt @@ -8,9 +8,7 @@ import androidx.activity.compose.LocalActivity import androidx.activity.compose.setContent import androidx.activity.enableEdgeToEdge import androidx.appcompat.app.AppCompatActivity -import androidx.compose.animation.* -import androidx.compose.animation.core.FastOutSlowInEasing -import androidx.compose.animation.core.tween +import androidx.compose.animation.ExperimentalAnimationApi import androidx.compose.foundation.background import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.foundation.layout.Box @@ -39,12 +37,16 @@ import app.morphe.manager.ui.screen.HomeScreen import app.morphe.manager.ui.screen.PatcherScreen import app.morphe.manager.ui.screen.SettingsScreen import app.morphe.manager.ui.screen.shared.AnimatedBackground +import app.morphe.manager.ui.screen.shared.BackgroundType +import app.morphe.manager.ui.screen.shared.MorpheAnimations import app.morphe.manager.ui.theme.ManagerTheme import app.morphe.manager.ui.theme.Theme import app.morphe.manager.ui.viewmodel.HomeViewModel import app.morphe.manager.ui.viewmodel.MainViewModel import app.morphe.manager.ui.viewmodel.PatcherViewModel +import app.morphe.manager.ui.viewmodel.ThemeSettingsViewModel import app.morphe.manager.util.UpdateNotificationManager +import app.morphe.manager.util.hasMppExtension import kotlinx.coroutines.launch import org.koin.androidx.compose.koinViewModel import org.koin.compose.koinInject @@ -100,6 +102,16 @@ class MainActivity : AppCompatActivity() { */ private fun handleDeepLinkIntent(intent: Intent?, vm: MainViewModel) { val data = intent?.data ?: return + + // Handle .mpp file open from file manager + if (intent.action == Intent.ACTION_VIEW && data.scheme in listOf("file", "content")) { + if (data.hasMppExtension(contentResolver)) { + vm.pendingMppUri = data + } + // Not .mpp - don't process further regardless + return + } + val isAddSource = data.scheme == "https" && data.host == "morphe.software" && data.path?.startsWith("/add-source") == true @@ -116,11 +128,21 @@ class MainActivity : AppCompatActivity() { private fun MorpheManager(vm: MainViewModel) { val navController = rememberNavController() val prefs: PreferencesManager = koinInject() + val themeViewModel: ThemeSettingsViewModel = koinViewModel() val backgroundType by prefs.backgroundType.getAsState() val enableParallax by prefs.enableBackgroundParallax.getAsState() + val randomInterval by prefs.randomBackgroundInterval.getAsState() + val resolvedRandomBackground by themeViewModel.resolvedRandomBackground.collectAsStateWithLifecycle() + + // Resolve which background to show whenever RANDOM mode is active or the interval changes + LaunchedEffect(backgroundType, randomInterval) { + if (backgroundType == BackgroundType.RANDOM) { + themeViewModel.resolveRandomBackground(randomInterval) + } + } - // Patcher background speed — driven by PatcherViewModel when on patcher screen. - // Exposed as a top-level mutable state so PatcherScreen can write into it. + // Patcher background speed - driven by PatcherViewModel when on patcher screen. + // Exposed as top-level mutable state so PatcherScreen can write into it val patcherBackgroundSpeed = androidx.compose.runtime.remember { androidx.compose.runtime.mutableFloatStateOf(1f) } val patchingCompleted = androidx.compose.runtime.remember { mutableStateOf(false) } @@ -138,6 +160,7 @@ private fun MorpheManager(vm: MainViewModel) { // Show animated background AnimatedBackground( type = backgroundType, + resolvedType = resolvedRandomBackground, enableParallax = enableParallax, speedMultiplier = patcherBackgroundSpeed.floatValue, patchingCompleted = patchingCompleted.value @@ -147,38 +170,10 @@ private fun MorpheManager(vm: MainViewModel) { NavHost( navController = navController, startDestination = HomeScreen, - enterTransition = { - slideInHorizontally( - initialOffsetX = { it }, - animationSpec = tween(400, easing = FastOutSlowInEasing) - ) + fadeIn( - animationSpec = tween(400, delayMillis = 100) - ) - }, - exitTransition = { - slideOutHorizontally( - targetOffsetX = { -it / 3 }, - animationSpec = tween(400, easing = FastOutSlowInEasing) - ) + fadeOut( - animationSpec = tween(300) - ) - }, - popEnterTransition = { - slideInHorizontally( - initialOffsetX = { -it / 3 }, - animationSpec = tween(400, easing = FastOutSlowInEasing) - ) + fadeIn( - animationSpec = tween(400, delayMillis = 100) - ) - }, - popExitTransition = { - slideOutHorizontally( - targetOffsetX = { it }, - animationSpec = tween(400, easing = FastOutSlowInEasing) - ) + fadeOut( - animationSpec = tween(300) - ) - }, + enterTransition = { MorpheAnimations.screenEnter }, + exitTransition = { MorpheAnimations.screenExit }, + popEnterTransition = { MorpheAnimations.screenEnter }, + popExitTransition = { MorpheAnimations.screenExit } ) { // Shared state between HomeScreen and PatcherScreen for mount install mode. // Set by HomeViewModel.resolvePrePatchInstallerChoice() @@ -207,6 +202,14 @@ private fun MorpheManager(vm: MainViewModel) { } } + // Handle .mpp file opened from file manager + LaunchedEffect(vm.pendingMppUri) { + vm.pendingMppUri?.let { uri -> + homeViewModel.setPendingMpp(uri) + vm.pendingMppUri = null + } + } + HomeScreen( onSettingsClick = { navController.navigate(Settings) }, onStartQuickPatch = { params -> diff --git a/app/src/main/java/app/morphe/manager/ManagerApplication.kt b/app/src/main/java/app/morphe/manager/ManagerApplication.kt index 6723e9bb3..a53875cf4 100644 --- a/app/src/main/java/app/morphe/manager/ManagerApplication.kt +++ b/app/src/main/java/app/morphe/manager/ManagerApplication.kt @@ -33,6 +33,7 @@ import org.koin.android.ext.koin.androidLogger import org.koin.androidx.workmanager.koin.workManagerFactory import org.koin.core.context.startKoin import org.lsposed.hiddenapibypass.HiddenApiBypass +import ru.solrudev.ackpine.Ackpine class ManagerApplication : Application() { private val scope = MainScope() @@ -61,6 +62,9 @@ class ManagerApplication : Application() { ) } + // Enable Ackpine logging + Ackpine.enableLogcatLogger() + // App icon loader (Coil) val pixels = 512 Coil.setImageLoader( @@ -170,5 +174,9 @@ class ManagerApplication : Application() { deleteRecursively() mkdirs() } + // Logs all app-private directories and their contents with file sizes on fresh start + scope.launch(Dispatchers.IO) { + fs.logStorageContents() + } } } diff --git a/app/src/main/java/app/morphe/manager/data/platform/Filesystem.kt b/app/src/main/java/app/morphe/manager/data/platform/Filesystem.kt index 46ea0e8a2..af20043f7 100644 --- a/app/src/main/java/app/morphe/manager/data/platform/Filesystem.kt +++ b/app/src/main/java/app/morphe/manager/data/platform/Filesystem.kt @@ -6,12 +6,16 @@ import android.content.Context import android.content.pm.PackageManager import android.os.Build import android.os.Environment +import android.util.Log import androidx.activity.result.contract.ActivityResultContract import androidx.activity.result.contract.ActivityResultContracts import app.morphe.manager.util.FilenameUtils import app.morphe.manager.util.RequestManageStorageContract +import app.morphe.manager.util.formatBytes import java.io.File +private const val TAG = "Morphe Filesystem" + class Filesystem(private val app: Application) { /** * A directory that gets cleared when the app restarts. @@ -57,4 +61,31 @@ class Filesystem(private val app: Application) { val safeVersion = FilenameUtils.sanitize(version.ifBlank { "unspecified" }) return patchedAppsDir.resolve("${safePackage}_${safeVersion}.apk") } + + /** + * Logs all app-private directories and their contents with file sizes. + * Useful for diagnosing storage issues on startup. + */ + fun logStorageContents() { + Log.i(TAG, "=== Storage contents ===") + for (dir in listOf(tempDir, uiTempDir, patchedAppsDir, originalApksDir)) { + logDir(dir.name, dir) + } + Log.i(TAG, "=== End of storage contents ===") + } + + private fun logDir(label: String, dir: File, indent: String = "") { + val totalSize = dir.walkBottomUp().filter { it.isFile }.sumOf { it.length() } + Log.i(TAG, "$indent[$label] ${dir.absolutePath} (total: ${formatBytes(totalSize)})") + dir.listFiles() + ?.sortedWith(compareBy({ it.isFile }, { it.name })) + ?.forEach { entry -> + if (entry.isDirectory) { + logDir(entry.name, entry, "$indent ") + } else { + Log.i(TAG, "$indent ${entry.name} (${formatBytes(entry.length())})") + } + } + ?: Log.i(TAG, "$indent (empty or unreadable)") + } } diff --git a/app/src/main/java/app/morphe/manager/data/room/apps/original/OriginalApk.kt b/app/src/main/java/app/morphe/manager/data/room/apps/original/OriginalApk.kt index e42a0e3b6..f1f580b05 100644 --- a/app/src/main/java/app/morphe/manager/data/room/apps/original/OriginalApk.kt +++ b/app/src/main/java/app/morphe/manager/data/room/apps/original/OriginalApk.kt @@ -3,7 +3,6 @@ package app.morphe.manager.data.room.apps.original import androidx.room.ColumnInfo import androidx.room.Entity import androidx.room.PrimaryKey -import java.io.File @Entity(tableName = "original_apks") data class OriginalApk( @@ -13,6 +12,4 @@ data class OriginalApk( @ColumnInfo(name = "file_path") val filePath: String, @ColumnInfo(name = "last_used") val lastUsed: Long = System.currentTimeMillis(), @ColumnInfo(name = "file_size") val fileSize: Long -) { - fun getFile(): File = File(filePath) -} +) diff --git a/app/src/main/java/app/morphe/manager/di/HttpModule.kt b/app/src/main/java/app/morphe/manager/di/HttpModule.kt index e5a2f2b4f..e2e323dd6 100644 --- a/app/src/main/java/app/morphe/manager/di/HttpModule.kt +++ b/app/src/main/java/app/morphe/manager/di/HttpModule.kt @@ -16,20 +16,17 @@ import org.koin.android.ext.koin.androidContext import org.koin.core.module.dsl.singleOf import org.koin.dsl.module import java.net.Inet4Address -import java.net.InetAddress val httpModule = module { fun provideHttpClient(context: Context, json: Json) = HttpClient(OkHttp) { engine { config { - dns(object : Dns { - override fun lookup(hostname: String): List { - val addresses = Dns.SYSTEM.lookup(hostname) - val ipv4Addresses = addresses.filterIsInstance() - // Force IPv4 if available, fallback to IPv6 only if no IPv4 addresses are found - return ipv4Addresses.ifEmpty { addresses } - } - }) + dns { hostname -> + val addresses = Dns.SYSTEM.lookup(hostname) + val ipv4Addresses = addresses.filterIsInstance() + // Force IPv4 if available, fallback to IPv6 only if no IPv4 addresses are found + ipv4Addresses.ifEmpty { addresses } + } // Force HTTP/1.1 to avoid intermittent HTTP/2 PROTOCOL_ERROR stream resets when // downloading patch bundles from GitHub-backed endpoints. protocols(listOf(Protocol.HTTP_1_1)) diff --git a/app/src/main/java/app/morphe/manager/di/ManagerModule.kt b/app/src/main/java/app/morphe/manager/di/ManagerModule.kt index b1812a6c0..882639ef6 100644 --- a/app/src/main/java/app/morphe/manager/di/ManagerModule.kt +++ b/app/src/main/java/app/morphe/manager/di/ManagerModule.kt @@ -1,8 +1,8 @@ package app.morphe.manager.di +import app.morphe.manager.domain.installer.AckpineInstaller import app.morphe.manager.domain.installer.InstallerManager import app.morphe.manager.domain.installer.RootInstaller -import app.morphe.manager.domain.installer.ShizukuInstaller import app.morphe.manager.domain.manager.AppIconManager import app.morphe.manager.domain.manager.HomeAppButtonPreferences import app.morphe.manager.domain.manager.KeystoreManager @@ -16,7 +16,7 @@ val managerModule = module { singleOf(::KeystoreManager) singleOf(::PM) singleOf(::RootInstaller) - singleOf(::ShizukuInstaller) + singleOf(::AckpineInstaller) singleOf(::InstallerManager) singleOf(::PatchOptionsPreferencesManager) singleOf(::AppIconManager) diff --git a/app/src/main/java/app/morphe/manager/domain/bundles/PatchBundleSource.kt b/app/src/main/java/app/morphe/manager/domain/bundles/PatchBundleSource.kt index 429b0169d..3007104cb 100644 --- a/app/src/main/java/app/morphe/manager/domain/bundles/PatchBundleSource.kt +++ b/app/src/main/java/app/morphe/manager/domain/bundles/PatchBundleSource.kt @@ -115,13 +115,12 @@ sealed class PatchBundleSource( val host = uri.host?.lowercase(java.util.Locale.US) ?: return null val segments = uri.path?.trim('/')?.split('/')?.filter { it.isNotBlank() } ?: return null - when { + when (host) { // raw.githubusercontent.com/owner/repo/... - host == "raw.githubusercontent.com" && segments.isNotEmpty() -> segments[0] + "raw.githubusercontent.com" if segments.isNotEmpty() -> segments[0] // github.com/owner/repo/... - host == "github.com" && segments.isNotEmpty() -> segments[0] - + "github.com" if segments.isNotEmpty() -> segments[0] else -> null } } catch (_: Exception) { diff --git a/app/src/main/java/app/morphe/manager/domain/installer/AckpineInstaller.kt b/app/src/main/java/app/morphe/manager/domain/installer/AckpineInstaller.kt new file mode 100644 index 000000000..55b337791 --- /dev/null +++ b/app/src/main/java/app/morphe/manager/domain/installer/AckpineInstaller.kt @@ -0,0 +1,192 @@ +package app.morphe.manager.domain.installer + +import android.app.Application +import android.content.Intent +import android.content.pm.PackageManager +import android.net.Uri +import android.util.Log +import app.morphe.manager.R +import rikka.shizuku.Shizuku +import rikka.shizuku.ShizukuProvider +import rikka.sui.Sui +import ru.solrudev.ackpine.installer.InstallFailure +import ru.solrudev.ackpine.installer.PackageInstaller +import ru.solrudev.ackpine.installer.parameters.InstallParameters +import ru.solrudev.ackpine.installer.parameters.InstallerType +import ru.solrudev.ackpine.installer.parameters.PackageSource +import ru.solrudev.ackpine.session.Session +import ru.solrudev.ackpine.session.await +import ru.solrudev.ackpine.session.parameters.Confirmation +import ru.solrudev.ackpine.shizuku.ShizukuPlugin +import ru.solrudev.ackpine.uninstaller.PackageUninstaller +import ru.solrudev.ackpine.uninstaller.parameters.UninstallParameters +import java.io.File +import kotlin.coroutines.cancellation.CancellationException + +private const val TAG = "Morphe AckpineInstaller" + +/** + * Wraps Ackpine for internal (PackageInstaller API) and Shizuku installs. + * Root/mount installs are still handled by [RootInstaller]. + */ +class AckpineInstaller(private val app: Application) { + + private val packageInstaller: PackageInstaller = PackageInstaller.getInstance(app) + private val packageUninstaller: PackageUninstaller = PackageUninstaller.getInstance(app) + + init { + val isSui = Sui.init(app.packageName) + if (!isSui) { + runCatching { ShizukuProvider.requestBinderForNonProviderProcess(app) } + } + } + + /** + * Installs an APK using the standard Android PackageInstaller API via Ackpine. + * Suspends until the user confirms or cancels the system dialog. + * + * @return null on success, or a typed [InstallFailure] the caller can pattern-match on. + * @throws InstallCancelledException when the user dismisses the system install dialog. + */ + suspend fun installInternal(apkFile: File): InstallFailure? { + require(apkFile.exists()) { "APK file does not exist: ${apkFile.path}" } + Log.d(TAG, "installInternal: ${apkFile.name} (${apkFile.length()} bytes)") + val session = packageInstaller.createSession( + InstallParameters.Builder(Uri.fromFile(apkFile)) + .setInstallerType(InstallerType.SESSION_BASED) + .setConfirmation(Confirmation.IMMEDIATE) + .setName(apkFile.name) + // PackageSource.LocalFile disables "restricted settings" enforcement on API 33+ + .setPackageSource(PackageSource.LocalFile) + .build() + ) + return try { + extractFailure(session.await()).also { failure -> + if (failure != null) { + Log.w(TAG, "installInternal failed: ${failure.javaClass.simpleName} - ${failure.message}") + } else { + Log.i(TAG, "installInternal succeeded: ${apkFile.name}") + } + } + } catch (_: CancellationException) { + throw InstallCancelledException() + } catch (e: Exception) { + Log.w(TAG, "installInternal exception: ${e.message}", e) + throw e + } + } + + /** + * Installs an APK silently via Shizuku/Sui using Ackpine's ShizukuPlugin. + * + * @return null on success, or a typed [InstallFailure] the caller can pattern-match on. + * @throws InstallCancelledException when aborted. + */ + suspend fun installShizuku(apkFile: File): InstallFailure? { + require(apkFile.exists()) { "APK file does not exist: ${apkFile.path}" } + Log.d(TAG, "installShizuku: ${apkFile.name} (${apkFile.length()} bytes)") + val session = packageInstaller.createSession( + InstallParameters.Builder(Uri.fromFile(apkFile)) + .setInstallerType(InstallerType.SESSION_BASED) + .setConfirmation(Confirmation.IMMEDIATE) + .setName(apkFile.name) + .registerPlugin( + ShizukuPlugin::class.java, + ShizukuPlugin.InstallParameters.Builder() + .setReplaceExisting(true) + .build() + ) + .build() + ) + return try { + extractFailure(session.await()).also { failure -> + if (failure != null) { + Log.w(TAG, "installShizuku failed: ${failure.javaClass.simpleName} - ${failure.message}") + } else { + Log.i(TAG, "installShizuku succeeded: ${apkFile.name}") + } + } + } catch (e: CancellationException) { + throw InstallCancelledException().initCause(e) + } catch (e: Exception) { + Log.w(TAG, "installShizuku exception: ${e.message}", e) + throw e + } + } + + /** + * Uninstalls a package via Ackpine. Shows the system confirmation dialog. + * Suspends until the user confirms or cancels. + * + * @throws UninstallCancelledException when the user dismisses the dialog. + * @throws UninstallFailedException on any other failure. + */ + suspend fun uninstall(packageName: String) { + val session = packageUninstaller.createSession( + UninstallParameters.Builder(packageName) + .setConfirmation(Confirmation.IMMEDIATE) + .build() + ) + try { + when (val result = session.await()) { + is Session.State.Succeeded -> return + is Session.State.Failed -> { + throw UninstallFailedException(result.failure.message ?: result.failure.javaClass.simpleName) + } + } + } catch (e: CancellationException) { + throw UninstallCancelledException().initCause(e) + } + } + + private fun extractFailure(result: Session.State.Completed): InstallFailure? = + when (result) { + is Session.State.Succeeded -> null + is Session.State.Failed -> result.failure + } + + fun isShizukuInstalled(): Boolean { + if (Sui.isSui()) return true + return runCatching { + app.packageManager.getPackageInfo(SHIZUKU_PACKAGE, 0) + }.isSuccess + } + + fun shizukuAvailability(@Suppress("UNUSED_PARAMETER") target: InstallerManager.InstallTarget): InstallerManager.Availability { + if (Shizuku.isPreV11()) { + return InstallerManager.Availability(false, R.string.installer_status_shizuku_unsupported) + } + val binderReady = runCatching { Shizuku.pingBinder() }.getOrElse { false } + if (!binderReady) { + return InstallerManager.Availability(false, R.string.installer_status_shizuku_not_running) + } + val permissionGranted = runCatching { + Shizuku.checkSelfPermission() == PackageManager.PERMISSION_GRANTED + }.getOrElse { false } + if (!permissionGranted) { + return InstallerManager.Availability(false, R.string.installer_status_shizuku_permission) + } + return InstallerManager.Availability(true) + } + + fun launchShizukuApp(): Boolean { + val intent = app.packageManager.getLaunchIntentForPackage(SHIZUKU_PACKAGE) + ?: return false + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + app.startActivity(intent) + return true + } + + companion object { + internal const val SHIZUKU_PACKAGE = "moe.shizuku.privileged.api" + } +} + +/** Thrown when the user dismissed the system install dialog. */ +class InstallCancelledException : Exception("Installation cancelled by user") + +/** Thrown when the user dismissed the system uninstall dialog. */ +class UninstallCancelledException : Exception("Uninstall cancelled by user") + +/** Thrown when Ackpine reports a non-abort uninstall failure. */ +class UninstallFailedException(reason: String) : Exception(reason) diff --git a/app/src/main/java/app/morphe/manager/domain/installer/InstallerFileProvider.kt b/app/src/main/java/app/morphe/manager/domain/installer/InstallerFileProvider.kt index 7061275f9..d21332e92 100644 --- a/app/src/main/java/app/morphe/manager/domain/installer/InstallerFileProvider.kt +++ b/app/src/main/java/app/morphe/manager/domain/installer/InstallerFileProvider.kt @@ -14,7 +14,7 @@ import java.io.FileNotFoundException /** * Lightweight content provider used to expose APK files to external installers. * - * It mirrors the behaviour we relied on from [androidx.core.content.FileProvider] while avoiding + * It mirrors the behavior we relied on from [androidx.core.content.FileProvider] while avoiding * the XML parsing crash that occurred on some devices when launching third-party installers. */ class InstallerFileProvider : ContentProvider() { @@ -26,7 +26,7 @@ class InstallerFileProvider : ContentProvider() { selection: String?, selectionArgs: Array?, sortOrder: String? - ): Cursor? { + ): Cursor { val columns = projection?.takeIf { it.isNotEmpty() } ?: arrayOf(OpenableColumns.DISPLAY_NAME, OpenableColumns.SIZE) val file = buildFile(contextOrThrow(), uri) @@ -79,11 +79,10 @@ class InstallerFileProvider : ContentProvider() { companion object { private const val APK_MIME = "application/vnd.android.package-archive" + const val SHARE_DIR = "installer_share" fun authority(context: Context): String = "${context.packageName}.installerfileprovider" - fun buildUri(context: Context, file: File): Uri = buildUri(context, file.name) - fun buildUri(context: Context, fileName: String): Uri = Uri.Builder() .scheme("content") @@ -91,6 +90,27 @@ class InstallerFileProvider : ContentProvider() { .appendPath(fileName) .build() + /** + * Copies [file] into the share directory under its original name (overwriting if size + * differs) and returns a content URI for it. Used by [AckpineInstaller] for internal + * and Shizuku installs, and by [InstallerManager] for external installer intents. + */ + fun getUriForFile(context: Context, file: File): Uri { + val shareDir = File(context.cacheDir, SHARE_DIR).also { it.mkdirs() } + val dest = File(shareDir, file.name) + // Always copy if size or mtime differs. We stamp dest with the source's mtime + // after each copy, so dest.lastModified() == file.lastModified() on the next + // call only if the source hasn't changed — skipping the copy is then safe. + // Using < instead of != would silently serve a stale cached APK when the patcher + // rebuilds the same-named output file without changing its size, causing + // INSTALL_FAILED_UPDATE_INCOMPATIBLE on update installs + if (!dest.exists() || dest.length() != file.length() || dest.lastModified() != file.lastModified()) { + file.copyTo(dest, overwrite = true) + dest.setLastModified(file.lastModified()) + } + return buildUri(context, dest.name) + } + private fun buildFile(context: Context, uri: Uri): File { if (uri.authority != authority(context)) { throw IllegalArgumentException("Unknown authority: ${uri.authority}") @@ -102,7 +122,7 @@ class InstallerFileProvider : ContentProvider() { val fileName = segments.first() require(".." !in fileName) { "Path traversal is not allowed." } - val dir = File(context.cacheDir, InstallerManager.SHARE_DIR) + val dir = File(context.cacheDir, SHARE_DIR) val target = File(dir, fileName) val canonicalDir = dir.canonicalFile val canonicalTarget = target.canonicalFile diff --git a/app/src/main/java/app/morphe/manager/domain/installer/InstallerManager.kt b/app/src/main/java/app/morphe/manager/domain/installer/InstallerManager.kt index 4acac6071..2b773daa0 100644 --- a/app/src/main/java/app/morphe/manager/domain/installer/InstallerManager.kt +++ b/app/src/main/java/app/morphe/manager/domain/installer/InstallerManager.kt @@ -1,11 +1,15 @@ package app.morphe.manager.domain.installer import android.Manifest +import android.annotation.SuppressLint import android.app.Application import android.content.ClipData import android.content.ComponentName import android.content.Intent -import android.content.pm.* +import android.content.pm.ActivityInfo +import android.content.pm.ApplicationInfo +import android.content.pm.PackageManager +import android.content.pm.ResolveInfo import android.graphics.drawable.Drawable import android.net.Uri import android.util.Log @@ -14,18 +18,14 @@ import app.morphe.manager.R import app.morphe.manager.domain.manager.InstallerPreferenceTokens import app.morphe.manager.domain.manager.PreferencesManager import java.io.File -import java.io.IOException -import java.util.Locale -import java.util.UUID class InstallerManager( private val app: Application, private val prefs: PreferencesManager, private val rootInstaller: RootInstaller, - private val shizukuInstaller: ShizukuInstaller + private val ackpineInstaller: AckpineInstaller ) { private val packageManager: PackageManager = app.packageManager - private val shareDir: File = File(app.cacheDir, SHARE_DIR).apply { mkdirs() } private val dummyUri: Uri = InstallerFileProvider.buildUri(app, "dummy.apk") private val defaultInstallerComponent: ComponentName? by lazy { resolveDefaultInstallerComponent() } private val defaultInstallerPackage: String? get() = defaultInstallerComponent?.packageName @@ -223,10 +223,10 @@ class InstallerManager( if (!availabilityFor(token, target).available) { null } else { - val shared = copyToShareDir(sourceFile) - val uri = InstallerFileProvider.buildUri(app, shared) + val uri = InstallerFileProvider.getUriForFile(app, sourceFile) val intent = Intent(Intent.ACTION_VIEW).apply { setDataAndType(uri, APK_MIME) + @SuppressLint("WrongConstant") addFlags( Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION or @@ -238,6 +238,7 @@ class InstallerManager( putExtra(Intent.EXTRA_INSTALLER_PACKAGE_NAME, app.packageName) component = token.componentName } + @SuppressLint("WrongConstant") app.grantUriPermission( token.componentName.packageName, uri, @@ -248,7 +249,7 @@ class InstallerManager( InstallPlan.External( target = target, intent = intent, - sharedFile = shared, + sharedFile = File(app.cacheDir, "${InstallerFileProvider.SHARE_DIR}/${sourceFile.name}"), uri = uri, expectedPackage = expectedPackage, installerLabel = resolveLabel(token.componentName), @@ -302,7 +303,7 @@ class InstallerManager( label = app.getString(R.string.installer_shizuku_name), description = app.getString(R.string.installer_shizuku_description), availability = availabilityFor(Token.Shizuku, target, checkRoot), - icon = if (shizukuInstaller.isInstalled()) loadInstallerIcon(ShizukuInstaller.PACKAGE_NAME) else null + icon = if (ackpineInstaller.isShizukuInstalled()) loadInstallerIcon(AckpineInstaller.SHIZUKU_PACKAGE) else null ) is Token.Component -> { @@ -317,21 +318,6 @@ class InstallerManager( } } - private fun copyToShareDir(source: File): File { - val target = File(shareDir, "${UUID.randomUUID()}.apk") - try { - source.inputStream().use { input -> - target.outputStream().use { output -> - input.copyTo(output) - } - } - } catch (error: IOException) { - target.delete() - throw error - } - return target - } - private fun buildSequence(target: InstallTarget): List { val tokens = mutableListOf() val primary = getPrimaryToken() @@ -358,14 +344,14 @@ class InstallerManager( Token.AutoSaved -> if (!target.supportsRoot) { Availability(false, R.string.installer_status_not_supported) } else if (checkRoot) { - // Expert mode: check root access + // Check root access if (!rootInstaller.hasRootAccess()) { Availability(false, R.string.installer_status_requires_root) } else { Availability(true) } } else { - // Morphe mode: check if device is rooted without requesting access + // Check if device is rooted without requesting access. // This prevents showing root installer on non-rooted devices if (!rootInstaller.isDeviceRooted()) { Availability(false, R.string.installer_status_requires_root) @@ -376,11 +362,11 @@ class InstallerManager( } Token.Shizuku -> { - if (!shizukuInstaller.isInstalled()) { + if (!ackpineInstaller.isShizukuInstalled()) { Availability(false, R.string.installer_status_shizuku_not_installed) } else if (checkRoot) { // Full availability check - shizukuInstaller.availability(target) + ackpineInstaller.shizukuAvailability(target) } else { // Just verify Shizuku is installed (for UI display) Availability(true) @@ -524,42 +510,37 @@ class InstallerManager( companion object { private const val APK_MIME = "application/vnd.android.package-archive" - internal const val SHARE_DIR = "installer_share" private const val AOSP_INSTALLER_PACKAGE = "com.google.android.packageinstaller" private const val AOSP_INSTALLER_LABEL = "Package installer" - private const val TAG = "InstallerManager" + private const val TAG = "Morphe InstallerManager" } - fun openShizukuApp(): Boolean = shizukuInstaller.launchApp() - - fun formatFailureHint(status: Int, extraMessage: String?): String? { - val normalizedExtra = extraMessage?.takeIf { it.isNotBlank() } - val base = when (status) { - PackageInstaller.STATUS_FAILURE -> app.getString(R.string.installer_hint_generic) - PackageInstaller.STATUS_FAILURE_ABORTED -> app.getString(R.string.installer_hint_aborted) - PackageInstaller.STATUS_FAILURE_BLOCKED -> app.getString(R.string.installer_hint_blocked) - PackageInstaller.STATUS_FAILURE_CONFLICT -> app.getString(R.string.installer_hint_conflict) - PackageInstaller.STATUS_FAILURE_INCOMPATIBLE -> app.getString(R.string.installer_hint_incompatible) - PackageInstaller.STATUS_FAILURE_INVALID -> app.getString(R.string.installer_hint_invalid) - PackageInstaller.STATUS_FAILURE_STORAGE -> app.getString(R.string.installer_hint_storage) - PackageInstaller.STATUS_FAILURE_TIMEOUT -> app.getString(R.string.installer_hint_timeout) - else -> null - } + fun openShizukuApp(): Boolean = ackpineInstaller.launchShizukuApp() - return when { - base == null -> normalizedExtra - normalizedExtra == null -> base - else -> app.getString(R.string.installer_hint_with_reason, base, normalizedExtra) + /** + * Returns a deduplicated list of entries for [target], ensuring [token] is always present + * even if it's not in the raw list (e.g. a previously selected external installer). + */ + fun ensureValidEntries( + entries: List, + token: Token, + target: InstallTarget + ): List { + val normalized = buildList { + val seen = mutableSetOf() + entries.forEach { entry -> + val key = when (val t = entry.token) { + is Token.Component -> t.componentName + else -> t + } + if (seen.add(key)) add(entry) + } } - } - - fun isSignatureMismatch(message: String?): Boolean { - val normalized = message?.lowercase(Locale.ROOT)?.trim().orEmpty() - if (normalized.isEmpty()) return false - return normalized.contains("install_failed_update_incompatible") || - normalized.contains("install_failed_signature_inconsistent") || - normalized.contains("signatures do not match") || - normalized.contains("signature mismatch") + val tokenExists = token == Token.Internal || + token == Token.AutoSaved || + normalized.any { tokensEqual(it.token, token) } + return if (tokenExists) normalized + else describeEntry(token, target)?.let { normalized + it } ?: normalized } } diff --git a/app/src/main/java/app/morphe/manager/domain/installer/ShizukuInstaller.kt b/app/src/main/java/app/morphe/manager/domain/installer/ShizukuInstaller.kt deleted file mode 100644 index 6350ebdc0..000000000 --- a/app/src/main/java/app/morphe/manager/domain/installer/ShizukuInstaller.kt +++ /dev/null @@ -1,256 +0,0 @@ -package app.morphe.manager.domain.installer - -import android.app.Application -import android.content.Intent -import android.content.IntentSender -import android.content.pm.IPackageInstaller -import android.content.pm.IPackageInstallerSession -import android.content.pm.IPackageManager -import android.content.pm.PackageInstaller -import android.content.pm.PackageManager -import android.os.Build -import android.os.Bundle -import android.os.IBinder -import android.os.Process -import android.os.RemoteException -import app.morphe.manager.R -import kotlinx.coroutines.CompletableDeferred -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.withContext -import rikka.shizuku.Shizuku -import rikka.shizuku.ShizukuBinderWrapper -import rikka.shizuku.ShizukuProvider -import rikka.shizuku.SystemServiceHelper -import rikka.sui.Sui -import java.io.File -import java.io.IOException -import java.lang.reflect.Constructor - -class ShizukuInstaller(private val app: Application) { - - init { - val isSui = Sui.init(app.packageName) - if (!isSui) { - runCatching { ShizukuProvider.requestBinderForNonProviderProcess(app) } - } - } - - data class InstallResult(val status: Int, val message: String?) - - fun availability(target: InstallerManager.InstallTarget): InstallerManager.Availability { - if (Shizuku.isPreV11()) { - return InstallerManager.Availability(false, R.string.installer_status_shizuku_unsupported) - } - val binderReady = runCatching { Shizuku.pingBinder() }.getOrElse { false } - if (!binderReady) { - return InstallerManager.Availability(false, R.string.installer_status_shizuku_not_running) - } - val permissionGranted = runCatching { Shizuku.checkSelfPermission() == PackageManager.PERMISSION_GRANTED }.getOrElse { false } - if (!permissionGranted) { - return InstallerManager.Availability(false, R.string.installer_status_shizuku_permission) - } - return InstallerManager.Availability(true) - } - - fun isInstalled(): Boolean { - if (Sui.isSui()) return true - return runCatching { - app.packageManager.getPackageInfo(PACKAGE_NAME, 0) - }.isSuccess - } - - fun launchApp(): Boolean { - val intent = app.packageManager.getLaunchIntentForPackage(PACKAGE_NAME) - ?: return false - intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) - app.startActivity(intent) - return true - } - - suspend fun install(sourceFile: File, expectedPackage: String): InstallResult = withContext(Dispatchers.IO) { - val packageInstaller = obtainPackageInstaller() - val isRoot = runCatching { Shizuku.getUid() }.getOrDefault(-1) == 0 - val installerPackageName = if (isRoot) app.packageName else SHELL_PACKAGE - val installerAttributionTag = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) app.attributionTag else null - val userId = if (isRoot) currentUserId() else 0 - - val packageInstallerWrapper = PackageInstallerCompat.createPackageInstaller( - packageInstaller, - installerPackageName, - installerAttributionTag, - userId, - app - ) - val params = PackageInstaller.SessionParams(PackageInstaller.SessionParams.MODE_FULL_INSTALL).apply { - runCatching { setAppPackageName(expectedPackage) } - setInstallReason(PackageManager.INSTALL_REASON_USER) - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { - setRequestUpdateOwnership(true) - } - } - PackageInstallerCompat.applyFlags(params) - - val sessionId = packageInstallerWrapper.createSession(params) - val sessionBinder = IPackageInstallerSession.Stub.asInterface( - ShizukuBinderWrapper(packageInstaller.openSession(sessionId).asBinder()) - ) - val session = PackageInstallerCompat.createSession(sessionBinder) - - try { - sourceFile.inputStream().use { input -> - session.openWrite(BASE_APK_NAME, 0, sourceFile.length()).use { output -> - input.copyTo(output) - session.fsync(output) - } - } - - val resultDeferred = CompletableDeferred() - val intentSender = IntentSenderCompat.create { intent -> - val status = intent.getIntExtra(PackageInstaller.EXTRA_STATUS, PackageInstaller.STATUS_FAILURE) - val message = intent.getStringExtra(PackageInstaller.EXTRA_STATUS_MESSAGE) - resultDeferred.complete(InstallResult(status, message)) - } - - session.commit(intentSender) - val result = resultDeferred.await() - if (result.status != PackageInstaller.STATUS_SUCCESS) { - throw InstallerOperationException(result.status, result.message) - } - result - } finally { - runCatching { session.close() } - } - } - - private fun obtainPackageInstaller(): IPackageInstaller { - val binder = SystemServiceHelper.getSystemService("package") - ?: throw IOException("Package service unavailable") - try { - val manager = IPackageManager.Stub.asInterface(ShizukuBinderWrapper(binder)) - val installer = manager.packageInstaller - return IPackageInstaller.Stub.asInterface(ShizukuBinderWrapper(installer.asBinder())) - } catch (error: RemoteException) { - throw IOException(error) - } - } - - private fun currentUserId(): Int = Process.myUid() / 100000 - - class InstallerOperationException(val status: Int, override val message: String?) : Exception(message) - - companion object { - private const val SHELL_PACKAGE = "com.android.shell" - private const val BASE_APK_NAME = "base.apk" - internal const val PACKAGE_NAME = "moe.shizuku.privileged.api" - } -} - -private object PackageInstallerCompat { - private const val INSTALL_REPLACE_EXISTING = 0x00000002 - private const val INSTALL_ALLOW_TEST = 0x00000004 - - fun createPackageInstaller( - remote: IPackageInstaller, - installerPackageName: String, - installerAttributionTag: String?, - userId: Int, - app: Application - ): PackageInstaller { - return try { - when { - Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> { - PackageInstaller::class.java - .getDeclaredConstructor( - IPackageInstaller::class.java, - String::class.java, - String::class.java, - Int::class.javaPrimitiveType - ) - .apply { isAccessible = true } - .newInstance(remote, installerPackageName, installerAttributionTag, userId) - } - Build.VERSION.SDK_INT >= Build.VERSION_CODES.O -> { - PackageInstaller::class.java - .getDeclaredConstructor( - IPackageInstaller::class.java, - String::class.java, - Int::class.javaPrimitiveType - ) - .apply { isAccessible = true } - .newInstance(remote, installerPackageName, userId) - } - else -> { - PackageInstaller::class.java - .getDeclaredConstructor( - android.content.Context::class.java, - PackageManager::class.java, - IPackageInstaller::class.java, - String::class.java, - Int::class.javaPrimitiveType - ) - .apply { isAccessible = true } - .newInstance(app, app.packageManager, remote, installerPackageName, userId) - } - } - } catch (error: ReflectiveOperationException) { - throw RuntimeException(error) - } - } - - fun createSession(remote: IPackageInstallerSession): PackageInstaller.Session { - return try { - PackageInstaller.Session::class.java - .getDeclaredConstructor(IPackageInstallerSession::class.java) - .apply { isAccessible = true } - .newInstance(remote) - } catch (error: ReflectiveOperationException) { - throw RuntimeException(error) - } - } - - fun applyFlags(params: PackageInstaller.SessionParams) { - runCatching { - val field = PackageInstaller.SessionParams::class.java.getDeclaredField("installFlags") - field.isAccessible = true - val current = field.getInt(params) - field.setInt(params, current or INSTALL_REPLACE_EXISTING or INSTALL_ALLOW_TEST) - } - } -} - -private object IntentSenderCompat { - fun create(callback: (Intent) -> Unit): IntentSender { - val binder = object : android.content.IIntentSender.Stub() { - override fun send( - code: Int, - intent: Intent?, - resolvedType: String?, - finishedReceiver: android.content.IIntentReceiver?, - requiredPermission: String?, - options: Bundle? - ): Int { - intent?.let(callback) - return 0 - } - - override fun send( - code: Int, - intent: Intent?, - resolvedType: String?, - whitelistToken: IBinder?, - finishedReceiver: android.content.IIntentReceiver?, - requiredPermission: String?, - options: Bundle? - ) { - intent?.let(callback) - } - } - return try { - val ctor: Constructor = IntentSender::class.java.getDeclaredConstructor(android.content.IIntentSender::class.java) - ctor.isAccessible = true - ctor.newInstance(binder) - } catch (error: ReflectiveOperationException) { - throw RuntimeException(error) - } - } -} diff --git a/app/src/main/java/app/morphe/manager/domain/manager/PreferencesManager.kt b/app/src/main/java/app/morphe/manager/domain/manager/PreferencesManager.kt index f81f6c9ba..8b4953574 100644 --- a/app/src/main/java/app/morphe/manager/domain/manager/PreferencesManager.kt +++ b/app/src/main/java/app/morphe/manager/domain/manager/PreferencesManager.kt @@ -12,9 +12,11 @@ import app.morphe.manager.patcher.runtime.calculateAdaptiveMemoryLimit import app.morphe.manager.ui.screen.shared.BackgroundType import app.morphe.manager.ui.theme.Theme import app.morphe.manager.ui.viewmodel.BundleSnapshot +import app.morphe.manager.ui.viewmodel.RandomInterval import app.morphe.manager.util.isArmV7 import app.morphe.manager.util.tag import app.morphe.manager.worker.UpdateCheckInterval +import app.morphe.patcher.dex.BytecodeMode import kotlinx.coroutines.runBlocking import kotlinx.serialization.Serializable @@ -25,9 +27,11 @@ class PreferencesManager( // Appearance tab val backgroundType = enumPreference("background_type", BackgroundType.CIRCLES) val enableBackgroundParallax = booleanPreference("enable_background_parallax", true) + val randomBackgroundInterval = enumPreference("random_background_interval", RandomInterval.ON_LAUNCH) val dynamicColor = booleanPreference("dynamic_color", true) val pureBlackTheme = booleanPreference("pure_black_theme", false) + val showGreetingPhrases = booleanPreference("show_greeting_phrases", true) val themePresetSelectionEnabled = booleanPreference("theme_preset_selection_enabled", true) val themePresetSelectionName = stringPreference("theme_preset_selection_name", "DEFAULT") val customAccentColor = stringPreference("custom_accent_color", "") @@ -58,6 +62,15 @@ class PreferencesManager( val stripUnusedNativeLibs = booleanPreference("strip_unused_native_libs", false) + /** + * Bytecode processing mode for the patcher. + * Defaults to [BytecodeMode.STRIP_FAST]. + */ + val bytecodeModePreference = enumPreference( + "bytecode_mode", + BytecodeMode.STRIP_FAST + ) + // System tab val installerPrimary = stringPreference("installer_primary", InstallerPreferenceTokens.INTERNAL) val promptInstallerOnInstall = booleanPreference("prompt_installer_on_install", false) @@ -65,9 +78,9 @@ class PreferencesManager( val installerHiddenComponents = stringSetPreference("installer_hidden_components", emptySet()) val useProcessRuntime = booleanPreference( - "use_process_runtime", - // Use process runtime fails for Android 10 and lower. - // Armv7 silently fails and nobody has researched why yet. + "process_runtime", // Old key was 'use_process_runtime' and may have the wrong default for some devices. + // Process runtime fails for Android 10 and lower. + // Armv7 silently fails and nobody has researched why. Build.VERSION.SDK_INT >= Build.VERSION_CODES.R && !isArmV7() ) val patcherProcessMemoryLimit = intPreference("use_process_runtime_memory_limit", PROCESS_RUNTIME_MEMORY_NOT_SET) @@ -81,10 +94,11 @@ class PreferencesManager( val allowMeteredUpdates = booleanPreference("allow_metered_updates", true) val firstLaunch = booleanPreference("first_launch", true) + val installationTime = longPreference("manager_installation_time", 0) val disablePatchVersionCompatCheck = booleanPreference("disable_patch_version_compatibility_check", false) - // Hidden preference to track if prerelease was auto-enabled + /** Hidden preference to track if prerelease was auto-enabled. */ private val prereleaseAutoEnabled = booleanPreference("prerelease_auto_enabled", false) init { @@ -159,11 +173,14 @@ class PreferencesManager( val patchSelectionHiddenActions: Set? = null, val acknowledgedDownloaderPlugins: Set? = null, val autoSaveDownloaderApks: Boolean? = null, + val showGreetingPhrases: Boolean? = null, val backgroundType: BackgroundType? = null, + val randomBackgroundInterval: RandomInterval? = null, val useExpertMode: Boolean? = null, val backgroundUpdateNotifications: Boolean? = null, val updateCheckInterval: UpdateCheckInterval? = null, val customBundles: List? = null, + val bytecodeModePreference: BytecodeMode? = null, ) suspend fun exportSettings() = SettingsSnapshot( @@ -191,10 +208,13 @@ class PreferencesManager( bundlePrereleasesEnabled = bundlePrereleasesEnabled.get(), bundleExperimentalVersionsEnabled = bundleExperimentalVersionsEnabled.get(), disablePatchVersionCompatCheck = disablePatchVersionCompatCheck.get(), + showGreetingPhrases = showGreetingPhrases.get(), backgroundType = backgroundType.get(), + randomBackgroundInterval = randomBackgroundInterval.get(), useExpertMode = useExpertMode.get(), backgroundUpdateNotifications = backgroundUpdateNotifications.get(), - updateCheckInterval = updateCheckInterval.get() + updateCheckInterval = updateCheckInterval.get(), + bytecodeModePreference = bytecodeModePreference.get(), ) suspend fun importSettings(snapshot: SettingsSnapshot) = edit { @@ -222,10 +242,13 @@ class PreferencesManager( snapshot.bundlePrereleasesEnabled?.let { bundlePrereleasesEnabled.value = it } snapshot.bundleExperimentalVersionsEnabled?.let { bundleExperimentalVersionsEnabled.value = it } snapshot.disablePatchVersionCompatCheck?.let { disablePatchVersionCompatCheck.value = it } + snapshot.showGreetingPhrases?.let { showGreetingPhrases.value = it } snapshot.backgroundType?.let { backgroundType.value = it } + snapshot.randomBackgroundInterval?.let { randomBackgroundInterval.value = it } snapshot.useExpertMode?.let { useExpertMode.value = it } snapshot.backgroundUpdateNotifications?.let { backgroundUpdateNotifications.value = it } snapshot.updateCheckInterval?.let { updateCheckInterval.value = it } + snapshot.bytecodeModePreference?.let { bytecodeModePreference.value = it } } companion object { diff --git a/app/src/main/java/app/morphe/manager/domain/repository/InstalledAppRepository.kt b/app/src/main/java/app/morphe/manager/domain/repository/InstalledAppRepository.kt index c0e49951f..bf5160e51 100644 --- a/app/src/main/java/app/morphe/manager/domain/repository/InstalledAppRepository.kt +++ b/app/src/main/java/app/morphe/manager/domain/repository/InstalledAppRepository.kt @@ -14,7 +14,7 @@ import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.first import kotlinx.coroutines.withContext -private const val TAG = "InstalledAppRepository" +private const val TAG = "Morphe InstalledAppRepository" class InstalledAppRepository( db: AppDatabase, diff --git a/app/src/main/java/app/morphe/manager/domain/repository/OriginalApkRepository.kt b/app/src/main/java/app/morphe/manager/domain/repository/OriginalApkRepository.kt index 7147679ed..bbbae8bf5 100644 --- a/app/src/main/java/app/morphe/manager/domain/repository/OriginalApkRepository.kt +++ b/app/src/main/java/app/morphe/manager/domain/repository/OriginalApkRepository.kt @@ -10,7 +10,7 @@ import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.withContext import java.io.File -private const val TAG = "OriginalApkRepository" +private const val TAG = "Morphe OriginalApkRepository" class OriginalApkRepository( db: AppDatabase, diff --git a/app/src/main/java/app/morphe/manager/domain/repository/PatchBundleRepository.kt b/app/src/main/java/app/morphe/manager/domain/repository/PatchBundleRepository.kt index 15d617415..93e025887 100644 --- a/app/src/main/java/app/morphe/manager/domain/repository/PatchBundleRepository.kt +++ b/app/src/main/java/app/morphe/manager/domain/repository/PatchBundleRepository.kt @@ -17,9 +17,13 @@ import app.morphe.manager.data.room.bundles.PatchBundleProperties import app.morphe.manager.data.room.bundles.Source import app.morphe.manager.domain.bundles.* import app.morphe.manager.domain.manager.PreferencesManager +import app.morphe.manager.network.utils.APIError +import app.morphe.manager.patcher.patch.BundleAppMetadata import app.morphe.manager.patcher.patch.PatchBundle import app.morphe.manager.patcher.patch.PatchBundleInfo +import app.morphe.manager.ui.viewmodel.BundleSnapshot import app.morphe.manager.util.* +import io.ktor.client.plugins.ResponseException import io.ktor.http.Url import kotlinx.collections.immutable.PersistentMap import kotlinx.collections.immutable.mutate @@ -39,10 +43,6 @@ import java.nio.ByteOrder import java.nio.charset.StandardCharsets import java.security.MessageDigest import java.util.Locale -import app.morphe.manager.util.syncFcmTopics -import app.morphe.manager.network.utils.APIError -import app.morphe.manager.ui.viewmodel.BundleSnapshot -import io.ktor.client.plugins.ResponseException import app.morphe.manager.data.room.bundles.Source as SourceInfo class PatchBundleRepository( @@ -55,20 +55,33 @@ class PatchBundleRepository( private val bundlesDir = app.getDir("patch_bundles", Context.MODE_PRIVATE) private val scope = CoroutineScope(Dispatchers.Default) - private val store = Store(scope, State()) + private val store = Store(scope, BundleState.Loading) + + val bundleState: StateFlow = store.state + .stateIn(scope, SharingStarted.Eagerly, BundleState.Loading) - val sources = store.state.map { it.sources.values.toList() } + val sources = store.state.map { (it as? BundleState.Ready)?.sources?.values?.toList() ?: emptyList() } val bundles = store.state.map { - it.sources.mapNotNull { (uid, src) -> + (it as? BundleState.Ready)?.sources?.mapNotNull { (uid, src) -> uid to (src.patchBundle ?: return@mapNotNull null) - }.toMap() + }?.toMap() ?: emptyMap() } - val allBundlesInfoFlow = store.state.map { it.info } + val allBundlesInfoFlow = store.state.map { (it as? BundleState.Ready)?.info ?: persistentMapOf() } val enabledBundlesInfoFlow = allBundlesInfoFlow.map { info -> info.filter { (_, bundleInfo) -> bundleInfo.enabled } } val bundleInfoFlow = enabledBundlesInfoFlow + /** + * Pre-built [BundleAppMetadata] map, updated whenever enabled bundles change. + * Shared across all consumers so [BundleAppMetadata.buildFrom] is never called more + * than once per bundle reload. + */ + val appMetadata: StateFlow> = + bundleInfoFlow + .map { BundleAppMetadata.buildFrom(it) } + .stateIn(scope, SharingStarted.Eagerly, emptyMap()) + fun scopedBundleInfoFlow(packageName: String, version: String?) = enabledBundlesInfoFlow.map { it.map { (_, bundleInfo) -> bundleInfo.forPackage( @@ -310,10 +323,10 @@ class PatchBundleRepository( private suspend inline fun dispatchAction( name: String, - crossinline block: suspend ActionContext.(current: State) -> State + crossinline block: suspend ActionContext.(current: BundleState) -> BundleState ) { - store.dispatch(object : Action { - override suspend fun ActionContext.execute(current: State) = block(current) + store.dispatch(object : Action { + override suspend fun ActionContext.execute(current: BundleState) = block(current) override fun toString() = name }) } @@ -321,7 +334,7 @@ class PatchBundleRepository( /** * Performs a reload. Do not call this outside of a store action. */ - private suspend fun doReload(): State { + private suspend fun doReload(): BundleState.Ready { val entities = loadEntitiesEnforcingOfficialOrder() val sources = entities.associate { it.uid to it.load() }.toMutableMap() @@ -330,22 +343,23 @@ class PatchBundleRepository( if (hasOutOfDateNames) dispatchAction( "Sync names" ) { state -> - val nameChanges = state.sources.mapNotNull { (_, src) -> + val ready = state as? BundleState.Ready ?: return@dispatchAction state + val nameChanges = ready.sources.mapNotNull { (_, src) -> if (!src.isNameOutOfDate) return@mapNotNull null val newName = src.patchBundle?.manifestAttributes?.name?.takeIf { it != src.name } ?: return@mapNotNull null src.uid to newName } - val sources = state.sources.toMutableMap() - val info = state.info.toMutableMap() + val sources = ready.sources.toMutableMap() + val info = ready.info.toMutableMap() nameChanges.forEach { (uid, name) -> updateDb(uid) { it.copy(name = name) } sources[uid] = sources[uid]!!.copy(name = name) info[uid] = info[uid]?.copy(name = name) ?: return@forEach } - State(sources.toPersistentMap(), info.toPersistentMap()) + ready.copy(sources = sources.toPersistentMap(), info = info.toPersistentMap()) } val info = loadMetadata(sources).toMutableMap() @@ -363,7 +377,7 @@ class PatchBundleRepository( } } - return State(sources.toPersistentMap(), info.toPersistentMap()) + return BundleState.Ready(sources.toPersistentMap(), info.toPersistentMap()) } suspend fun reload() = dispatchAction("Full reload") { @@ -410,7 +424,8 @@ class PatchBundleRepository( if (failures.isNotEmpty()) { dispatchAction("Mark bundles as failed") { state -> - state.copy(sources = state.sources.mutate { + val ready = state as? BundleState.Ready ?: return@dispatchAction state + ready.copy(sources = ready.sources.mutate { failures.forEach { (uid, throwable) -> it[uid] = it[uid]?.copy(error = throwable) ?: return@forEach } @@ -587,7 +602,7 @@ class PatchBundleRepository( suspend fun reset() = dispatchAction("Reset") { state -> dao.reset() - state.sources.keys.forEach { directoryOf(it).deleteRecursively() } + (state as? BundleState.Ready)?.sources?.keys?.forEach { directoryOf(it).deleteRecursively() } doReload() } @@ -680,8 +695,9 @@ class PatchBundleRepository( suspend fun remove(vararg bundles: PatchBundleSource) = dispatchAction("Remove (${bundles.map { it.uid }.joinToString(",")})") { state -> - val sources = state.sources.toMutableMap() - val info = state.info.toMutableMap() + val ready = state as? BundleState.Ready ?: return@dispatchAction state + val sources = ready.sources.toMutableMap() + val info = ready.info.toMutableMap() bundles.forEach { dao.remove(it.uid) directoryOf(it.uid).deleteRecursively() @@ -692,7 +708,7 @@ class PatchBundleRepository( val (affectedCount, remaining) = cancelRemoteUpdates(bundles.map { it.uid }.toSet()) updateProgressAfterRemoval(affectedCount, remaining) - State(sources.toPersistentMap(), info.toPersistentMap()) + ready.copy(sources = sources.toPersistentMap(), info = info.toPersistentMap()) } enum class DisplayNameUpdateResult { @@ -739,9 +755,10 @@ class PatchBundleRepository( if (result == DisplayNameUpdateResult.SUCCESS || result == DisplayNameUpdateResult.NO_CHANGE) { dispatchAction("Sync display name ($uid)") { state -> - val src = state.sources[uid] ?: return@dispatchAction state + val ready = state as? BundleState.Ready ?: return@dispatchAction state + val src = ready.sources[uid] ?: return@dispatchAction state val updated = src.copy(displayName = normalized) - state.copy(sources = state.sources.put(uid, updated)) + ready.copy(sources = ready.sources.put(uid, updated)) } } @@ -759,13 +776,14 @@ class PatchBundleRepository( prefs.bundlePrereleasesEnabled.update(current) dispatchAction("Set prerelease ($uid=$usePrerelease)") { state -> - val src = state.sources[uid] ?: return@dispatchAction state + val ready = state as? BundleState.Ready ?: return@dispatchAction state + val src = ready.sources[uid] ?: return@dispatchAction state val updated = when (src) { is APIPatchBundle -> src.copy(usePrerelease = usePrerelease) is JsonPatchBundle -> src.copy(usePrerelease = usePrerelease) else -> return@dispatchAction state } - state.copy(sources = state.sources.put(uid, updated)) + ready.copy(sources = ready.sources.put(uid, updated)) } // If this is the default Morphe Patches bundle, sync FCM patches topic @@ -780,7 +798,7 @@ class PatchBundleRepository( // Skip download if the bundle is disabled - it will be downloaded when re-enabled // via disable() which triggers startRemoteUpdateJob for newly enabled bundles. - val isEnabled = store.state.value.sources[uid]?.enabled == true + val isEnabled = (store.state.value as? BundleState.Ready)?.sources?.get(uid)?.enabled == true if (!isEnabled) return // Trigger update so the new channel takes effect immediately. @@ -1035,7 +1053,9 @@ class PatchBundleRepository( // Check for duplicate source - val isDuplicate = state.sources.values.any { src -> + val ready = state as? BundleState.Ready ?: return@dispatchAction state + + val isDuplicate = ready.sources.values.any { src -> src is RemotePatchBundle && src.endpoint.equals(normalizedUrl, ignoreCase = true) } @@ -1070,7 +1090,7 @@ class PatchBundleRepository( if (bundle.uid == src.uid) onProgress?.invoke(bytesRead, bytesTotal) } ) - state.copy(sources = state.sources.put(src.uid, src)) + ready.copy(sources = ready.sources.put(src.uid, src)) } /** @@ -1301,7 +1321,8 @@ class PatchBundleRepository( if (!allowMeteredUpdates && networkInfo.isMetered()) return null return try { - val remoteBundles = store.state.value.sources.values + val remoteBundles = (store.state.value as? BundleState.Ready)?.sources?.values + .orEmpty() .filterIsInstance() if (remoteBundles.isEmpty()) return null @@ -1346,12 +1367,12 @@ class PatchBundleRepository( private val allowUnsafeNetwork: Boolean = false, private val onPerBundleProgress: ((bundle: RemotePatchBundle, bytesRead: Long, bytesTotal: Long?) -> Unit)? = null, private val predicate: (bundle: RemotePatchBundle) -> Boolean = { true }, - ) : Action { + ) : Action { override fun toString() = if (force) "Redownload remote bundles" else "Update check" override suspend fun ActionContext.execute( - current: State - ): State { + current: BundleState + ): BundleState { startRemoteUpdateJob( force = force, showToast = showToast, @@ -1462,7 +1483,8 @@ class PatchBundleRepository( return@coroutineScope } - val targets = store.state.value.sources.values + val targets = (store.state.value as? BundleState.Ready)?.sources?.values + .orEmpty() .filterIsInstance() .filter { predicate(it) } @@ -1528,16 +1550,6 @@ class PatchBundleRepository( continue } catch (e: Exception) { handleBundleDownloadError(e, bundle) - // Auto-disable bundles that have never been downloaded successfully. - // Bundles that already have a local copy are left enabled so the user - // can still patch with the cached version despite the network error - if (!bundle.patchesJarFile.exists()) { - Log.w(tag, "Bundle ${bundle.name} has no local copy after failure; disabling automatically") - dispatchAction("Auto-disable failed uninstalled bundle (${bundle.uid})") { _ -> - updateDb(bundle.uid) { it.copy(enabled = false) } - doReload() - } - } continue } @@ -1662,9 +1674,10 @@ class PatchBundleRepository( private inner class ManualUpdateCheck( private val targetUids: Set? = null - ) : Action { - override suspend fun ActionContext.execute(current: State) = coroutineScope { - val manualBundles = current.sources.values + ) : Action { + override suspend fun ActionContext.execute(current: BundleState) = coroutineScope { + val ready = current as? BundleState.Ready ?: return@coroutineScope current + val manualBundles = ready.sources.values .filterIsInstance() .filter { targetUids?.contains(it.uid) ?: !it.autoUpdate @@ -1676,7 +1689,7 @@ class PatchBundleRepository( } else { manualUpdateInfoFlow.update { map -> map.filterKeys { uid -> - val bundle = current.sources[uid] as? RemotePatchBundle + val bundle = ready.sources[uid] as? RemotePatchBundle bundle != null && !bundle.autoUpdate } } @@ -1725,10 +1738,16 @@ class PatchBundleRepository( } } - data class State( - val sources: PersistentMap = persistentMapOf(), - val info: PersistentMap = persistentMapOf() - ) + sealed class BundleState { + /** DB not yet read — UI shows shimmer */ + data object Loading : BundleState() + + /** Pipeline ready (even if sources list is empty) */ + data class Ready( + val sources: PersistentMap = persistentMapOf(), + val info: PersistentMap = persistentMapOf(), + ) : BundleState() + } enum class BundleUpdateResult { None, // Update in progress @@ -1804,7 +1823,8 @@ class PatchBundleRepository( suspend fun importCustomBundles(snapshots: List) { if (snapshots.isEmpty()) return dispatchAction("Import custom bundles") { state -> - val existingEndpoints = state.sources.values + val ready = state as? BundleState.Ready ?: return@dispatchAction state + val existingEndpoints = ready.sources.values .filterIsInstance() .map { it.endpoint.lowercase(Locale.US) } .toSet() diff --git a/app/src/main/java/app/morphe/manager/network/api/MorpheAPI.kt b/app/src/main/java/app/morphe/manager/network/api/MorpheAPI.kt index 804785b10..d3a09d0cb 100644 --- a/app/src/main/java/app/morphe/manager/network/api/MorpheAPI.kt +++ b/app/src/main/java/app/morphe/manager/network/api/MorpheAPI.kt @@ -294,7 +294,7 @@ class MorpheAPI( suspend fun getAppUpdate(): MorpheAsset? { val usePrereleases = prefs.useManagerPrereleases.get() val currentWeight = versionWeight(BuildConfig.VERSION_NAME.removePrefix("v")) - val branch = if (usePrereleases || isDevBuild) "dev" else "main" + val branch = if (usePrereleases) "dev" else "main" val candidate = if (USE_MANAGER_DIRECT_JSON) { getManagerFromJson(branch).fallbackTo { diff --git a/app/src/main/java/app/morphe/manager/patcher/Session.kt b/app/src/main/java/app/morphe/manager/patcher/Session.kt index a7315e978..23dd920b1 100644 --- a/app/src/main/java/app/morphe/manager/patcher/Session.kt +++ b/app/src/main/java/app/morphe/manager/patcher/Session.kt @@ -11,6 +11,7 @@ import app.morphe.manager.patcher.Session.Companion.component1 import app.morphe.manager.patcher.Session.Companion.component2 import app.morphe.manager.patcher.logger.Logger import app.morphe.manager.ui.model.State +import app.morphe.patcher.dex.BytecodeMode import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import java.io.Closeable @@ -23,12 +24,12 @@ internal typealias PatchList = List> class Session( cacheDir: String, frameworkDir: String, - aaptPath: String, private val androidContext: Context, private val logger: Logger, private val input: File, private val onPatchCompleted: suspend () -> Unit, - private val onProgress: (name: String?, state: State?, message: String?) -> Unit + private val onProgress: (name: String?, state: State?, message: String?) -> Unit, + bytecodeMode: BytecodeMode = BytecodeMode.STRIP_FAST, ) : Closeable { private fun updateProgress(name: String? = null, state: State? = null, message: String? = null) = onProgress(name, state, message) @@ -39,7 +40,7 @@ class Session( apkFile = input, temporaryFilesPath = tempDir, frameworkFileDirectory = frameworkDir, - aaptBinaryPath = aaptPath + useBytecodeMode = bytecodeMode, ) ) diff --git a/app/src/main/java/app/morphe/manager/patcher/aapt/Aapt.kt b/app/src/main/java/app/morphe/manager/patcher/aapt/Aapt.kt deleted file mode 100644 index a271aeea3..000000000 --- a/app/src/main/java/app/morphe/manager/patcher/aapt/Aapt.kt +++ /dev/null @@ -1,12 +0,0 @@ -package app.morphe.manager.patcher.aapt - -import android.content.Context -import app.morphe.manager.patcher.LibraryResolver -import android.os.Build.SUPPORTED_ABIS as DEVICE_ABIS -object Aapt : LibraryResolver() { - private val WORKING_ABIS = setOf("arm64-v8a", "x86", "x86_64", "armeabi-v7a") - - fun supportsDevice() = (DEVICE_ABIS intersect WORKING_ABIS).isNotEmpty() - - fun binary(context: Context) = findLibraryExact(context, "libaapt2.so") -} diff --git a/app/src/main/java/app/morphe/manager/patcher/patch/PatchBundle.kt b/app/src/main/java/app/morphe/manager/patcher/patch/PatchBundle.kt index 9ac46c387..39d70e23e 100644 --- a/app/src/main/java/app/morphe/manager/patcher/patch/PatchBundle.kt +++ b/app/src/main/java/app/morphe/manager/patcher/patch/PatchBundle.kt @@ -1,5 +1,6 @@ package app.morphe.manager.patcher.patch +import android.os.Build import android.os.Parcelable import app.morphe.patcher.patch.Patch import app.morphe.patcher.patch.loadPatchesFromDex @@ -59,7 +60,18 @@ data class PatchBundle(val patchesJar: String) : Parcelable { private fun loadBundle(bundle: PatchBundle): Collection> { validateDexEntries(bundle.patchesJar) val patchFiles = runCatching { - loadPatchesFromDex(setOf(File(bundle.patchesJar))).byPatchesFile + val jarFile = File(bundle.patchesJar) + loadPatchesFromDex( + setOf(jarFile), + if (Build.VERSION.SDK_INT > Build.VERSION_CODES.O) { + null + } else { + // Must pass in any directory that exists. + // Directory is ignored with Android 8.0+, but is required + // for Android 8.0 otherwise NPE occurs. + jarFile.parentFile + } + ).byPatchesFile }.getOrElse { error -> throw IllegalStateException("Patch bundle is corrupted or incomplete", error) } diff --git a/app/src/main/java/app/morphe/manager/patcher/patch/PatchInfo.kt b/app/src/main/java/app/morphe/manager/patcher/patch/PatchInfo.kt index 6558349a1..c210c0b51 100644 --- a/app/src/main/java/app/morphe/manager/patcher/patch/PatchInfo.kt +++ b/app/src/main/java/app/morphe/manager/patcher/patch/PatchInfo.kt @@ -49,7 +49,16 @@ data class PatchInfo( } .toMap() .toImmutableMap() - .takeIf { it.isNotEmpty() } + .takeIf { it.isNotEmpty() }, + versionMinSdks = compatibility.targets + .mapNotNull { target -> + val v = target.version ?: return@mapNotNull null + val sdk = target.minSdk ?: return@mapNotNull null + v to sdk + } + .toMap() + .toImmutableMap() + .takeIf { it.isNotEmpty() }, ) } ?.toImmutableList() @@ -131,6 +140,8 @@ data class CompatiblePackage( val signatures: ImmutableSet? = null, /** Per-version user-facing descriptions. */ val versionDescriptions: ImmutableMap? = null, + /** Minimum Android SDK version required per app version. */ + val versionMinSdks: ImmutableMap? = null, ) @Immutable diff --git a/app/src/main/java/app/morphe/manager/patcher/runtime/CoroutineRuntime.kt b/app/src/main/java/app/morphe/manager/patcher/runtime/CoroutineRuntime.kt index ff978fcf4..23c207f69 100644 --- a/app/src/main/java/app/morphe/manager/patcher/runtime/CoroutineRuntime.kt +++ b/app/src/main/java/app/morphe/manager/patcher/runtime/CoroutineRuntime.kt @@ -24,70 +24,76 @@ class CoroutineRuntime(private val context: Context) : Runtime(context) { logger: Logger, onPatchCompleted: suspend () -> Unit, onProgress: ProgressEventHandler, - stripNativeLibs: Boolean, + skipUnneededSplits: Boolean, + onMergedApkReady: (suspend (File) -> Unit)?, ) { MemoryMonitor.startMemoryPolling(logger) - val selectedBundles = selectedPatches.keys - val bundles = bundles() - val uids = bundles.entries.associate { (key, value) -> value to key } + try { + val selectedBundles = selectedPatches.keys + val bundles = bundles() + val uids = bundles.entries.associate { (key, value) -> value to key } - val allPatches = - PatchBundle.Loader.patches(bundles.values, packageName) - .mapKeys { (b, _) -> uids[b]!! } - .filterKeys { it in selectedBundles } + val allPatches = + PatchBundle.Loader.patches(bundles.values, packageName) + .mapKeys { (b, _) -> uids[b]!! } + .filterKeys { it in selectedBundles } - val patchList = selectedPatches.flatMap { (bundle, selected) -> - allPatches[bundle]?.filter { it.name in selected } - ?: throw IllegalArgumentException("Patch bundle $bundle does not exist") - } + val patchList = selectedPatches.flatMap { (bundle, selected) -> + allPatches[bundle]?.filter { it.name in selected } + ?: throw IllegalArgumentException("Patch bundle $bundle does not exist") + } - // Set all patch options. - options.forEach { (bundle, bundlePatchOptions) -> - val patches = allPatches[bundle] ?: return@forEach - val patchesByName = patches.associateBy { it.name } + // Set all patch options. + options.forEach { (bundle, bundlePatchOptions) -> + val patches = allPatches[bundle] ?: return@forEach + val patchesByName = patches.associateBy { it.name } - bundlePatchOptions.forEach { (patchName, configuredPatchOptions) -> - // Morphe: Skip if patch doesn't exist in this bundle - val patch = patchesByName[patchName] ?: return@forEach + bundlePatchOptions.forEach { (patchName, configuredPatchOptions) -> + // Morphe: Skip if patch doesn't exist in this bundle + val patch = patchesByName[patchName] ?: return@forEach - configuredPatchOptions.forEach { (key, value) -> - patch.options[key] = value + configuredPatchOptions.forEach { (key, value) -> + patch.options[key] = value + } } } - } - onProgress(null, State.COMPLETED, null) // Loading patches + onProgress(null, State.COMPLETED, null) // Loading patches - val preparation = SplitApkPreparer.prepareIfNeeded( - File(inputFile), - File(cacheDir), - logger, - stripNativeLibs - ) - try { - if (preparation.merged) { - onProgress(null, State.COMPLETED, null) - } + val preparation = SplitApkPreparer.prepareIfNeeded( + source = File(inputFile), + workspace = File(cacheDir), + logger = logger, + skipUnneededSplits = skipUnneededSplits, + onProgress = { message -> onProgress(message, State.RUNNING, null) } + ) + + try { + if (preparation.merged) { + onProgress(null, State.COMPLETED, null) + onMergedApkReady?.invoke(preparation.file) + } - Session( - cacheDir, - frameworkPath, - aaptPath, - context, - logger, - preparation.file, - onPatchCompleted = onPatchCompleted, - onProgress - ).use { session -> - session.run( - File(outputFile), - patchList - ) + Session( + cacheDir = cacheDir, + frameworkDir = frameworkPath, + androidContext = context, + logger = logger, + input = preparation.file, + onPatchCompleted = onPatchCompleted, + onProgress = onProgress, + bytecodeMode = prefs.bytecodeModePreference.get(), + ).use { session -> + session.run( + File(outputFile), + patchList + ) + } + } finally { + preparation.cleanup() } } finally { - preparation.cleanup() - MemoryMonitor.stopMemoryPolling(logger) } } diff --git a/app/src/main/java/app/morphe/manager/patcher/runtime/ProcessRuntime.kt b/app/src/main/java/app/morphe/manager/patcher/runtime/ProcessRuntime.kt index 27142e251..73392ced3 100644 --- a/app/src/main/java/app/morphe/manager/patcher/runtime/ProcessRuntime.kt +++ b/app/src/main/java/app/morphe/manager/patcher/runtime/ProcessRuntime.kt @@ -15,6 +15,7 @@ import app.morphe.manager.patcher.runtime.process.IPatcherProcess import app.morphe.manager.patcher.runtime.process.Parameters import app.morphe.manager.patcher.runtime.process.PatchConfiguration import app.morphe.manager.patcher.runtime.process.PatcherProcess +import app.morphe.manager.patcher.split.SplitApkPreparer import app.morphe.manager.patcher.worker.ProgressEventHandler import app.morphe.manager.ui.model.State import app.morphe.manager.util.Options @@ -39,7 +40,6 @@ import kotlin.math.max const val PROCESS_RUNTIME_MEMORY_MINIMUM = 512 const val PROCESS_RUNTIME_MEMORY_MAX_LIMIT = 1280 const val PROCESS_RUNTIME_MEMORY_MAX_LIMIT_INITIALIZATION = 1024 -private const val PROCESS_RUNTIME_MEMORY_DEFAULT = 640 private const val PROCESS_RUNTIME_MEMORY_DEFAULT_MINIMUM = 640 const val PROCESS_RUNTIME_MEMORY_LOW_WARNING = 640 const val PROCESS_RUNTIME_MEMORY_STEP = 128 @@ -113,11 +113,11 @@ class ProcessRuntime( logger: Logger, onPatchCompleted: suspend () -> Unit, onProgress: ProgressEventHandler, - stripNativeLibs: Boolean, + skipUnneededSplits: Boolean, + onMergedApkReady: (suspend (File) -> Unit)?, ) = coroutineScope { - val minMemoryLimit = 200 + val minMemoryLimit = 256 var memoryMB = max(minMemoryLimit, prefs.patcherProcessMemoryLimit.get()) - var retried = false while (true) { try { @@ -128,21 +128,12 @@ class ProcessRuntime( packageName, selectedPatches, options, - stripNativeLibs, + skipUnneededSplits, logger, onPatchCompleted, - onProgress + onProgress, + onMergedApkReady ) - // Success - update preference and return. - if (retried && prefs.patcherProcessMemoryLimit.get() != memoryMB) { - if (memoryMB < PROCESS_RUNTIME_MEMORY_DEFAULT) { - // Don't save a value lower than the expected minimum. - // Instead, allow discovering the actual memory limit again next time. - memoryMB = PROCESS_RUNTIME_MEMORY_DEFAULT - } - Log.i(tag, "Updating process memory limit setting to: $memoryMB") - prefs.patcherProcessMemoryLimit.update(memoryMB) - } return@coroutineScope } catch (e: Exception) { @@ -153,7 +144,6 @@ class ProcessRuntime( } if (isMemoryFailure && !skipMemoryRetry && memoryMB > minMemoryLimit) { - retried = true memoryMB -= PROCESS_RUNTIME_MEMORY_STEP Log.i(tag, "Process memory limit failed, retrying with: $memoryMB") continue @@ -170,10 +160,11 @@ class ProcessRuntime( packageName: String, selectedPatches: PatchSelection, options: Options, - stripNativeLibs: Boolean, + skipUnneededSplits: Boolean, logger: Logger, onPatchCompleted: suspend () -> Unit, onProgress: ProgressEventHandler, + onMergedApkReady: (suspend (File) -> Unit)?, ) = coroutineScope { // Get the location of our own Apk. val managerBaseApk = pm.getPackageInfo(context.packageName)!!.applicationInfo!!.sourceDir @@ -195,6 +186,14 @@ class ProcessRuntime( val appProcessBin = resolveAppProcessBin(context) + // Determine merged APK path before launching the process so it is accessible + // after patching.await() to invoke onMergedApkReady in the coroutineScope. + val mergedInputPath = if (SplitApkPreparer.isSplitArchive(File(inputFile))) { + File(cacheDir).resolve("merged-process-input-${System.currentTimeMillis()}.apk").absolutePath + } else { + null + } + launch(Dispatchers.IO) { val result = process( appProcessBin, @@ -250,7 +249,6 @@ class ProcessRuntime( } val parameters = Parameters( - aaptPath = aaptPath, frameworkDir = frameworkPath, cacheDir = cacheDir, packageName = packageName, @@ -263,14 +261,27 @@ class ProcessRuntime( options[uid].orEmpty() ) }, - stripNativeLibs = stripNativeLibs + skipUnneededSplits = skipUnneededSplits, + mergedInputFile = mergedInputPath, + bytecodeMode = prefs.bytecodeModePreference.get(), ) binder.start(parameters, eventHandler) } - // Wait until patching finishes. - patching.await() + // Wait until patching finishes + val mergedFile = mergedInputPath?.let { File(it) } + try { + patching.await() + // If PatcherProcess merged a split archive, notify the caller so the merged APK + // can be saved to originalApksDir for future repatching + if (mergedFile?.exists() == true) { + onMergedApkReady?.invoke(mergedFile) + } + } finally { + // Always clean up the temporary merged file regardless of success or failure + mergedFile?.takeIf { it.exists() }?.delete() + } } companion object : LibraryResolver() { diff --git a/app/src/main/java/app/morphe/manager/patcher/runtime/Runtime.kt b/app/src/main/java/app/morphe/manager/patcher/runtime/Runtime.kt index f29b2e785..203dd22f7 100644 --- a/app/src/main/java/app/morphe/manager/patcher/runtime/Runtime.kt +++ b/app/src/main/java/app/morphe/manager/patcher/runtime/Runtime.kt @@ -4,7 +4,6 @@ import android.content.Context import app.morphe.manager.data.platform.Filesystem import app.morphe.manager.domain.manager.PreferencesManager import app.morphe.manager.domain.repository.PatchBundleRepository -import app.morphe.manager.patcher.aapt.Aapt import app.morphe.manager.patcher.logger.Logger import app.morphe.manager.patcher.worker.ProgressEventHandler import app.morphe.manager.util.Options @@ -12,7 +11,7 @@ import app.morphe.manager.util.PatchSelection import kotlinx.coroutines.flow.first import org.koin.core.component.KoinComponent import org.koin.core.component.inject -import java.io.FileNotFoundException +import java.io.File sealed class Runtime(context: Context) : KoinComponent { private val fs: Filesystem by inject() @@ -20,8 +19,6 @@ sealed class Runtime(context: Context) : KoinComponent { protected val prefs: PreferencesManager by inject() protected val cacheDir: String = fs.tempDir.absolutePath - protected val aaptPath = Aapt.binary(context)?.absolutePath - ?: throw FileNotFoundException("Could not resolve aapt.") protected val frameworkPath: String = context.cacheDir.resolve("framework").also { it.mkdirs() }.absolutePath @@ -36,6 +33,7 @@ sealed class Runtime(context: Context) : KoinComponent { logger: Logger, onPatchCompleted: suspend () -> Unit, onProgress: ProgressEventHandler, - stripNativeLibs: Boolean, + skipUnneededSplits: Boolean, + onMergedApkReady: (suspend (File) -> Unit)? = null, ) } diff --git a/app/src/main/java/app/morphe/manager/patcher/runtime/process/Parameters.kt b/app/src/main/java/app/morphe/manager/patcher/runtime/process/Parameters.kt index 595b7a18b..0ea9eb242 100644 --- a/app/src/main/java/app/morphe/manager/patcher/runtime/process/Parameters.kt +++ b/app/src/main/java/app/morphe/manager/patcher/runtime/process/Parameters.kt @@ -2,19 +2,23 @@ package app.morphe.manager.patcher.runtime.process import android.os.Parcelable import app.morphe.manager.patcher.patch.PatchBundle +import app.morphe.patcher.dex.BytecodeMode import kotlinx.parcelize.Parcelize import kotlinx.parcelize.RawValue @Parcelize data class Parameters( val cacheDir: String, - val aaptPath: String, val frameworkDir: String, val packageName: String, val inputFile: String, val outputFile: String, val configurations: List, - val stripNativeLibs: Boolean, + val skipUnneededSplits: Boolean = false, + // If non-null, PatcherProcess writes the merged mono-APK to this path after prepareIfNeeded. + // ProcessRuntime reads it back so the main process knows the merged file location. + val mergedInputFile: String? = null, + val bytecodeMode: BytecodeMode, ) : Parcelable @Parcelize diff --git a/app/src/main/java/app/morphe/manager/patcher/runtime/process/PatcherProcess.kt b/app/src/main/java/app/morphe/manager/patcher/runtime/process/PatcherProcess.kt index fb65bd589..318b6c649 100644 --- a/app/src/main/java/app/morphe/manager/patcher/runtime/process/PatcherProcess.kt +++ b/app/src/main/java/app/morphe/manager/patcher/runtime/process/PatcherProcess.kt @@ -81,21 +81,29 @@ class PatcherProcess(private val context: Context) : IPatcherProcess.Stub() { events.progress(null, State.COMPLETED.name, null) // Loading patches val preparation = SplitApkPreparer.prepareIfNeeded( - File(parameters.inputFile), - File(parameters.cacheDir), - logger, - parameters.stripNativeLibs, - onProgress = { message -> logger.info(message) } + source = File(parameters.inputFile), + workspace = File(parameters.cacheDir), + logger = logger, + skipUnneededSplits = parameters.skipUnneededSplits, + onProgress = { message -> + logger.info(message) + events.progress(message, State.RUNNING.name, null) + } ) try { if (preparation.merged) { events.progress(null, State.COMPLETED.name, null) + + // Copy merged APK to the agreed path so ProcessRuntime can read it back + // in the main process after this process finishes + parameters.mergedInputFile?.let { dest -> + preparation.file.copyTo(File(dest), overwrite = true) + } } Session( cacheDir = parameters.cacheDir, - aaptPath = parameters.aaptPath, frameworkDir = parameters.frameworkDir, androidContext = context, logger = logger, @@ -103,16 +111,18 @@ class PatcherProcess(private val context: Context) : IPatcherProcess.Stub() { onPatchCompleted = { events.patchSucceeded() }, onProgress = { name, state, message -> events.progress(name, state?.name, message) - } + }, + bytecodeMode = parameters.bytecodeMode, ).use { it.run(File(parameters.outputFile), patchList) } + MemoryMonitor.stopMemoryPolling(logger) events.finished(null) } catch (e: Exception) { + MemoryMonitor.stopMemoryPolling(logger) events.finished(e.stackTraceToString()) } finally { preparation.cleanup() - MemoryMonitor.stopMemoryPolling(logger) } } } diff --git a/app/src/main/java/app/morphe/manager/patcher/split/Merger.kt b/app/src/main/java/app/morphe/manager/patcher/split/Merger.kt index 5c5cc51c2..9daee59da 100644 --- a/app/src/main/java/app/morphe/manager/patcher/split/Merger.kt +++ b/app/src/main/java/app/morphe/manager/patcher/split/Merger.kt @@ -20,7 +20,7 @@ import java.nio.charset.CoderMalfunctionError import java.nio.file.Path import java.util.Locale -const val TAG = "APKEditor" +const val TAG = "Morphe APKEditor" private class ApkEditorLogger( private val onProgress: ((String) -> Unit)? = null diff --git a/app/src/main/java/app/morphe/manager/patcher/split/SplitApkPreparer.kt b/app/src/main/java/app/morphe/manager/patcher/split/SplitApkPreparer.kt index dbf5eecbd..b38f570e4 100644 --- a/app/src/main/java/app/morphe/manager/patcher/split/SplitApkPreparer.kt +++ b/app/src/main/java/app/morphe/manager/patcher/split/SplitApkPreparer.kt @@ -6,7 +6,6 @@ import android.util.DisplayMetrics import android.util.Log import app.morphe.manager.patcher.logger.LogLevel import app.morphe.manager.patcher.logger.Logger -import app.morphe.manager.patcher.util.NativeLibStripper import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.runInterruptible import java.io.File @@ -15,13 +14,25 @@ import java.nio.file.Files import java.util.Locale import java.util.zip.ZipFile +/** + * Prepares split APK bundles (APKS/APKM/XAPK and plain ZIPs with embedded APKs) for patching + * by extracting and merging all constituent modules into a single monolithic APK. + */ object SplitApkPreparer { + // Recognized split archive container extensions private val SUPPORTED_EXTENSIONS = setOf("apks", "apkm", "xapk") private const val SKIPPED_STEP_PREFIX = "[skipped]" + + // All known ABI identifiers as they appear in split module names, pre-computed once private val KNOWN_ABIS = setOf("armeabi", "armeabi-v7a", "arm64-v8a", "x86", "x86_64") + private val KNOWN_ABI_TOKENS = KNOWN_ABIS.flatMap { abi -> + val normalized = abi.lowercase(Locale.ROOT) + setOf(normalized, normalized.replace('-', '_'), normalized.replace('_', '-')) + }.toSet() private val DENSITY_QUALIFIERS = setOf("ldpi", "mdpi", "tvdpi", "hdpi", "xhdpi", "xxhdpi", "xxxhdpi") + /** Returns `true` if [file] is a split APK bundle. */ fun isSplitArchive(file: File?): Boolean { if (file == null || !file.exists()) return false val extension = file.extension.lowercase(Locale.ROOT) @@ -29,11 +40,18 @@ object SplitApkPreparer { return hasEmbeddedApkEntries(file) } + /** + * If [source] is a split APK bundle, extracts all modules into [workspace], merges them into + * a single APK and returns a [PreparationResult] pointing to the merged file. + * If [source] is already a plain APK, returns a [PreparationResult] wrapping it unchanged. + * + * The caller is responsible for invoking [PreparationResult.cleanup] when the result is no + * longer needed to remove temporary files. + */ suspend fun prepareIfNeeded( source: File, workspace: File, logger: Logger = DefaultLogger, - stripNativeLibs: Boolean = false, skipUnneededSplits: Boolean = false, onProgress: ((String) -> Unit)? = null, onSubSteps: ((List) -> Unit)? = null, @@ -57,10 +75,8 @@ object SplitApkPreparer { val mergeOrder = Merger.listMergeOrder(modulesDir.toPath()) val supportedTokens = supportedAbiTokens() val skippedModules = buildSet { - if (stripNativeLibs) { - addAll(mergeOrder.filter { shouldSkipModule(it, supportedTokens) }) - } if (skipUnneededSplits) { + addAll(mergeOrder.filter { shouldSkipModule(it, supportedTokens) }) val localeTokens = deviceLocaleTokens() val densityQualifier = deviceDensityQualifier() addAll( @@ -74,7 +90,7 @@ object SplitApkPreparer { ) } } - onSubSteps?.invoke(buildSplitSubSteps(mergeOrder, skippedModules, stripNativeLibs)) + onSubSteps?.invoke(buildSplitSubSteps(mergeOrder, skippedModules)) Merger.merge( apkDir = modulesDir.toPath(), @@ -84,13 +100,7 @@ object SplitApkPreparer { sortApkEntries = sortMergedApkEntries ) - if (stripNativeLibs) { - onProgress?.invoke("Stripping native libraries") - NativeLibStripper.strip(mergedApk) - } - onProgress?.invoke("Finalizing merged APK") - persistMergedIfDownloaded(source, mergedApk, logger) logger.info( "Split APK merged to ${mergedApk.absolutePath} " + @@ -108,6 +118,7 @@ object SplitApkPreparer { } } + // Returns true if the ZIP file at [file] contains at least one embedded .apk entry private fun hasEmbeddedApkEntries(file: File): Boolean = runCatching { ZipFile(file).use { zip -> @@ -121,8 +132,7 @@ object SplitApkPreparer { private fun buildSplitSubSteps( mergeOrder: List, - skippedModules: Set, - stripNativeLibs: Boolean + skippedModules: Set ): List { val steps = mutableListOf() steps.add("Extracting split APKs") @@ -132,35 +142,22 @@ object SplitApkPreparer { val (skipped, remaining) = mergeOrder.partition { skippedLookup.contains(it.lowercase(Locale.ROOT)) } - (skipped + remaining).forEach { name -> - val label = "Merging $name" - val entry = if (skippedLookup.contains(name.lowercase(Locale.ROOT))) { - "$SKIPPED_STEP_PREFIX$label" - } else { - label - } - steps.add(entry) - } + skipped.forEach { steps.add("$SKIPPED_STEP_PREFIX Merging $it") } + remaining.forEach { steps.add("Merging $it") } steps.add("Writing merged APK") - if (stripNativeLibs) { - steps.add("Stripping native libraries") - } steps.add("Finalizing merged APK") return steps } + // Returns the set of name tokens for the device's primary ABI (the first entry in + // Build.SUPPORTED_ABIS, which is always the most preferred one). + // Tokens cover both dash and underscore forms so they match any split module naming variant private fun supportedAbiTokens(): Set = - selectPrimaryAbi(Build.SUPPORTED_ABIS.toList()) - ?.let { primary -> - buildAbiTokens(primary) - .map { it.lowercase(Locale.ROOT) } - .toSet() - } - ?: Build.SUPPORTED_ABIS - .flatMap { abi -> buildAbiTokens(abi) } - .map { it.lowercase(Locale.ROOT) } - .toSet() + buildAbiTokens(Build.SUPPORTED_ABIS.first()) + .map { it.lowercase(Locale.ROOT) } + .toSet() + // Produces all name variants for a single ABI string: normalized, dash form, underscore form. private fun buildAbiTokens(abi: String): Set { val normalized = abi.lowercase(Locale.ROOT) return setOf( @@ -170,23 +167,24 @@ object SplitApkPreparer { ) } - private fun selectPrimaryAbi(supportedAbis: List): String? = - supportedAbis.firstOrNull { it.isNotBlank() } - + // Returns true if [moduleName] is a native-library split for an ABI that is NOT in [supportedTokens]. + // Modules that don't look like ABI splits at all are kept (return false) private fun shouldSkipModule( moduleName: String, supportedTokens: Set ): Boolean { val lower = moduleName.lowercase(Locale.ROOT) - val knownTokens = KNOWN_ABIS.flatMap { buildAbiTokens(it) }.toSet() - if (knownTokens.none { lower.contains(it) }) return false + if (KNOWN_ABI_TOKENS.none { lower.contains(it) }) return false return supportedTokens.none { lower.contains(it) } } + // Returns true if [moduleName] is a locale or density config split that does not match the + // current device. ABI splits are intentionally excluded here - they are handled separately + // by [shouldSkipModule] private fun shouldSkipModuleForDevice( moduleName: String, localeTokens: Set, - densityQualifier: String? + densityQualifier: String ): Boolean { val qualifiers = splitConfigQualifiers(moduleName) if (qualifiers.isEmpty()) return false @@ -194,8 +192,7 @@ object SplitApkPreparer { for (qualifier in qualifiers) { if (isDensityQualifier(qualifier)) { - val deviceDensity = densityQualifier ?: continue - if (qualifier != deviceDensity) return true + if (qualifier != densityQualifier) return true continue } val localeQualifier = parseLocaleQualifier(qualifier) ?: continue @@ -208,10 +205,10 @@ object SplitApkPreparer { private fun isAbiSplit(moduleName: String): Boolean { val lower = moduleName.lowercase(Locale.ROOT) - val knownTokens = KNOWN_ABIS.flatMap { buildAbiTokens(it) }.toSet() - return knownTokens.any { lower.contains(it) } + return KNOWN_ABI_TOKENS.any { lower.contains(it) } } + // Extracts the qualifier tokens from a config split module name private fun splitConfigQualifiers(moduleName: String): List { val normalized = moduleName.lowercase(Locale.ROOT).removeSuffix(".apk") val splitIndex = normalized.indexOf("split_config.") @@ -256,6 +253,9 @@ object SplitApkPreparer { } } + // Returns all locale tokens for the system's active locale list, covering language, + // language+region, and language+script variants. Uses Resources.getSystem() to read the + // actual system locale regardless of any in-app language override private fun deviceLocaleTokens(): Set { val list = Resources.getSystem().configuration.locales val locales = (0 until list.size()).map { index -> list[index] } @@ -283,8 +283,10 @@ object SplitApkPreparer { return tokens } - private fun deviceDensityQualifier(): String? { - val density = Resources.getSystem().displayMetrics?.densityDpi ?: return null + // Maps the system's screen density to the closest standard density qualifier string. + // Uses Resources.getSystem() to avoid being affected by any in-app configuration override + private fun deviceDensityQualifier(): String { + val density = Resources.getSystem().displayMetrics.densityDpi return when { density <= DisplayMetrics.DENSITY_LOW -> "ldpi" density <= DisplayMetrics.DENSITY_MEDIUM -> "mdpi" @@ -329,26 +331,13 @@ object SplitApkPreparer { extracted } + /** Result of [prepareIfNeeded]. */ data class PreparationResult( val file: File, val merged: Boolean, val cleanup: () -> Unit = {} ) - private fun persistMergedIfDownloaded(source: File, merged: File, logger: Logger) { - // Only persist back to the downloads cache when the original input lives in our downloaded-apps dir. - val downloadsRoot = source.parentFile?.parentFile - val isDownloadedApp = downloadsRoot?.name?.startsWith("app_downloaded-apps") == true - if (!isDownloadedApp) return - - runCatching { - merged.copyTo(source, overwrite = true) - logger.info("Persisted merged split APK back to downloads cache: ${source.absolutePath}") - }.onFailure { error -> - logger.warn("Failed to persist merged split APK to downloads cache: ${error.message}") - } - } - private object DefaultLogger : Logger() { override fun log(level: LogLevel, message: String) { Log.d("SplitApkPreparer", "[${level.name}] $message") diff --git a/app/src/main/java/app/morphe/manager/patcher/util/NativeLibStripper.kt b/app/src/main/java/app/morphe/manager/patcher/util/NativeLibStripper.kt index b54c986c6..51218150b 100644 --- a/app/src/main/java/app/morphe/manager/patcher/util/NativeLibStripper.kt +++ b/app/src/main/java/app/morphe/manager/patcher/util/NativeLibStripper.kt @@ -2,21 +2,22 @@ package app.morphe.manager.patcher.util import android.os.Build import android.util.Log +import app.morphe.manager.patcher.logger.Logger +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext import java.io.File import java.util.zip.ZipEntry import java.util.zip.ZipFile import java.util.zip.ZipInputStream import java.util.zip.ZipOutputStream -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.withContext object NativeLibStripper { - private const val TAG = "NativeLibStripper" + private const val TAG = "Morphe NativeLibStripper" - suspend fun strip(apkFile: File): Boolean = - strip(apkFile, Build.SUPPORTED_ABIS.filter { it.isNotBlank() }) + suspend fun strip(apkFile: File, logger: Logger? = null): Boolean = + strip(apkFile, Build.SUPPORTED_ABIS.filter { it.isNotBlank() }, logger) - suspend fun strip(apkFile: File, supportedAbis: List): Boolean = + suspend fun strip(apkFile: File, supportedAbis: List, logger: Logger? = null): Boolean = withContext(Dispatchers.IO) { if (supportedAbis.isEmpty()) return@withContext false @@ -65,7 +66,9 @@ object NativeLibStripper { } tempFile.copyTo(apkFile, overwrite = true) tempFile.delete() - Log.i(TAG, "Removed $removedEntries native library entries for unsupported ABIs") + val message = "Stripped native libraries for unsupported ABIs (removed $removedEntries entries)" + Log.i(TAG, message) + logger?.info(message) true } else { tempFile.delete() diff --git a/app/src/main/java/app/morphe/manager/patcher/worker/PatcherWorker.kt b/app/src/main/java/app/morphe/manager/patcher/worker/PatcherWorker.kt index a18167ae2..d7285438d 100644 --- a/app/src/main/java/app/morphe/manager/patcher/worker/PatcherWorker.kt +++ b/app/src/main/java/app/morphe/manager/patcher/worker/PatcherWorker.kt @@ -20,6 +20,7 @@ import app.morphe.manager.domain.installer.RootInstaller import app.morphe.manager.domain.manager.KeystoreManager import app.morphe.manager.domain.manager.PreferencesManager import app.morphe.manager.domain.repository.InstalledAppRepository +import app.morphe.manager.domain.repository.OriginalApkRepository import app.morphe.manager.domain.worker.Worker import app.morphe.manager.domain.worker.WorkerRepository import app.morphe.manager.patcher.logger.Logger @@ -47,6 +48,7 @@ class PatcherWorker( private val pm: PM by inject() private val fs: Filesystem by inject() private val installedAppRepository: InstalledAppRepository by inject() + private val originalApkRepository: OriginalApkRepository by inject() private val rootInstaller: RootInstaller by inject() class Args( @@ -65,12 +67,15 @@ class PatcherWorker( override suspend fun getForegroundInfo() = ForegroundInfo( - 1, + NOTIFICATION_ID, createNotification(), if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) ServiceInfo.FOREGROUND_SERVICE_TYPE_SPECIAL_USE else 0 ) - private fun createNotification(): Notification { + private fun createNotification( + stepName: String? = null, + patchProgress: Pair? = null, // completed to total patches + ): Notification { val notificationIntent = Intent(applicationContext, MainActivity::class.java).apply { flags = Intent.FLAG_ACTIVITY_SINGLE_TOP or Intent.FLAG_ACTIVITY_CLEAR_TOP } @@ -88,14 +93,36 @@ class PatcherWorker( applicationContext.getSystemService(NotificationManager::class.java) notificationManager.createNotificationChannel(channel) return Notification.Builder(applicationContext, channel.id) - .setContentTitle(applicationContext.getText(R.string.patcher_notification_title)) + .setContentTitle( + stepName ?: applicationContext.getString(R.string.patcher_notification_title) + ) .setContentText(applicationContext.getText(R.string.patcher_notification_text)) + .apply { + if (patchProgress != null) { + val (completed, total) = patchProgress + setSubText("$completed / $total") + setProgress(total, completed, false) + } + } .setSmallIcon(Icon.createWithResource(applicationContext, R.drawable.ic_notification)) .setContentIntent(pendingIntent) .setCategory(Notification.CATEGORY_SERVICE) + .setOngoing(true) .build() } + private fun updatePatcherNotification( + stepName: String?, + patchProgress: Pair? = null, + ) { + val notificationManager = + applicationContext.getSystemService(NotificationManager::class.java) + // Android won't visually switch from indeterminate → determinate on the same notification + // ID unless we first post a brief non-indeterminate update. Post the real notification + // directly — the determinate bar replaces the spinning one cleanly this way + notificationManager.notify(NOTIFICATION_ID, createNotification(stepName, patchProgress)) + } + override suspend fun doWork(): Result { if (runAttemptCount > 0) { Log.d(tag, "Android requested retrying but retrying is disabled.".logFmt()) @@ -137,8 +164,32 @@ class PatcherWorker( private suspend fun runPatcher(args: Args): Result { - fun updateProgress(name: String? = null, state: State? = null, message: String? = null) = + val totalPatches = args.selectedPatches.values.sumOf { it.size } + var completedPatches = 0 + // Cached so onPatchCompleted can update the title without a string lookup race + val applyingPatchesLabel = applicationContext.getString(R.string.applying_patches) + + fun updateProgress(name: String? = null, state: State? = null, message: String? = null) { + if (name != null && state == State.RUNNING) { + // When entering the patch execution phase start with 0/N + val progress = if (totalPatches > 0 && name == applyingPatchesLabel) + completedPatches to totalPatches + else + null + updatePatcherNotification(stepName = name, patchProgress = progress) + } args.onProgress(name, state, message) + } + + val onPatchCompleted: suspend () -> Unit = { + completedPatches++ + // Update both title and progress bar together on every completed patch + updatePatcherNotification( + stepName = applyingPatchesLabel, + patchProgress = completedPatches to totalPatches + ) + args.onPatchCompleted() + } val patchedApk = fs.tempDir.resolve("patched.apk") @@ -215,6 +266,21 @@ class PatcherWorker( CoroutineRuntime(applicationContext) } + // After merging a split archive (in either runtime), save the resulting mono-APK + // directly to originalApksDir so it is used for repatching instead of the archive + val onMergedApkReady: suspend (File) -> Unit = { mergedFile -> + val version = pm.getPackageInfo(mergedFile)?.versionName + ?.takeUnless { it.isBlank() } + ?: args.input.version + ?: "unknown" + val savedFile = originalApkRepository.saveOriginalApk( + packageName = args.packageName, + version = version, + sourceFile = mergedFile + ) + args.setInputFile(savedFile ?: mergedFile, true, true) + } + try { runtime.execute( inputFile.absolutePath, @@ -223,9 +289,10 @@ class PatcherWorker( args.selectedPatches, args.options, args.logger, - args.onPatchCompleted, - args.onProgress, - stripNativeLibs + onPatchCompleted, + ::updateProgress, + stripNativeLibs, + onMergedApkReady ) } catch (e: Exception) { if (!useProcessRuntime || Build.VERSION.SDK_INT > Build.VERSION_CODES.Q || !isOomRelated(e)) { @@ -241,14 +308,15 @@ class PatcherWorker( args.selectedPatches, args.options, args.logger, - args.onPatchCompleted, - args.onProgress, - stripNativeLibs + onPatchCompleted, + ::updateProgress, + stripNativeLibs, + onMergedApkReady ) } if (stripNativeLibs && !inputIsSplitArchive) { - NativeLibStripper.strip(patchedApk) + NativeLibStripper.strip(patchedApk, args.logger) } keystoreManager.sign(patchedApk, File(args.output)) @@ -317,6 +385,8 @@ class PatcherWorker( private const val LOG_PREFIX = "[Worker]" private fun String.logFmt() = "$LOG_PREFIX $this" + const val NOTIFICATION_ID = 1 + const val PROCESS_EXIT_CODE_KEY = "process_exit_code" const val PROCESS_PREVIOUS_LIMIT_KEY = "process_previous_limit" const val PROCESS_FAILURE_MESSAGE_KEY = "process_failure_message" diff --git a/app/src/main/java/app/morphe/manager/receiver/InstallReceiver.kt b/app/src/main/java/app/morphe/manager/receiver/InstallReceiver.kt deleted file mode 100644 index 87f70d118..000000000 --- a/app/src/main/java/app/morphe/manager/receiver/InstallReceiver.kt +++ /dev/null @@ -1,83 +0,0 @@ -package app.morphe.manager.receiver - -import android.content.BroadcastReceiver -import android.content.Context -import android.content.Intent -import android.content.pm.PackageInstaller -import android.os.Build -import android.provider.Settings -import android.util.Log -import androidx.core.net.toUri -import app.morphe.manager.service.InstallService - -@Suppress("DEPRECATION") -class InstallReceiver : BroadcastReceiver() { - override fun onReceive(context: Context, intent: Intent) { - val extraStatus = intent.getIntExtra(PackageInstaller.EXTRA_STATUS, -999) - val extraStatusMessage = intent.getStringExtra(PackageInstaller.EXTRA_STATUS_MESSAGE) - val extraPackageName = intent.getStringExtra(PackageInstaller.EXTRA_PACKAGE_NAME) - Log.d("InstallReceiver", "onReceive(status=$extraStatus, pkg=$extraPackageName, msg=${extraStatusMessage?.take(120)})") - - when (extraStatus) { - PackageInstaller.STATUS_PENDING_USER_ACTION -> { - val userActionIntent = if (Build.VERSION.SDK_INT >= 33) { - intent.getParcelableExtra(Intent.EXTRA_INTENT, Intent::class.java) - } else { - intent.getParcelableExtra(Intent.EXTRA_INTENT) as? Intent - } - - if (!tryStartUserAction(context, userActionIntent)) { - val fallback = Intent( - Settings.ACTION_MANAGE_UNKNOWN_APP_SOURCES, - "package:${context.packageName}".toUri() - ).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) - - if (!tryStartUserAction(context, fallback)) { - sendResultBroadcast( - context = context, - status = PackageInstaller.STATUS_FAILURE_BLOCKED, - statusMessage = extraStatusMessage ?: "Unable to launch installer confirmation.", - packageName = extraPackageName - ) - } - } - } - - else -> { - sendResultBroadcast( - context = context, - status = extraStatus, - statusMessage = extraStatusMessage, - packageName = extraPackageName - ) - } - } - } - - private fun sendResultBroadcast( - context: Context, - status: Int, - statusMessage: String?, - packageName: String? - ) { - context.sendBroadcast( - Intent().apply { - action = InstallService.APP_INSTALL_ACTION - setPackage(context.packageName) - putExtra(InstallService.EXTRA_INSTALL_STATUS, status) - putExtra(InstallService.EXTRA_INSTALL_STATUS_MESSAGE, statusMessage) - putExtra(InstallService.EXTRA_PACKAGE_NAME, packageName) - } - ) - } - - private fun tryStartUserAction(context: Context, action: Intent?): Boolean { - if (action == null) return false - return runCatching { - action.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) - context.startActivity(action) - }.onFailure { - Log.w("InstallReceiver", "Failed to start installer user action", it) - }.isSuccess - } -} diff --git a/app/src/main/java/app/morphe/manager/receiver/UninstallReceiver.kt b/app/src/main/java/app/morphe/manager/receiver/UninstallReceiver.kt deleted file mode 100644 index 2814e288c..000000000 --- a/app/src/main/java/app/morphe/manager/receiver/UninstallReceiver.kt +++ /dev/null @@ -1,83 +0,0 @@ -package app.morphe.manager.receiver - -import android.content.BroadcastReceiver -import android.content.Context -import android.content.Intent -import android.content.pm.PackageInstaller -import android.os.Build -import android.provider.Settings -import android.util.Log -import androidx.core.net.toUri -import app.morphe.manager.service.UninstallService - -@Suppress("DEPRECATION") -class UninstallReceiver : BroadcastReceiver() { - override fun onReceive(context: Context, intent: Intent) { - val extraStatus = intent.getIntExtra(PackageInstaller.EXTRA_STATUS, -999) - val extraStatusMessage = intent.getStringExtra(PackageInstaller.EXTRA_STATUS_MESSAGE) - val targetPackage = intent.getStringExtra(PackageInstaller.EXTRA_PACKAGE_NAME) - Log.d("UninstallReceiver", "onReceive(status=$extraStatus, pkg=$targetPackage, msg=${extraStatusMessage?.take(120)})") - - when (extraStatus) { - PackageInstaller.STATUS_PENDING_USER_ACTION -> { - val userActionIntent = if (Build.VERSION.SDK_INT >= 33) { - intent.getParcelableExtra(Intent.EXTRA_INTENT, Intent::class.java) - } else { - intent.getParcelableExtra(Intent.EXTRA_INTENT) as? Intent - } - - if (!tryStartUserAction(context, userActionIntent)) { - val fallback = Intent( - Settings.ACTION_APPLICATION_DETAILS_SETTINGS, - "package:${targetPackage.orEmpty()}".toUri() - ).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) - - if (!tryStartUserAction(context, fallback)) { - sendResultBroadcast( - context = context, - status = PackageInstaller.STATUS_FAILURE_BLOCKED, - statusMessage = extraStatusMessage ?: "Unable to launch uninstall confirmation.", - targetPackage = targetPackage - ) - } - } - } - - else -> { - sendResultBroadcast( - context = context, - status = extraStatus, - statusMessage = extraStatusMessage, - targetPackage = targetPackage - ) - } - } - } - - private fun sendResultBroadcast( - context: Context, - status: Int, - statusMessage: String?, - targetPackage: String? - ) { - context.sendBroadcast( - Intent().apply { - action = UninstallService.APP_UNINSTALL_ACTION - setPackage(context.packageName) - putExtra(UninstallService.EXTRA_UNINSTALL_PACKAGE_NAME, targetPackage) - putExtra(UninstallService.EXTRA_UNINSTALL_STATUS, status) - putExtra(UninstallService.EXTRA_UNINSTALL_STATUS_MESSAGE, statusMessage) - } - ) - } - - private fun tryStartUserAction(context: Context, action: Intent?): Boolean { - if (action == null) return false - return runCatching { - action.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) - context.startActivity(action) - }.onFailure { - Log.w("UninstallReceiver", "Failed to start uninstall user action", it) - }.isSuccess - } -} diff --git a/app/src/main/java/app/morphe/manager/service/InstallService.kt b/app/src/main/java/app/morphe/manager/service/InstallService.kt deleted file mode 100644 index 56ab7eeff..000000000 --- a/app/src/main/java/app/morphe/manager/service/InstallService.kt +++ /dev/null @@ -1,84 +0,0 @@ -package app.morphe.manager.service - -import android.app.Service -import android.content.Intent -import android.content.pm.PackageInstaller -import android.os.Build -import android.os.IBinder -import android.provider.Settings -import android.util.Log -import androidx.core.net.toUri - -@Suppress("DEPRECATION") -class InstallService : Service() { - - override fun onStartCommand( - intent: Intent, flags: Int, startId: Int - ): Int { - val extraStatus = intent.getIntExtra(PackageInstaller.EXTRA_STATUS, -999) - val extraStatusMessage = intent.getStringExtra(PackageInstaller.EXTRA_STATUS_MESSAGE) - val extraPackageName = intent.getStringExtra(PackageInstaller.EXTRA_PACKAGE_NAME) - when (extraStatus) { - PackageInstaller.STATUS_PENDING_USER_ACTION -> { - val userActionIntent = if (Build.VERSION.SDK_INT >= 33) { - intent.getParcelableExtra(Intent.EXTRA_INTENT, Intent::class.java) - } else { - intent.getParcelableExtra(Intent.EXTRA_INTENT) as? Intent - } - - if (!tryStartUserAction(userActionIntent)) { - val fallback = Intent( - Settings.ACTION_MANAGE_UNKNOWN_APP_SOURCES, - "package:$packageName".toUri() - ).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) - - if (!tryStartUserAction(fallback)) { - sendBroadcast(Intent().apply { - action = APP_INSTALL_ACTION - `package` = packageName - putExtra(EXTRA_INSTALL_STATUS, PackageInstaller.STATUS_FAILURE_BLOCKED) - putExtra( - EXTRA_INSTALL_STATUS_MESSAGE, - extraStatusMessage ?: "Unable to launch installer confirmation." - ) - putExtra(EXTRA_PACKAGE_NAME, extraPackageName) - }) - } - } - } - - else -> { - sendBroadcast(Intent().apply { - action = APP_INSTALL_ACTION - `package` = packageName - putExtra(EXTRA_INSTALL_STATUS, extraStatus) - putExtra(EXTRA_INSTALL_STATUS_MESSAGE, extraStatusMessage) - putExtra(EXTRA_PACKAGE_NAME, extraPackageName) - }) - } - } - stopSelf() - return START_NOT_STICKY - } - - override fun onBind(intent: Intent?): IBinder? = null - - private fun tryStartUserAction(action: Intent?): Boolean { - if (action == null) return false - return runCatching { - action.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) - startActivity(action) - }.onFailure { - Log.w("InstallService", "Failed to start installer user action", it) - }.isSuccess - } - - companion object { - const val APP_INSTALL_ACTION = "APP_INSTALL_ACTION" - - const val EXTRA_INSTALL_STATUS = "EXTRA_INSTALL_STATUS" - const val EXTRA_INSTALL_STATUS_MESSAGE = "EXTRA_INSTALL_STATUS_MESSAGE" - const val EXTRA_PACKAGE_NAME = "EXTRA_PACKAGE_NAME" - } - -} diff --git a/app/src/main/java/app/morphe/manager/service/UninstallService.kt b/app/src/main/java/app/morphe/manager/service/UninstallService.kt deleted file mode 100644 index 33174e69b..000000000 --- a/app/src/main/java/app/morphe/manager/service/UninstallService.kt +++ /dev/null @@ -1,89 +0,0 @@ -package app.morphe.manager.service - -import android.app.Service -import android.content.Intent -import android.content.pm.PackageInstaller -import android.os.Build -import android.os.IBinder -import android.provider.Settings -import android.util.Log -import androidx.core.net.toUri - -@Suppress("DEPRECATION") -class UninstallService : Service() { - - override fun onStartCommand( - intent: Intent, - flags: Int, - startId: Int - ): Int { - val extraStatus = intent.getIntExtra(PackageInstaller.EXTRA_STATUS, -999) - val extraStatusMessage = intent.getStringExtra(PackageInstaller.EXTRA_STATUS_MESSAGE) - - val targetPackage = intent.getStringExtra(PackageInstaller.EXTRA_PACKAGE_NAME) - - when (extraStatus) { - PackageInstaller.STATUS_PENDING_USER_ACTION -> { - val userActionIntent = if (Build.VERSION.SDK_INT >= 33) { - intent.getParcelableExtra(Intent.EXTRA_INTENT, Intent::class.java) - } else { - intent.getParcelableExtra(Intent.EXTRA_INTENT) as? Intent - } - - if (!tryStartUserAction(userActionIntent)) { - val fallbackTarget = targetPackage.orEmpty() - val fallback = Intent( - Settings.ACTION_APPLICATION_DETAILS_SETTINGS, - "package:$fallbackTarget".toUri() - ).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) - - if (!tryStartUserAction(fallback)) { - sendBroadcast(Intent().apply { - action = APP_UNINSTALL_ACTION - `package` = packageName - putExtra(EXTRA_UNINSTALL_PACKAGE_NAME, targetPackage) - putExtra(EXTRA_UNINSTALL_STATUS, PackageInstaller.STATUS_FAILURE_BLOCKED) - putExtra( - EXTRA_UNINSTALL_STATUS_MESSAGE, - extraStatusMessage ?: "Unable to launch uninstall confirmation." - ) - }) - } - } - } - - else -> { - sendBroadcast(Intent().apply { - action = APP_UNINSTALL_ACTION - `package` = packageName - putExtra(EXTRA_UNINSTALL_PACKAGE_NAME, targetPackage) - putExtra(EXTRA_UNINSTALL_STATUS, extraStatus) - putExtra(EXTRA_UNINSTALL_STATUS_MESSAGE, extraStatusMessage) - }) - } - } - stopSelf() - return START_NOT_STICKY - } - - override fun onBind(intent: Intent?): IBinder? = null - - private fun tryStartUserAction(action: Intent?): Boolean { - if (action == null) return false - return runCatching { - action.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) - startActivity(action) - }.onFailure { - Log.w("UninstallService", "Failed to start uninstall user action", it) - }.isSuccess - } - - companion object { - const val APP_UNINSTALL_ACTION = "APP_UNINSTALL_ACTION" - - const val EXTRA_UNINSTALL_STATUS = "EXTRA_UNINSTALL_STATUS" - const val EXTRA_UNINSTALL_STATUS_MESSAGE = "EXTRA_INSTALL_STATUS_MESSAGE" - const val EXTRA_UNINSTALL_PACKAGE_NAME = "EXTRA_UNINSTALL_PACKAGE_NAME" - } - -} diff --git a/app/src/main/java/app/morphe/manager/ui/model/PatcherStep.kt b/app/src/main/java/app/morphe/manager/ui/model/PatcherStep.kt index 7b9577461..aa436813b 100644 --- a/app/src/main/java/app/morphe/manager/ui/model/PatcherStep.kt +++ b/app/src/main/java/app/morphe/manager/ui/model/PatcherStep.kt @@ -24,10 +24,6 @@ enum class State { WAITING, RUNNING, FAILED, COMPLETED } -enum class ProgressKey { - DOWNLOAD -} - interface StepProgressProvider { val downloadProgress: Pair? } @@ -40,6 +36,5 @@ data class Step( /** [0, 1] Percentage of the total operation */ val progressPercentage : Double, val state: State = State.WAITING, - val message: String? = null, - val progressKey: ProgressKey? = null + val message: String? = null ) : Parcelable diff --git a/app/src/main/java/app/morphe/manager/ui/screen/HomeScreen.kt b/app/src/main/java/app/morphe/manager/ui/screen/HomeScreen.kt index 5ae94a960..84d48803f 100644 --- a/app/src/main/java/app/morphe/manager/ui/screen/HomeScreen.kt +++ b/app/src/main/java/app/morphe/manager/ui/screen/HomeScreen.kt @@ -8,7 +8,6 @@ package app.morphe.manager.ui.screen import android.annotation.SuppressLint import android.view.HapticFeedbackConstants import androidx.activity.compose.rememberLauncherForActivityResult -import androidx.activity.result.contract.ActivityResultContracts import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.statusBarsPadding @@ -18,10 +17,12 @@ import androidx.compose.runtime.* import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalView +import androidx.compose.ui.res.stringResource import androidx.lifecycle.compose.collectAsStateWithLifecycle import app.morphe.manager.R import app.morphe.manager.domain.manager.PreferencesManager import app.morphe.manager.domain.repository.PatchBundleRepository +import app.morphe.manager.ui.model.HomeAppItem import app.morphe.manager.ui.screen.home.* import app.morphe.manager.ui.screen.settings.system.PrePatchInstallerDialog import app.morphe.manager.ui.viewmodel.* @@ -50,33 +51,48 @@ fun HomeScreen( val view = LocalView.current // Dialog states - val showInstalledAppDialog = remember { mutableStateOf(null) } val showUpdateDetailsDialog = remember { mutableStateOf(false) } + // Patches dialog state (swipe-right on app card) + val patchesSheetItem = remember { mutableStateOf(null) } + // Pull to refresh state val isRefreshing by homeViewModel.isRefreshing.collectAsStateWithLifecycle() - // Get greeting message - var greetingMessage by remember { mutableStateOf(context.getString(HomeAndPatcherMessages.getHomeMessage(context))) } + // Reactively observe the preference so the greeting updates immediately + val showGreetingPhrases by prefs.showGreetingPhrases.getAsState() + + // Re-evaluated whenever showPatchingPhrases changes + var greetingMessage by remember(showGreetingPhrases) { + mutableStateOf( + if (showGreetingPhrases) context.getString(HomeAndPatcherMessages.getHomeMessage(context)) else null + ) + } - // Handle refresh with haptic feedback + // Handle refresh with haptic feedback. + // showPatchingPhrases is read from the reactive state captured in the + // outer scope so the lambda always uses the current value at invocation. val onRefresh: () -> Unit = { view.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS) HomeAndPatcherMessages.resetHomeMessage() - greetingMessage = context.getString(HomeAndPatcherMessages.getHomeMessage(context)) + greetingMessage = if (showGreetingPhrases) context.getString(HomeAndPatcherMessages.getHomeMessage(context)) else null homeViewModel.refresh() } // Collect state flows val availablePatches by homeViewModel.availablePatches.collectAsStateWithLifecycle(0) - // Dynamic app items from bundles - val homeAppItems by homeViewModel.homeAppItems.collectAsStateWithLifecycle() - // Hidden packages filtered to only active-bundle packages (reactive) - val hiddenAppItems by homeViewModel.hiddenAppItems.collectAsStateWithLifecycle() + // Atomic home state - null means pipeline is still initializing (shimmer) + val homeAppState by homeViewModel.homeAppState.collectAsStateWithLifecycle() + val homeAppItems = homeAppState?.visible ?: emptyList() + val hiddenAppItems = homeAppState?.hidden ?: emptyList() + val bundlePipelineLoading = homeAppState == null val showOtherAppsButton by homeViewModel.showOtherAppsButton.collectAsStateWithLifecycle() val showSearchButton by homeViewModel.showSearchButton.collectAsStateWithLifecycle() val useExpertMode by prefs.useExpertMode.getAsState() + // Gesture hint: shown once per bundle addition, in-memory + val showGestureHint by homeViewModel.showSwipeGestureHint.collectAsStateWithLifecycle() + val isDeviceRooted = homeViewModel.rootInstaller.isDeviceRooted() if (!isDeviceRooted) { // Non-root: always standard install, sync the state @@ -93,17 +109,14 @@ fun HomeScreen( homeViewModel.onStartQuickPatch = onStartQuickPatch } - // Initialize launchers - // GetContent is used instead of OpenDocument so that third-party file managers - // appear as available options in Android's picker - val openApkPicker = rememberLauncherForActivityResult( - contract = ActivityResultContracts.GetContent() - ) { uri -> - uri?.let { homeViewModel.handleApkSelection(it) } - } + val openApkPicker = rememberAdaptiveFilePicker( + mimeTypes = APK_FILE_MIME_TYPES, + chooserTitle = stringResource(R.string.home_select_apk_title) + ) { uri -> uri?.let { homeViewModel.handleApkSelection(it) } } - val openBundlePicker = rememberLauncherForActivityResult( - contract = ActivityResultContracts.GetContent() + val openBundlePicker = rememberAdaptiveFilePicker( + mimeTypes = MPP_FILE_MIME_TYPES, + chooserTitle = stringResource(R.string.sources_dialog_local_file) ) { uri -> uri?.let { homeViewModel.selectedBundleUri = it @@ -143,26 +156,12 @@ fun HomeScreen( ) } - // Installed App Info Dialog - showInstalledAppDialog.value?.let { packageName -> - key(packageName) { - InstalledAppInfoDialog( - packageName = packageName, - onDismiss = { showInstalledAppDialog.value = null }, - onTriggerPatchFlow = { originalPackageName -> - showInstalledAppDialog.value = null - homeViewModel.showPatchDialog(originalPackageName) - }, - homeViewModel = homeViewModel - ) - } - } - // All dialogs HomeDialogs( homeViewModel = homeViewModel, - storagePickerLauncher = { openApkPicker.launch("*/*") }, - openBundlePicker = { openBundlePicker.launch("*/*") } + storagePickerLauncher = { openApkPicker() }, + openBundlePicker = { openBundlePicker() }, + patchesItem = patchesSheetItem ) // Pre-patching installer selection dialog for root-capable devices. @@ -208,13 +207,18 @@ fun HomeScreen( android11BugActive = homeViewModel.android11BugActive, installedApp = item.installedApp ) - item.installedApp?.let { showInstalledAppDialog.value = it.currentPackageName } + item.installedApp?.let { + homeViewModel.openInstalledAppInfo(it.currentPackageName) + } }, - onInstalledAppClick = { app -> showInstalledAppDialog.value = app.currentPackageName }, onHideApp = { packageName -> homeViewModel.hideApp(packageName) }, + onHideMultiple = { packageNames -> packageNames.forEach { homeViewModel.hideApp(it) } }, onUnhideApp = { packageName -> homeViewModel.unhideApp(packageName) }, + onShowPatches = { item -> patchesSheetItem.value = item }, + showGestureHint = showGestureHint, + onGestureHintShown = { homeViewModel.markSwipeGestureHintShown() }, hiddenAppItems = hiddenAppItems, - installedAppsLoading = homeViewModel.installedAppsLoading, + installedAppsLoading = bundlePipelineLoading || homeViewModel.installedAppsLoading, // Search showSearchButton = showSearchButton, diff --git a/app/src/main/java/app/morphe/manager/ui/screen/PatcherScreen.kt b/app/src/main/java/app/morphe/manager/ui/screen/PatcherScreen.kt index 47a0b1d1d..02e6ea004 100644 --- a/app/src/main/java/app/morphe/manager/ui/screen/PatcherScreen.kt +++ b/app/src/main/java/app/morphe/manager/ui/screen/PatcherScreen.kt @@ -44,7 +44,6 @@ import app.morphe.manager.ui.screen.settings.advanced.NotificationPermissionDial import app.morphe.manager.ui.screen.settings.system.InstallerSelectionDialog import app.morphe.manager.ui.viewmodel.InstallViewModel import app.morphe.manager.ui.viewmodel.PatcherViewModel -import app.morphe.manager.ui.viewmodel.SettingsViewModel.Companion.ensureValidEntries import app.morphe.manager.util.APK_MIMETYPE import app.morphe.manager.util.EventEffect import app.morphe.manager.util.tag @@ -342,10 +341,9 @@ fun PatcherScreen( // Installer entries with periodic updates var options by remember(primaryToken) { mutableStateOf( - ensureValidEntries( + installerManager.ensureValidEntries( installerManager.listEntries(installTarget, includeNone = false), primaryToken, - installerManager, installTarget ) ) @@ -354,10 +352,9 @@ fun PatcherScreen( // Periodically update installer list for availability changes LaunchedEffect(installTarget, primaryToken) { while (isActive) { - options = ensureValidEntries( + options = installerManager.ensureValidEntries( installerManager.listEntries(installTarget, includeNone = false), primaryToken, - installerManager, installTarget ) delay(1_500) diff --git a/app/src/main/java/app/morphe/manager/ui/screen/SettingsScreen.kt b/app/src/main/java/app/morphe/manager/ui/screen/SettingsScreen.kt index 787c9a63f..54d165031 100644 --- a/app/src/main/java/app/morphe/manager/ui/screen/SettingsScreen.kt +++ b/app/src/main/java/app/morphe/manager/ui/screen/SettingsScreen.kt @@ -8,8 +8,8 @@ package app.morphe.manager.ui.screen import android.annotation.SuppressLint import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.result.contract.ActivityResultContracts -import androidx.compose.animation.* -import androidx.compose.animation.core.tween +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.animateContentSize import androidx.compose.foundation.layout.* import androidx.compose.foundation.pager.HorizontalPager import androidx.compose.foundation.pager.rememberPagerState @@ -45,10 +45,9 @@ import app.morphe.manager.ui.screen.settings.system.AboutDialog import app.morphe.manager.ui.screen.settings.system.ChangelogDialog import app.morphe.manager.ui.screen.settings.system.InstallerSelectionDialogContainer import app.morphe.manager.ui.screen.settings.system.KeystoreCredentialsDialog -import app.morphe.manager.ui.screen.shared.MorpheDefaults +import app.morphe.manager.ui.screen.shared.MorpheAnimations import app.morphe.manager.ui.viewmodel.* -import app.morphe.manager.util.JSON_MIMETYPE -import app.morphe.manager.util.toast +import app.morphe.manager.util.* import kotlinx.coroutines.launch import org.koin.androidx.compose.koinViewModel import org.koin.core.parameter.parametersOf @@ -80,6 +79,7 @@ fun SettingsScreen( ) { val context = LocalContext.current val coroutineScope = rememberCoroutineScope() + val isTV = remember { context.isAndroidTv() } // Pager state for swipeable tabs val pagerState = rememberPagerState( @@ -96,17 +96,18 @@ fun SettingsScreen( // Dialog states val showAboutDialog = rememberSaveable { mutableStateOf(false) } - val showKeystoreCredentialsDialog = rememberSaveable { mutableStateOf(false) } val showInstallerDialog = remember { mutableStateOf(false) } val showChangelogDialog = remember { mutableStateOf(false) } - // Import launchers - val importKeystoreLauncher = rememberLauncherForActivityResult( - contract = ActivityResultContracts.GetContent() + // Import pickers - GetContentWithChooser on phones, OpenDocument on Android TV + val importKeystoreLauncher = rememberAdaptiveFilePicker( + mimeTypes = arrayOf("*/*"), + chooserTitle = stringResource(R.string.settings_system_import_keystore) ) { uri -> uri?.let { importExportViewModel.startKeystoreImport(it) } } - val importSettingsLauncher = rememberLauncherForActivityResult( - contract = ActivityResultContracts.GetContent() + val importSettingsLauncher = rememberAdaptiveFilePicker( + mimeTypes = arrayOf(JSON_MIMETYPE, TEXT_MIMETYPE), + chooserTitle = stringResource(R.string.settings_system_import_manager_settings) ) { uri -> uri?.let { importExportViewModel.importManagerSettings(it) } } // Export launchers @@ -119,32 +120,25 @@ fun SettingsScreen( ) { uri -> uri?.let { importExportViewModel.exportManagerSettings(it) } } val exportDebugLogsLauncher = rememberLauncherForActivityResult( - contract = ActivityResultContracts.CreateDocument("text/plain") + contract = ActivityResultContracts.CreateDocument(TEXT_MIMETYPE) ) { uri -> uri?.let { importExportViewModel.exportDebugLogs(it) } } - // Show keystore credentials dialog when needed - LaunchedEffect(importExportViewModel.showCredentialsDialog) { - showKeystoreCredentialsDialog.value = importExportViewModel.showCredentialsDialog - } - // Show about dialog if (showAboutDialog.value) { AboutDialog(onDismiss = { showAboutDialog.value = false }) } // Show keystore credentials dialog - if (showKeystoreCredentialsDialog.value) { + if (importExportViewModel.showCredentialsDialog) { KeystoreCredentialsDialog( onDismiss = { importExportViewModel.cancelKeystoreImport() - showKeystoreCredentialsDialog.value = false }, - onSubmit = { alias, pass -> + initialFormat = importExportViewModel.detectedKeystoreFormat, + onSubmit = { alias, pass, format -> coroutineScope.launch { - val result = importExportViewModel.tryKeystoreImport(alias, pass) - if (result) { - showKeystoreCredentialsDialog.value = false - } else { + val result = importExportViewModel.tryKeystoreImport(alias, pass, format) + if (!result) { context.toast(context.getString(R.string.settings_system_import_keystore_wrong_credentials)) } } @@ -199,11 +193,20 @@ fun SettingsScreen( settingsViewModel = settingsViewModel, onShowInstallerDialog = { showInstallerDialog.value = true }, importExportViewModel = importExportViewModel, - onImportKeystore = { importKeystoreLauncher.launch("*/*") }, - onExportKeystore = { exportKeystoreLauncher.launch("Morphe.keystore") }, - onImportSettings = { importSettingsLauncher.launch(JSON_MIMETYPE) }, - onExportSettings = { exportSettingsLauncher.launch("morphe_manager_settings.json") }, - onExportDebugLogs = { exportDebugLogsLauncher.launch(importExportViewModel.debugLogFileName) }, + onImportKeystore = { importKeystoreLauncher() }, + onExportKeystore = { + if (isTV) importExportViewModel.exportKeystoreToDownloads() + else exportKeystoreLauncher.launch("Morphe.keystore") + }, + onImportSettings = { importSettingsLauncher() }, + onExportSettings = { + if (isTV) importExportViewModel.exportManagerSettingsToDownloads() + else exportSettingsLauncher.launch("morphe_manager_settings.json") + }, + onExportDebugLogs = { + if (isTV) importExportViewModel.exportDebugLogsToDownloads() + else exportDebugLogsLauncher.launch(importExportViewModel.debugLogFileName) + }, onAboutClick = { showAboutDialog.value = true }, onChangelogClick = { showChangelogDialog.value = true } ) @@ -316,8 +319,8 @@ private fun NavigationItem( AnimatedVisibility( visible = isSelected, - enter = fadeIn(tween(MorpheDefaults.ANIMATION_DURATION)) + expandHorizontally(tween(MorpheDefaults.ANIMATION_DURATION)), - exit = fadeOut(tween(MorpheDefaults.ANIMATION_DURATION)) + shrinkHorizontally(tween(MorpheDefaults.ANIMATION_DURATION)) + enter = MorpheAnimations.expandHorizFadeIn, + exit = MorpheAnimations.shrinkHorizFadeOut ) { Row { Spacer(modifier = Modifier.width(8.dp)) diff --git a/app/src/main/java/app/morphe/manager/ui/screen/home/ExpertModeDialog.kt b/app/src/main/java/app/morphe/manager/ui/screen/home/ExpertModeDialog.kt index 68061103c..84269ba24 100644 --- a/app/src/main/java/app/morphe/manager/ui/screen/home/ExpertModeDialog.kt +++ b/app/src/main/java/app/morphe/manager/ui/screen/home/ExpertModeDialog.kt @@ -24,12 +24,15 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.rotate +import androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.geometry.Offset import androidx.compose.ui.geometry.Size import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalSoftwareKeyboardController import androidx.compose.ui.res.stringResource import androidx.compose.ui.semantics.contentDescription import androidx.compose.ui.semantics.semantics @@ -167,9 +170,15 @@ fun ExpertModeDialog( // Search bar AnimatedVisibility( visible = searchVisible, - enter = expandVertically(tween(MorpheDefaults.ANIMATION_DURATION)) + fadeIn(tween(MorpheDefaults.ANIMATION_DURATION)), - exit = shrinkVertically(tween(MorpheDefaults.ANIMATION_DURATION)) + fadeOut(tween(MorpheDefaults.ANIMATION_DURATION)) + enter = MorpheAnimations.expandFadeEnter, + exit = MorpheAnimations.shrinkFadeExit ) { + val focusRequester = remember { FocusRequester() } + val keyboardController = LocalSoftwareKeyboardController.current + LaunchedEffect(Unit) { + focusRequester.requestFocus() + keyboardController?.show() + } MorpheDialogTextField( value = searchQuery, onValueChange = { searchQuery = it }, @@ -182,7 +191,8 @@ fun ExpertModeDialog( contentDescription = stringResource(R.string.expert_mode_search) ) }, - showClearButton = true + showClearButton = true, + modifier = Modifier.focusRequester(focusRequester) ) } @@ -888,7 +898,7 @@ private fun PatchOptionsDialog( } }, footer = { - MorpheDialogButton( + MorpheDialogOutlinedButton( text = stringResource(R.string.close), onClick = onDismiss, modifier = Modifier.fillMaxWidth() @@ -1902,8 +1912,8 @@ fun ExpandableSurface( // Expandable content AnimatedVisibility( visible = expanded, - enter = expandVertically() + fadeIn(), - exit = shrinkVertically() + fadeOut() + enter = MorpheAnimations.expandFadeEnter, + exit = MorpheAnimations.shrinkFadeExit ) { content() } diff --git a/app/src/main/java/app/morphe/manager/ui/screen/home/HomeBottomActionBar.kt b/app/src/main/java/app/morphe/manager/ui/screen/home/HomeBottomActionBar.kt index 9ad7934b8..9bc443bdc 100644 --- a/app/src/main/java/app/morphe/manager/ui/screen/home/HomeBottomActionBar.kt +++ b/app/src/main/java/app/morphe/manager/ui/screen/home/HomeBottomActionBar.kt @@ -1,20 +1,13 @@ package app.morphe.manager.ui.screen.home -import android.annotation.SuppressLint import android.view.HapticFeedbackConstants -import androidx.compose.animation.* -import androidx.compose.animation.core.FastOutSlowInEasing -import androidx.compose.animation.core.tween +import androidx.compose.animation.AnimatedVisibility import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.layout.* import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons import androidx.compose.material.icons.outlined.* -import androidx.compose.material3.CircularProgressIndicator -import androidx.compose.material3.Icon -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Surface -import androidx.compose.material3.Text +import androidx.compose.material3.* import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -27,7 +20,7 @@ import androidx.compose.ui.semantics.* import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import app.morphe.manager.R -import app.morphe.manager.ui.screen.shared.MorpheDefaults +import app.morphe.manager.ui.screen.shared.MorpheAnimations /** * Section 5: Bottom action bar. @@ -35,14 +28,13 @@ import app.morphe.manager.ui.screen.shared.MorpheDefaults */ @Composable fun HomeBottomActionBar( + modifier: Modifier = Modifier, onBundlesClick: () -> Unit, onSettingsClick: () -> Unit, isExpertModeEnabled: Boolean = false, showSearchButton: Boolean = false, searchActive: Boolean = false, - onSearchClick: () -> Unit = {}, - @SuppressLint("ModifierParameter") - modifier: Modifier = Modifier + onSearchClick: () -> Unit = {} ) { // Show labels only when there are 2 buttons, buttons are wider so there's space val showLabels = !showSearchButton @@ -57,7 +49,7 @@ fun HomeBottomActionBar( .fillMaxWidth() .padding(bottom = 8.dp) .padding(horizontal = 16.dp), - horizontalArrangement = Arrangement.spacedBy(32.dp), + horizontalArrangement = Arrangement.spacedBy(16.dp), verticalAlignment = Alignment.CenterVertically ) { // Left: Sources button @@ -73,8 +65,8 @@ fun HomeBottomActionBar( AnimatedVisibility( visible = showSearchButton, modifier = Modifier.weight(1f), - enter = fadeIn(tween(MorpheDefaults.ANIMATION_DURATION)) + expandHorizontally(tween(MorpheDefaults.ANIMATION_DURATION, easing = FastOutSlowInEasing)), - exit = fadeOut(tween(MorpheDefaults.ANIMATION_DURATION)) + shrinkHorizontally(tween(MorpheDefaults.ANIMATION_DURATION, easing = FastOutSlowInEasing)) + enter = MorpheAnimations.expandHorizFadeIn, + exit = MorpheAnimations.shrinkHorizFadeOut ) { val searchExpandedLabel = stringResource(R.string.expanded) val searchCollapsedLabel = stringResource(R.string.collapsed) diff --git a/app/src/main/java/app/morphe/manager/ui/screen/home/HomeDialogs.kt b/app/src/main/java/app/morphe/manager/ui/screen/home/HomeDialogs.kt index 7cd6039be..26cea8817 100644 --- a/app/src/main/java/app/morphe/manager/ui/screen/home/HomeDialogs.kt +++ b/app/src/main/java/app/morphe/manager/ui/screen/home/HomeDialogs.kt @@ -6,18 +6,12 @@ package app.morphe.manager.ui.screen.home import android.annotation.SuppressLint +import android.os.Build import android.widget.Toast import androidx.compose.animation.AnimatedVisibility -import androidx.compose.animation.core.tween -import androidx.compose.animation.fadeIn -import androidx.compose.animation.fadeOut -import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.* import androidx.compose.foundation.selection.selectable import androidx.compose.foundation.selection.selectableGroup -import androidx.compose.ui.semantics.Role -import androidx.compose.ui.semantics.contentDescription -import androidx.compose.ui.semantics.semantics -import androidx.compose.foundation.layout.* import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons @@ -26,16 +20,17 @@ import androidx.compose.material.icons.filled.Check import androidx.compose.material.icons.filled.Download import androidx.compose.material.icons.outlined.* import androidx.compose.material3.* -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.alpha import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalUriHandler import androidx.compose.ui.res.stringResource +import androidx.compose.ui.semantics.Role +import androidx.compose.ui.semantics.contentDescription +import androidx.compose.ui.semantics.semantics import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.text.font.FontWeight @@ -46,19 +41,15 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle import app.morphe.manager.R import app.morphe.manager.domain.bundles.RemotePatchBundle import app.morphe.manager.domain.repository.PatchBundleRepository +import app.morphe.manager.ui.model.HomeAppItem import app.morphe.manager.ui.screen.shared.* import app.morphe.manager.ui.viewmodel.BundledAppTarget import app.morphe.manager.ui.viewmodel.HomeViewModel import app.morphe.manager.ui.viewmodel.SavedApkInfo -import app.morphe.manager.util.KnownApps -import app.morphe.manager.util.RemoteAvatar -import app.morphe.manager.util.htmlAnnotatedString -import app.morphe.manager.util.toast +import app.morphe.manager.util.* import app.morphe.patcher.patch.AppTarget -import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.delay import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext import java.net.URI /** @@ -69,7 +60,8 @@ import java.net.URI fun HomeDialogs( homeViewModel: HomeViewModel, storagePickerLauncher: () -> Unit, - openBundlePicker: () -> Unit + openBundlePicker: () -> Unit, + patchesItem: MutableState ) { val uriHandler = LocalUriHandler.current val scope = rememberCoroutineScope() @@ -77,9 +69,11 @@ fun HomeDialogs( // Dialog 1: APK availability AnimatedVisibility( - visible = homeViewModel.showApkAvailabilityDialog && homeViewModel.pendingPackageName != null && homeViewModel.pendingAppName != null, - enter = fadeIn(tween(MorpheDefaults.ANIMATION_DURATION)), - exit = fadeOut(tween(if (homeViewModel.showDownloadInstructionsDialog) 0 else MorpheDefaults.ANIMATION_DURATION)) + visible = homeViewModel.showApkAvailabilityDialog && + homeViewModel.pendingPackageName != null && + homeViewModel.pendingAppName != null, + enter = MorpheAnimations.fadeIn, + exit = MorpheAnimations.fadeOut(if (homeViewModel.showDownloadInstructionsDialog) 0 else MorpheDefaults.ANIMATION_DURATION) ) { val appName = homeViewModel.pendingAppName ?: return@AnimatedVisibility val recommendedVersion = homeViewModel.pendingRecommendedVersion @@ -124,9 +118,11 @@ fun HomeDialogs( // Dialog 2: Download instructions AnimatedVisibility( - visible = homeViewModel.showDownloadInstructionsDialog && homeViewModel.pendingPackageName != null && homeViewModel.pendingAppName != null, - enter = fadeIn(tween(MorpheDefaults.ANIMATION_DURATION)), - exit = fadeOut(tween(if (homeViewModel.showFilePickerPromptDialog) 0 else MorpheDefaults.ANIMATION_DURATION)) + visible = homeViewModel.showDownloadInstructionsDialog && + homeViewModel.pendingPackageName != null && + homeViewModel.pendingAppName != null, + enter = MorpheAnimations.overlayEnter, + exit = MorpheAnimations.fadeOut(if (homeViewModel.showFilePickerPromptDialog) 0 else MorpheDefaults.ANIMATION_DURATION) ) { val usingMountInstall = homeViewModel.usingMountInstall // Remember packageName to prevent color flickering during exit animation @@ -167,8 +163,8 @@ fun HomeDialogs( // Dialog 3: File picker prompt AnimatedVisibility( visible = homeViewModel.showFilePickerPromptDialog && homeViewModel.pendingAppName != null, - enter = fadeIn(tween(MorpheDefaults.ANIMATION_DURATION)), - exit = fadeOut(tween(MorpheDefaults.ANIMATION_DURATION)) + enter = MorpheAnimations.overlayEnter, + exit = MorpheAnimations.overlayExit ) { val appName = homeViewModel.pendingAppName ?: return@AnimatedVisibility val isOtherApps = homeViewModel.pendingPackageName == null @@ -190,8 +186,8 @@ fun HomeDialogs( // Unsupported version dialog AnimatedVisibility( visible = homeViewModel.showUnsupportedVersionDialog != null, - enter = fadeIn(tween(MorpheDefaults.ANIMATION_DURATION)), - exit = fadeOut(tween(MorpheDefaults.ANIMATION_DURATION)) + enter = MorpheAnimations.overlayEnter, + exit = MorpheAnimations.overlayExit ) { val dialogState = homeViewModel.showUnsupportedVersionDialog ?: return@AnimatedVisibility val isExpertMode = homeViewModel.prefs.useExpertMode.getBlocking() @@ -218,8 +214,8 @@ fun HomeDialogs( // Experimental version warning dialog AnimatedVisibility( visible = homeViewModel.showExperimentalVersionDialog != null, - enter = fadeIn(tween(MorpheDefaults.ANIMATION_DURATION)), - exit = fadeOut(tween(MorpheDefaults.ANIMATION_DURATION)) + enter = MorpheAnimations.overlayEnter, + exit = MorpheAnimations.overlayExit ) { val dialogState = homeViewModel.showExperimentalVersionDialog ?: return@AnimatedVisibility @@ -233,8 +229,8 @@ fun HomeDialogs( // Wrong package dialog AnimatedVisibility( visible = homeViewModel.showWrongPackageDialog != null, - enter = fadeIn(tween(MorpheDefaults.ANIMATION_DURATION)), - exit = fadeOut(tween(MorpheDefaults.ANIMATION_DURATION)) + enter = MorpheAnimations.overlayEnter, + exit = MorpheAnimations.overlayExit ) { val dialogState = homeViewModel.showWrongPackageDialog ?: return@AnimatedVisibility @@ -245,6 +241,21 @@ fun HomeDialogs( ) } + // No compatible versions dialog - shown when every declared version requires a higher SDK + AnimatedVisibility( + visible = homeViewModel.showNoCompatibleVersionsDialog != null, + enter = MorpheAnimations.overlayEnter, + exit = MorpheAnimations.overlayExit + ) { + val packageName = homeViewModel.showNoCompatibleVersionsDialog ?: return@AnimatedVisibility + val appName = homeViewModel.bundleAppMetadataFlow.value[packageName]?.displayName + ?: KnownApps.getAppName(packageName) + NoCompatibleVersionsDialog( + appName = appName, + onDismiss = { homeViewModel.showNoCompatibleVersionsDialog = null } + ) + } + // Split APK Warning Dialog - shown when user picks a split APK for an app that prefers full APK if (homeViewModel.showSplitApkWarningDialog) { val appName = homeViewModel.pendingAppName ?: "" @@ -291,6 +302,21 @@ fun HomeDialogs( ) } + // Installed App Info Dialog + homeViewModel.showInstalledAppInfoDialog?.let { packageName -> + key(packageName, homeViewModel.installedAppDialogToken) { + InstalledAppInfoDialog( + packageName = packageName, + onDismiss = homeViewModel::dismissInstalledAppInfo, + onTriggerPatchFlow = { originalPackageName -> + homeViewModel.dismissInstalledAppInfo() + homeViewModel.showPatchDialog(originalPackageName) + }, + homeViewModel = homeViewModel + ) + } + } + // Expert Mode Dialog if (homeViewModel.showExpertModeDialog) { ExpertModeDialog( @@ -405,6 +431,16 @@ fun HomeDialogs( ) } + // .mpp file opened from file manager: Add bundle confirmation dialog + homeViewModel.pendingMppUri?.let { + MppImportDialog( + manifest = homeViewModel.pendingMppManifest, + fileName = homeViewModel.pendingMppFileName, + onConfirm = { homeViewModel.confirmMppImport() }, + onDismiss = { homeViewModel.dismissMppImport() } + ) + } + // Rename bundle dialog if (homeViewModel.showRenameBundleDialog && homeViewModel.bundleToRename != null) { val bundle = homeViewModel.bundleToRename!! @@ -438,6 +474,24 @@ fun HomeDialogs( } ) } + + // Patches preview dialog (swipe-right on home app card) + patchesItem.value?.let { item -> + val patchesByBundle = remember(item.packageName) { + homeViewModel.getPatchesForPackage(item.packageName) + } + val bundleNames = remember(patchesByBundle) { + patchesByBundle.keys.associateWith { uid -> + homeViewModel.getBundleDisplayName(uid) ?: uid.toString() + } + } + AppPatchesDialog( + item = item, + patchesByBundle = patchesByBundle, + bundleNames = bundleNames, + onDismiss = { patchesItem.value = null } + ) + } } /** @@ -464,6 +518,18 @@ private fun ApkAvailabilityDialog( onNeedApk: () -> Unit, onUseSaved: () -> Unit ) { + val deviceSdk = Build.VERSION.SDK_INT + + // Versions whose minSdk exceeds the current device - shown greyed-out and non-selectable + val incompatibleSdkVersions: Set = remember(compatibleVersions, deviceSdk) { + compatibleVersions + .mapNotNull { b -> + val v = b.target.version ?: return@mapNotNull null + val minSdk = b.target.minSdk ?: return@mapNotNull null + if (deviceSdk < minSdk) v else null + } + .toSet() + } MorpheDialog( onDismissRequest = onDismiss, title = stringResource(R.string.home_apk_availability_dialog_title), @@ -526,7 +592,8 @@ private fun ApkAvailabilityDialog( recommendedBundleVersions = recommendedBundleVersions, onVersionSelect = onVersionSelect, anyString = anyString, - hasMultipleBundles = compatibleVersions.map { it.bundleUid }.distinct().size > 1 + hasMultipleBundles = compatibleVersions.map { it.bundleUid }.distinct().size > 1, + incompatibleSdkVersions = incompatibleSdkVersions, ) } else { VersionListCard( @@ -537,7 +604,8 @@ private fun ApkAvailabilityDialog( .toSet(), descriptions = compatibleVersions .mapNotNull { b -> b.target.version?.let { v -> b.target.description?.let { d -> v to d } } } - .toMap() + .toMap(), + incompatibleSdkVersions = incompatibleSdkVersions, ) } } else { @@ -1169,20 +1237,74 @@ fun WrongPackageDialog( } } +/** + * Shown when the device SDK is lower than the minSdk of every declared AppTarget for this app. + * Informs the user that their device does not meet the requirements for any supported version. + */ +@Composable +private fun NoCompatibleVersionsDialog( + appName: String, + onDismiss: () -> Unit +) { + val deviceSdk = Build.VERSION.SDK_INT + + MorpheDialog( + onDismissRequest = onDismiss, + title = stringResource(R.string.home_apk_no_compatible_versions_title), + footer = { + MorpheDialogButton( + text = stringResource(android.R.string.ok), + onClick = onDismiss, + modifier = Modifier.fillMaxWidth() + ) + } + ) { + Column( + modifier = Modifier.fillMaxWidth(), + verticalArrangement = Arrangement.spacedBy(16.dp), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Icon( + imageVector = Icons.Outlined.PhoneAndroid, + contentDescription = null, + tint = MaterialTheme.colorScheme.error, + modifier = Modifier.size(48.dp) + ) + Text( + text = htmlAnnotatedString( + stringResource( + R.string.home_apk_no_compatible_versions_message, + appName, + deviceSdk.androidVersionName(), + deviceSdk + ) + ), + style = MaterialTheme.typography.bodyLarge, + color = LocalDialogSecondaryTextColor.current, + textAlign = TextAlign.Center, + modifier = Modifier.fillMaxWidth() + ) + } + } +} + /** * Version list card where each row is tappable. - * The selected version gets a checkmark; the recommended version is labelled when not selected. - * Experimental versions are always labelled regardless of selection state. + * The selected version gets a checkmark; the recommended version is labeled when not selected. + * Experimental versions are always labeled regardless of selection state. + * Versions whose [AppTarget.minSdk] exceeds the current device SDK are shown greyed-out + * and cannot be selected. */ @Composable private fun SelectableVersionListCard( + modifier: Modifier = Modifier, versions: List, selectedVersion: AppTarget?, recommendedBundleVersions: Map, onVersionSelect: (AppTarget) -> Unit, anyString: String, hasMultipleBundles: Boolean, - modifier: Modifier = Modifier + incompatibleSdkVersions: Set = emptySet() ) { if (versions.isEmpty()) return @@ -1198,12 +1320,16 @@ private fun SelectableVersionListCard( versions.forEachIndexed { index, bundled -> val target = bundled.target val versionString = target.version ?: anyString - val isSelected = target.version != null && target.version == selectedVersion?.version - val isRecommended = target.version != null && + val isIncompatibleSdk = target.version != null && target.version in incompatibleSdkVersions + val isSelected = !isIncompatibleSdk && target.version != null && target.version == selectedVersion?.version + val isRecommended = !isIncompatibleSdk && target.version != null && target.version == recommendedBundleVersions[bundled.bundleUid]?.version val recommendedLabel = stringResource(R.string.home_apk_availability_recommended_label) val experimentalLabel = stringResource(R.string.home_dialog_unsupported_version_experimental_label) val selectedLabel = stringResource(R.string.home_selected_version) + val requiresAndroidLabel = target.minSdk?.let { sdk -> + stringResource(R.string.home_version_requires_android, sdk.androidVersionName()) + } // Bundle section header - only when multiple bundles are present and uid changes if (hasMultipleBundles && bundled.bundleUid != lastBundleUid) { @@ -1238,6 +1364,13 @@ private fun SelectableVersionListCard( } val badge: @Composable (() -> Unit)? = when { + isIncompatibleSdk -> ({ + InfoBadge( + text = requiresAndroidLabel ?: "API ${target.minSdk ?: "?"}+", + style = InfoBadgeStyle.Error, + isCompact = true + ) + }) target.isExperimental -> ({ InfoBadge( text = experimentalLabel, @@ -1258,6 +1391,7 @@ private fun SelectableVersionListCard( val rowContentDesc = buildString { append(versionString) when { + isIncompatibleSdk -> requiresAndroidLabel?.let { append(", $it") } target.isExperimental -> append(", $experimentalLabel") isRecommended -> append(", $recommendedLabel") } @@ -1269,10 +1403,13 @@ private fun SelectableVersionListCard( Row( modifier = Modifier .fillMaxWidth() - .selectable( - selected = isSelected, - onClick = { onVersionSelect(target) }, - role = Role.RadioButton + .then( + if (isIncompatibleSdk) Modifier + else Modifier.selectable( + selected = isSelected, + onClick = { onVersionSelect(target) }, + role = Role.RadioButton + ) ) .semantics { contentDescription = rowContentDesc } .padding(horizontal = 16.dp, vertical = 12.dp), @@ -1295,7 +1432,9 @@ private fun SelectableVersionListCard( } Column( - modifier = Modifier.weight(1f), + modifier = Modifier + .weight(1f) + .then(if (isIncompatibleSdk) Modifier.alpha(0.4f) else Modifier), verticalArrangement = Arrangement.spacedBy(2.dp) ) { Row( @@ -1308,6 +1447,7 @@ private fun SelectableVersionListCard( fontFamily = FontFamily.Monospace, fontWeight = if (isSelected) FontWeight.Bold else FontWeight.Normal, color = when { + isIncompatibleSdk -> LocalDialogTextColor.current isSelected -> MaterialTheme.colorScheme.primary target.isExperimental -> MaterialTheme.colorScheme.tertiary else -> LocalDialogTextColor.current @@ -1344,17 +1484,16 @@ private fun SelectableVersionListCard( } - @Composable private fun VersionListCard( + modifier: Modifier = Modifier, versions: List, recommendedIndex: Int = 0, isCompatible: Boolean = false, showUnpatchedBadge: Boolean = false, experimentalVersions: Set = emptySet(), descriptions: Map = emptyMap(), - @SuppressLint("ModifierParameter") - modifier: Modifier = Modifier + incompatibleSdkVersions: Set = emptySet(), ) { if (versions.isEmpty()) return @@ -1384,10 +1523,18 @@ private fun VersionListCard( ) { versions.forEachIndexed { index, version -> val isExperimentalVersion = version in experimentalVersions + val isIncompatibleSdk = version in incompatibleSdkVersions val versionDescription = descriptions[version] // Resolve badge once - drives both the badge composable and version text color val badge: @Composable (() -> Unit)? = when { + isIncompatibleSdk -> ({ + InfoBadge( + text = stringResource(R.string.home_apk_availability_incompatible_label), + style = InfoBadgeStyle.Error, + isCompact = true + ) + }) isExperimentalVersion -> ({ InfoBadge( text = stringResource(R.string.home_dialog_unsupported_version_experimental_label), @@ -1413,7 +1560,9 @@ private fun VersionListCard( } Column( - modifier = Modifier.fillMaxWidth(), + modifier = Modifier + .fillMaxWidth() + .then(if (isIncompatibleSdk) Modifier.alpha(0.4f) else Modifier), verticalArrangement = Arrangement.spacedBy(3.dp) ) { // Version + optional badge inline @@ -1674,3 +1823,163 @@ fun DeepLinkAddSourceDialog( } } } + +/** + * Confirmation dialog shown when a .mpp file is opened from a file manager. + */ +@Composable +fun MppImportDialog( + manifest: MppManifest?, + fileName: String?, + onConfirm: () -> Unit, + onDismiss: () -> Unit +) { + MorpheDialog( + onDismissRequest = onDismiss, + title = stringResource(R.string.deep_link_add_source_title), + compactPadding = true, + footer = { + MorpheDialogButtonRow( + primaryText = stringResource(R.string.add), + onPrimaryClick = onConfirm, + primaryIcon = Icons.Outlined.Extension, + secondaryText = stringResource(android.R.string.cancel), + onSecondaryClick = onDismiss + ) + } + ) { + Column( + verticalArrangement = Arrangement.spacedBy(16.dp), + horizontalAlignment = Alignment.CenterHorizontally, + modifier = Modifier.fillMaxWidth() + ) { + // Icon + Surface( + shape = CircleShape, + color = MaterialTheme.colorScheme.primaryContainer, + modifier = Modifier.size(56.dp) + ) { + Box(contentAlignment = Alignment.Center) { + Icon( + imageVector = Icons.Outlined.FolderZip, + contentDescription = null, + tint = MaterialTheme.colorScheme.onPrimaryContainer, + modifier = Modifier.size(28.dp) + ) + } + } + + // Message + Text( + text = stringResource(R.string.deep_link_add_source_message), + style = MaterialTheme.typography.bodyLarge, + color = LocalDialogSecondaryTextColor.current, + textAlign = TextAlign.Center, + modifier = Modifier.fillMaxWidth() + ) + + // Bundle details card + Surface( + shape = RoundedCornerShape(12.dp), + color = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.5f), + modifier = Modifier.fillMaxWidth() + ) { + Column( + modifier = Modifier.padding(12.dp), + verticalArrangement = Arrangement.spacedBy(6.dp) + ) { + // Name (bold title) + val displayName = manifest?.name ?: fileName + if (displayName != null) { + Text( + text = displayName, + style = MaterialTheme.typography.titleSmall, + fontWeight = FontWeight.SemiBold, + color = LocalDialogTextColor.current, + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + } + + // Description + manifest?.description?.let { + Text( + text = it, + style = MaterialTheme.typography.bodySmall, + color = LocalDialogSecondaryTextColor.current, + maxLines = 2, + overflow = TextOverflow.Ellipsis + ) + } + + // Metadata row: version, author + if (manifest?.version != null || manifest?.author != null) { + Row( + horizontalArrangement = Arrangement.spacedBy(8.dp), + verticalAlignment = Alignment.CenterVertically + ) { + manifest.version?.let { version -> + InfoBadge( + text = "v$version", + icon = Icons.Outlined.NewReleases, + style = InfoBadgeStyle.Primary, + isCompact = true + ) + } + manifest.author?.let { author -> + InfoBadge( + text = author, + icon = Icons.Outlined.Person, + style = InfoBadgeStyle.Default, + isCompact = true + ) + } + } + } + + // Source URL + manifest?.source?.let { source -> + Text( + text = source, + style = MaterialTheme.typography.bodySmall, + fontFamily = FontFamily.Monospace, + color = LocalDialogSecondaryTextColor.current, + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + } + + // Filename (always shown as secondary info) + if (fileName != null && manifest?.name != null) { + Row( + horizontalArrangement = Arrangement.spacedBy(4.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Icon( + imageVector = Icons.Outlined.Description, + contentDescription = null, + tint = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.size(12.dp) + ) + Text( + text = fileName, + style = MaterialTheme.typography.bodySmall, + fontFamily = FontFamily.Monospace, + color = LocalDialogSecondaryTextColor.current, + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + } + } + } + } + + InfoBadge( + text = stringResource(R.string.deep_link_add_source_warning), + style = InfoBadgeStyle.Warning, + icon = Icons.Outlined.Warning, + isExpanded = true + ) + } + } +} diff --git a/app/src/main/java/app/morphe/manager/ui/screen/home/InstalledAppInfoDialog.kt b/app/src/main/java/app/morphe/manager/ui/screen/home/InstalledAppInfoDialog.kt index 83c5173eb..1f52a7749 100644 --- a/app/src/main/java/app/morphe/manager/ui/screen/home/InstalledAppInfoDialog.kt +++ b/app/src/main/java/app/morphe/manager/ui/screen/home/InstalledAppInfoDialog.kt @@ -10,9 +10,10 @@ import android.content.pm.PackageInfo import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.result.contract.ActivityResultContracts.CreateDocument import androidx.compose.animation.* -import androidx.compose.animation.core.tween +import androidx.compose.animation.core.* import androidx.compose.foundation.background import androidx.compose.foundation.layout.* +import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.outlined.Launch @@ -27,6 +28,9 @@ import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.graphicsLayer +import androidx.compose.ui.graphics.luminance import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.pluralStringResource @@ -35,20 +39,23 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.compose.ui.util.lerp import androidx.lifecycle.compose.collectAsStateWithLifecycle import app.morphe.manager.R import app.morphe.manager.data.room.apps.installed.InstallType import app.morphe.manager.data.room.apps.installed.InstalledApp import app.morphe.manager.patcher.patch.PatchInfo +import app.morphe.manager.ui.screen.settings.system.InstallerSelectionDialog import app.morphe.manager.ui.screen.settings.system.InstallerUnavailableDialog import app.morphe.manager.ui.screen.shared.* -import app.morphe.manager.ui.screen.shared.MorpheDefaults import app.morphe.manager.ui.viewmodel.HomeViewModel import app.morphe.manager.ui.viewmodel.InstallViewModel import app.morphe.manager.ui.viewmodel.InstalledAppInfoViewModel import app.morphe.manager.util.* import org.koin.androidx.compose.koinViewModel import org.koin.core.parameter.parametersOf +import java.io.File data class AppliedPatchBundleUi( val uid: Int, @@ -90,6 +97,18 @@ fun InstalledAppInfoDialog( val appUpdates by homeViewModel.appUpdatesAvailable.collectAsStateWithLifecycle() val hasUpdate = appUpdates[packageName] == true + // Accent color resolution order: bundle metadata (appIconColor) -> KnownApps.brandColor -> default. + // originalPackageName needed because metadata is keyed by original pkg, not patched. + val bundleAppMetadata by homeViewModel.bundleAppMetadataFlow.collectAsStateWithLifecycle() + val appAccentColor: Color by remember(packageName) { + derivedStateOf { + val orig = viewModel.installedApp?.originalPackageName ?: packageName + bundleAppMetadata[orig]?.downloadColor + ?: KnownApps.fromPackage(orig)?.brandColor + ?: KnownApps.DEFAULT_DOWNLOAD_COLOR + } + } + // Dialog states val showUninstallConfirm = remember { mutableStateOf(false) } val showDeleteDialog = remember { mutableStateOf(false) } @@ -97,6 +116,9 @@ fun InstalledAppInfoDialog( val showMountWarningDialog = remember { mutableStateOf(false) } val pendingMountWarningAction = remember { mutableStateOf<(() -> Unit)?>(null) } + // Content entrance animation + val entered = remember { mutableStateOf(false) } + // Bundle data val appliedBundles by viewModel.appliedBundles.collectAsStateWithLifecycle() val bundlesUsedSummary by viewModel.bundlesUsedSummary.collectAsStateWithLifecycle() @@ -137,7 +159,10 @@ fun InstalledAppInfoDialog( } // Set back click handler - SideEffect { viewModel.onBackClick = onDismiss } + SideEffect { + viewModel.onBackClick = onDismiss + viewModel.onAppStateChanged = { pkg -> homeViewModel.notifyAppStateChanged(pkg) } + } // Handle install result LaunchedEffect(installState) { @@ -152,6 +177,7 @@ fun InstalledAppInfoDialog( else -> InstallType.DEFAULT } viewModel.updateInstallType(finalPackageName, newInstallType) + homeViewModel.notifyAppStateChanged(finalPackageName) } is InstallViewModel.InstallState.Error -> { // Show error toast @@ -172,6 +198,22 @@ fun InstalledAppInfoDialog( ) } + // Installer selection dialog (shown when promptInstallerOnInstall is enabled) + if (installViewModel.showInstallerSelectionDialog) { + val options = remember { installViewModel.getInstallerOptions() } + val primaryToken = remember { installViewModel.getPrimaryInstallerToken() } + InstallerSelectionDialog( + title = stringResource(R.string.installer_title), + options = options, + selected = primaryToken, + onDismiss = installViewModel::dismissInstallerSelectionDialog, + onConfirm = { token -> + installViewModel.proceedWithSelectedInstaller(token) + }, + onOpenShizuku = installViewModel::openShizukuApp + ) + } + // Sub-dialogs if (showAppliedPatchesDialog.value && appliedPatches != null) { AppliedPatchesDialog(bundles = appliedBundles, onDismiss = { showAppliedPatchesDialog.value = false }) @@ -228,106 +270,280 @@ fun InstalledAppInfoDialog( onDismissRequest = onDismiss, title = null, dismissOnClickOutside = true, - compactPadding = true, - footer = null + noPadding = true, + footer = null, + onEntered = { entered.value = true } ) { if (isLoading || installedApp == null) { Box( - modifier = Modifier - .fillMaxWidth() - .height(200.dp), + modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center ) { LoadingIndicator() } } else { - Column( - modifier = Modifier.fillMaxWidth(), - verticalArrangement = Arrangement.spacedBy(16.dp) - ) { - // App Header Card - AppHeaderCard( - appInfo = appInfo, - packageName = packageName, - installedApp = installedApp - ) + val windowSize = rememberWindowSize() + if (windowSize.useTwoColumnLayout) { + // Tablet layout: left column has header + info, right column has actions + Row(modifier = Modifier.fillMaxSize()) { + // Left column: header + info section + LazyColumn( + modifier = Modifier + .weight(1f) + .fillMaxHeight() + .padding(start = 20.dp), + contentPadding = PaddingValues(bottom = 24.dp), + verticalArrangement = Arrangement.spacedBy(12.dp) + ) { + item(contentType = "hero") { + AppHeroHeader( + appInfo = appInfo, + packageName = packageName, + installedApp = installedApp, + accentColor = appAccentColor, + compact = true, + modifier = Modifier.clip(RoundedCornerShape(bottomStart = 16.dp, bottomEnd = 16.dp)) + ) + } + item { + StaggeredItem(entered = entered.value, index = 1) { + InfoSection( + installedApp = installedApp, + appliedPatches = appliedPatches, + bundlesUsedSummary = bundlesUsedSummary, + onShowPatches = { showAppliedPatchesDialog.value = true }, + ) + } + } + item { Spacer(Modifier.navigationBarsPadding()) } + } - // Deleted app warning banner - AnimatedVisibility( - visible = viewModel.isAppDeleted, - enter = fadeIn(tween(MorpheDefaults.ANIMATION_DURATION)) + expandVertically(tween(MorpheDefaults.ANIMATION_DURATION)), - exit = fadeOut(tween(MorpheDefaults.ANIMATION_DURATION)) + shrinkVertically(tween(MorpheDefaults.ANIMATION_DURATION)) - ) { - WarningBanner( - icon = Icons.Outlined.Warning, - title = stringResource(R.string.home_app_info_app_deleted_warning), - description = stringResource(R.string.home_app_info_app_deleted_description), - buttonText = stringResource(R.string.patch), - buttonIcon = Icons.Outlined.AutoFixHigh, - onClick = { - onDismiss() - onTriggerPatchFlow(installedApp.originalPackageName) - }, - isError = true - ) + // Right column: banners + actions centered vertically + Column( + modifier = Modifier + .weight(1f) + .fillMaxHeight() + .padding(horizontal = 20.dp) + .navigationBarsPadding(), + verticalArrangement = Arrangement.spacedBy(12.dp, Alignment.CenterVertically), + horizontalAlignment = Alignment.CenterHorizontally + ) { + AnimatedVisibility( + visible = viewModel.isAppDeleted, + enter = MorpheAnimations.expandFadeEnter, + exit = MorpheAnimations.shrinkFadeExit + ) { + StaggeredItem(entered = entered.value, index = 1) { + WarningBanner( + icon = Icons.Outlined.Warning, + title = stringResource(R.string.home_app_info_app_deleted_warning), + description = stringResource(R.string.home_app_info_app_deleted_description), + buttonText = stringResource(R.string.patch), + buttonIcon = Icons.Outlined.AutoFixHigh, + onClick = { + onDismiss() + onTriggerPatchFlow(installedApp.originalPackageName) + }, + isError = true + ) + } + } + AnimatedVisibility( + visible = hasUpdate && !viewModel.isAppDeleted, + enter = MorpheAnimations.expandFadeEnter, + exit = MorpheAnimations.shrinkFadeExit + ) { + StaggeredItem(entered = entered.value, index = 2) { + WarningBanner( + icon = Icons.Outlined.Update, + title = stringResource(R.string.home_app_info_patch_update_available), + description = stringResource(R.string.home_app_info_patch_update_available_description), + buttonText = stringResource(R.string.patch), + buttonIcon = Icons.Outlined.AutoFixHigh, + onClick = { + onDismiss() + onTriggerPatchFlow(installedApp.originalPackageName) + }, + isError = false + ) + } + } + StaggeredItem(entered = entered.value, index = 3) { + ActionsSection( + viewModel = viewModel, + installViewModel = installViewModel, + installedApp = installedApp, + availablePatches = availablePatches, + isInstalling = isInstalling, + mountOperation = mountOperation, + hasUpdate = hasUpdate, + accentColor = appAccentColor, + onPatchClick = { handlePatchClick() }, + onUninstall = { showUninstallConfirm.value = true }, + onDelete = { showDeleteDialog.value = true }, + onExport = { exportSavedLauncher.launch(exportFileName) }, + onShowMountWarning = { action -> + pendingMountWarningAction.value = action + showMountWarningDialog.value = true + }, + modifier = Modifier.animateContentSize(animationSpec = tween(220)) + ) + } + if (!viewModel.hasOriginalApk) { + StaggeredItem(entered = entered.value, index = 4) { + InfoBadge( + text = stringResource(R.string.home_app_info_no_saved_apk), + style = InfoBadgeStyle.Warning, + icon = Icons.Outlined.Info, + isExpanded = true, + modifier = Modifier.fillMaxWidth() + ) + } + } + } } - - // Update available banner - AnimatedVisibility( - visible = hasUpdate && !viewModel.isAppDeleted, - enter = fadeIn(tween(MorpheDefaults.ANIMATION_DURATION)) + expandVertically(tween(MorpheDefaults.ANIMATION_DURATION)), - exit = fadeOut(tween(MorpheDefaults.ANIMATION_DURATION)) + shrinkVertically(tween(MorpheDefaults.ANIMATION_DURATION)) + } else { + // Single-column layout for phones + LazyColumn( + modifier = Modifier.fillMaxSize(), + contentPadding = PaddingValues(bottom = 24.dp), ) { - WarningBanner( - icon = Icons.Outlined.Update, - title = stringResource(R.string.home_app_info_patch_update_available), - description = stringResource(R.string.home_app_info_patch_update_available_description), - buttonText = stringResource(R.string.patch), - buttonIcon = Icons.Outlined.AutoFixHigh, - onClick = { - onDismiss() - onTriggerPatchFlow(installedApp.originalPackageName) - }, - isError = false - ) - } + // Hero header + item(contentType = "hero") { + AppHeroHeader( + appInfo = appInfo, + packageName = packageName, + installedApp = installedApp, + accentColor = appAccentColor, + modifier = Modifier.clip(RoundedCornerShape(bottomStart = 16.dp, bottomEnd = 16.dp)) + ) + } - // Info Section - InfoSection( - installedApp = installedApp, - appliedPatches = appliedPatches, - bundlesUsedSummary = bundlesUsedSummary, - onShowPatches = { showAppliedPatchesDialog.value = true } - ) + // Stagger index counter: hero header is index 0 (animated independently). + // Banner item always occupies index 1 (permanent item, AnimatedVisibility + // controls visibility) so subsequent indices are stable regardless of + // banner state - avoiding LazyColumn position-key conflicts + var staggerIndex = 2 + + // Warning banners (deleted / update) + item(key = "banner") { + Column { + androidx.compose.animation.AnimatedVisibility( + visible = viewModel.isAppDeleted, + enter = MorpheAnimations.expandFadeEnter, + exit = MorpheAnimations.shrinkFadeExit + ) { + Column { + Spacer(Modifier.height(12.dp)) + StaggeredItem(entered = entered.value, index = 1) { + WarningBanner( + icon = Icons.Outlined.Warning, + title = stringResource(R.string.home_app_info_app_deleted_warning), + description = stringResource(R.string.home_app_info_app_deleted_description), + buttonText = stringResource(R.string.patch), + buttonIcon = Icons.Outlined.AutoFixHigh, + onClick = { + onDismiss() + onTriggerPatchFlow(installedApp.originalPackageName) + }, + isError = true, + modifier = Modifier.padding(horizontal = 20.dp) + ) + } + } + } + androidx.compose.animation.AnimatedVisibility( + visible = hasUpdate && !viewModel.isAppDeleted, + enter = MorpheAnimations.expandFadeEnter, + exit = MorpheAnimations.shrinkFadeExit + ) { + Column { + Spacer(Modifier.height(12.dp)) + StaggeredItem(entered = entered.value, index = 1) { + WarningBanner( + icon = Icons.Outlined.Update, + title = stringResource(R.string.home_app_info_patch_update_available), + description = stringResource(R.string.home_app_info_patch_update_available_description), + buttonText = stringResource(R.string.patch), + buttonIcon = Icons.Outlined.AutoFixHigh, + onClick = { + onDismiss() + onTriggerPatchFlow(installedApp.originalPackageName) + }, + isError = false, + modifier = Modifier.padding(horizontal = 20.dp) + ) + } + } + } + } + } - // Actions Section - ActionsSection( - viewModel = viewModel, - installViewModel = installViewModel, - installedApp = installedApp, - availablePatches = availablePatches, - isInstalling = isInstalling, - mountOperation = mountOperation, - hasUpdate = hasUpdate, - onPatchClick = { handlePatchClick() }, - onUninstall = { showUninstallConfirm.value = true }, - onDelete = { showDeleteDialog.value = true }, - onExport = { exportSavedLauncher.launch(exportFileName) }, - onShowMountWarning = { action -> - pendingMountWarningAction.value = action - showMountWarningDialog.value = true + // Info Section + val infoIdx = staggerIndex++ + item { + Box(modifier = Modifier.padding(top = 12.dp)) { + StaggeredItem(entered = entered.value, index = infoIdx) { + InfoSection( + installedApp = installedApp, + appliedPatches = appliedPatches, + bundlesUsedSummary = bundlesUsedSummary, + onShowPatches = { showAppliedPatchesDialog.value = true }, + ) + } + } } - ) - // Info about saved APK availability - if (!viewModel.hasOriginalApk) { - InfoBadge( - text = stringResource(R.string.home_app_info_no_saved_apk), - style = InfoBadgeStyle.Warning, - icon = Icons.Outlined.Info, - isExpanded = true, - modifier = Modifier.fillMaxWidth() - ) + // Actions Section + val actionsIdx = staggerIndex++ + item { + StaggeredItem(entered = entered.value, index = actionsIdx) { + ActionsSection( + viewModel = viewModel, + installViewModel = installViewModel, + installedApp = installedApp, + availablePatches = availablePatches, + isInstalling = isInstalling, + mountOperation = mountOperation, + hasUpdate = hasUpdate, + accentColor = appAccentColor, + onPatchClick = { handlePatchClick() }, + onUninstall = { showUninstallConfirm.value = true }, + onDelete = { showDeleteDialog.value = true }, + onExport = { exportSavedLauncher.launch(exportFileName) }, + onShowMountWarning = { action -> + pendingMountWarningAction.value = action + showMountWarningDialog.value = true + }, + modifier = Modifier + .padding(horizontal = 20.dp) + .padding(top = 12.dp) + .animateContentSize(animationSpec = tween(220)) + ) + } + } + + // Info about saved APK availability + if (!viewModel.hasOriginalApk) { + val idx = staggerIndex++ + item { + StaggeredItem(entered = entered.value, index = idx) { + InfoBadge( + text = stringResource(R.string.home_app_info_no_saved_apk), + style = InfoBadgeStyle.Warning, + icon = Icons.Outlined.Info, + isExpanded = true, + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 20.dp) + .padding(top = 12.dp) + ) + } + } + } + + // Bottom nav bar padding + item { Spacer(Modifier.navigationBarsPadding()) } } } } @@ -345,6 +561,7 @@ private fun WarningBanner( buttonText: String, buttonIcon: ImageVector, onClick: () -> Unit, + modifier: Modifier = Modifier, isError: Boolean = false ) { val containerColor = if (isError) { @@ -359,200 +576,449 @@ private fun WarningBanner( MaterialTheme.colorScheme.onPrimaryContainer } - MorpheCard( - cornerRadius = 12.dp, - elevation = 2.dp + Column( + modifier = modifier + .fillMaxWidth() + .clip(RoundedCornerShape(12.dp)) + .background(containerColor) + .padding(12.dp), + verticalArrangement = Arrangement.spacedBy(8.dp), + horizontalAlignment = Alignment.CenterHorizontally ) { - Column( - modifier = Modifier - .fillMaxWidth() - .background(containerColor) - .padding(12.dp), - verticalArrangement = Arrangement.spacedBy(8.dp), - horizontalAlignment = Alignment.CenterHorizontally + // Header with icon + Row( + modifier = Modifier.wrapContentWidth(), + horizontalArrangement = Arrangement.spacedBy(8.dp), + verticalAlignment = Alignment.CenterVertically ) { - // Header with icon - Row( - modifier = Modifier.wrapContentWidth(), - horizontalArrangement = Arrangement.spacedBy(8.dp), - verticalAlignment = Alignment.CenterVertically - ) { - Icon( - imageVector = icon, - contentDescription = null, - tint = contentColor, - modifier = Modifier.size(20.dp) - ) - Text( - text = title, - style = MaterialTheme.typography.titleSmall, - fontWeight = FontWeight.Bold, - color = contentColor, - textAlign = TextAlign.Center - ) - } - - // Description + Icon( + imageVector = icon, + contentDescription = null, + tint = contentColor, + modifier = Modifier.size(20.dp) + ) Text( - text = description, - style = MaterialTheme.typography.bodySmall, - color = contentColor.copy(alpha = 0.9f), + text = title, + style = MaterialTheme.typography.titleSmall, + fontWeight = FontWeight.Bold, + color = contentColor, textAlign = TextAlign.Center ) - - // Action button - ActionButton( - text = buttonText, - icon = buttonIcon, - onClick = onClick, - isPrimary = true, - isHighlighted = true, - modifier = Modifier.fillMaxWidth() - ) } + + // Description + Text( + text = description, + style = MaterialTheme.typography.bodySmall, + color = contentColor.copy(alpha = 0.9f), + textAlign = TextAlign.Center + ) + + // Action button + PrimaryActionButton( + action = ActionItem(text = buttonText, icon = buttonIcon, onClick = onClick), + accentColor = if (isError) MaterialTheme.colorScheme.error else MaterialTheme.colorScheme.primary, + modifier = Modifier.fillMaxWidth() + ) } } +/** + * Hero header for the app info dialog. + */ @Composable -private fun AppHeaderCard( +private fun AppHeroHeader( appInfo: PackageInfo?, packageName: String, installedApp: InstalledApp, + accentColor: Color, + modifier: Modifier = Modifier, + compact: Boolean = false ) { - MorpheCard( - cornerRadius = 16.dp, - elevation = 2.dp - ) { - Row( + val onHero = MaterialTheme.colorScheme.onBackground + val isExtremeAccent = accentColor.luminance() !in 0.04f..0.92f + val chipBg = if (isExtremeAccent) onHero.copy(alpha = 0.12f) else accentColor.copy(alpha = 0.18f) + + val iconSize = if (compact) 56.dp else 88.dp + val iconCorner = if (compact) 14.dp else 22.dp + val topPadding = if (compact) 8.dp else 12.dp + val bottomPadding = 10.dp + val chipSpacerHeight = 8.dp + + // Entrance animations (progress-based: 0f -> 1f). + // One Float per visual group; alpha, offset and scale are derived via lerp + // to avoid redundant Recomposition subscribers. + var entered by remember { mutableStateOf(false) } + LaunchedEffect(Unit) { entered = true } + + // Icon: spring with overshoot (first thing the eye sees, no delay needed). + val iconProgress by animateFloatAsState( + targetValue = if (entered) 1f else 0f, + animationSpec = spring(dampingRatio = 0.55f, stiffness = 320f), + label = "heroIconProgress" + ) + + // Name + version share one clock; stagger handled inside graphicsLayer via lerp + val textProgress by animateFloatAsState( + targetValue = if (entered) 1f else 0f, + animationSpec = tween(durationMillis = 260, delayMillis = 60, easing = EaseOutCubic), + label = "heroTextProgress" + ) + + // Both chips share one clock; chip 2 uses a clamped sub-range for its offset + val chipsProgress by animateFloatAsState( + targetValue = if (entered) 1f else 0f, + animationSpec = tween(durationMillis = 240, delayMillis = 160, easing = EaseOutBack), + label = "heroChipsProgress" + ) + + Box(modifier = modifier.fillMaxWidth()) { + // Flat tinted background + Box( modifier = Modifier .fillMaxWidth() - .padding(16.dp), - horizontalArrangement = Arrangement.spacedBy(16.dp), - verticalAlignment = Alignment.CenterVertically + .matchParentSize() + .background(accentColor.copy(alpha = 0.15f)) + ) + + Column( + modifier = Modifier + .fillMaxWidth() + .statusBarsPadding() + .padding(start = 20.dp, end = 20.dp, top = topPadding, bottom = bottomPadding) ) { - // App icon - AppIcon( - packageInfo = appInfo, - contentDescription = null, - modifier = Modifier.size(64.dp) - ) + val (chipIcon, chipLabel) = when (installedApp.installType) { + InstallType.MOUNT -> Icons.Outlined.Link to R.string.mount + InstallType.SHIZUKU -> Icons.Outlined.Terminal to R.string.home_app_info_install_type_shizuku + InstallType.CUSTOM -> Icons.Outlined.Build to R.string.home_app_info_install_type_custom_installer + InstallType.SAVED -> Icons.Outlined.Save to R.string.saved + InstallType.DEFAULT -> Icons.Outlined.InstallMobile to R.string.home_app_info_install_type_system_installer + } - // App details - Column( - modifier = Modifier.weight(1f), - verticalArrangement = Arrangement.spacedBy(6.dp) + Row( + horizontalArrangement = Arrangement.spacedBy(16.dp), + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.fillMaxWidth() ) { - AppLabel( + // Animated app icon + AppIcon( packageInfo = appInfo, - style = MaterialTheme.typography.titleLarge.copy(fontWeight = FontWeight.Bold), - defaultText = packageName + contentDescription = null, + modifier = Modifier + .size(iconSize) + .clip(RoundedCornerShape(iconCorner)) + .graphicsLayer { + val s = lerp(0.6f, 1f, iconProgress) + scaleX = s + scaleY = s + alpha = iconProgress.coerceIn(0f, 1f) + } ) + Column( + modifier = Modifier.weight(1f), + verticalArrangement = Arrangement.spacedBy(4.dp) + ) { + // Animated app name (leads textProgress) + Box( + modifier = Modifier.graphicsLayer { + translationX = lerp(40f, 0f, textProgress) + alpha = textProgress.coerceIn(0f, 1f) + } + ) { + AppLabel( + packageInfo = appInfo, + style = MaterialTheme.typography.headlineSmall.copy( + fontWeight = FontWeight.Bold, + fontSize = 22.sp, + color = onHero + ), + maxLines = 1, + overflow = TextOverflow.Ellipsis, + defaultText = packageName + ) + } + // Animated version (slightly behind name via sub-range) + Text( + text = appInfo?.versionName?.let { "v$it" } ?: installedApp.version, + style = MaterialTheme.typography.bodyMedium, + color = onHero.copy(alpha = 0.50f), + modifier = Modifier.graphicsLayer { + val p = ((textProgress - 0.15f) / 0.85f).coerceIn(0f, 1f) + translationX = lerp(40f, 0f, p) + alpha = p + } + ) - Text( - text = appInfo?.versionName?.let { "v$it" } ?: installedApp.version, - style = MaterialTheme.typography.bodyMedium, - color = LocalDialogSecondaryTextColor.current - ) + + } + // Compact mode: chips column on the right + if (compact) { + Column( + modifier = Modifier.graphicsLayer { + translationY = lerp(20f, 0f, chipsProgress) + alpha = chipsProgress.coerceIn(0f, 1f) + }, + verticalArrangement = Arrangement.spacedBy(6.dp), + horizontalAlignment = Alignment.End + ) { + InfoChip(icon = chipIcon, text = stringResource(chipLabel), bg = chipBg, fg = onHero) + installedApp.patchedAt?.let { ts -> + InfoChip( + icon = Icons.Outlined.Schedule, + text = getRelativeTimeString(ts), + bg = chipBg, + fg = onHero + ) + } + } + } + } + + // Normal mode: chips on separate row below + if (!compact) { + Spacer(Modifier.height(chipSpacerHeight)) + + Row( + horizontalArrangement = Arrangement.spacedBy(8.dp), + ) { + // Animated chip 1 + Box( + modifier = Modifier.graphicsLayer { + translationY = lerp(20f, 0f, chipsProgress) + alpha = chipsProgress.coerceIn(0f, 1f) + } + ) { + InfoChip(icon = chipIcon, text = stringResource(chipLabel), bg = chipBg, fg = onHero) + } + // Animated chip 2 (sub-range: starts when chip1 is 30% done) + installedApp.patchedAt?.let { ts -> + Box( + modifier = Modifier.graphicsLayer { + val p = ((chipsProgress - 0.3f) / 0.7f).coerceIn(0f, 1f) + translationY = lerp(20f, 0f, p) + alpha = p + } + ) { + InfoChip( + icon = Icons.Outlined.Schedule, + text = getRelativeTimeString(ts), + bg = chipBg, + fg = onHero + ) + } + } + } } } } } +/** + * Wraps content with a staggered entrance animation. + * Uses a single progress float (0 to 1); alpha, offsetY and scale are + * derived via lerp - one Recomposition subscriber instead of three. + * Each item appears [index] * 60ms after [entered] becomes true. + */ +@Composable +private fun StaggeredItem( + entered: Boolean, + index: Int, + content: @Composable () -> Unit +) { + val progress by animateFloatAsState( + targetValue = if (entered) 1f else 0f, + animationSpec = tween( + durationMillis = 280, + delayMillis = index * 60, + easing = EaseOutCubic + ), + label = "itemProgress$index" + ) + Box( + modifier = Modifier.graphicsLayer { + alpha = progress + translationY = lerp(28f, 0f, progress) + val s = lerp(0.97f, 1f, progress) + scaleX = s + scaleY = s + } + ) { + content() + } +} + +@Composable +private fun InfoChip(icon: ImageVector, text: String, bg: Color, fg: Color) { + Row( + modifier = Modifier + .clip(RoundedCornerShape(50)) + .background(bg) + .padding(horizontal = 10.dp, vertical = 5.dp), + horizontalArrangement = Arrangement.spacedBy(5.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Icon(icon, null, tint = fg, modifier = Modifier.size(13.dp)) + Text( + text, + style = MaterialTheme.typography.labelSmall, + color = fg, + fontWeight = FontWeight.Medium, + maxLines = 1 + ) + } +} + @Composable private fun InfoSection( installedApp: InstalledApp, appliedPatches: Map>?, bundlesUsedSummary: String, - onShowPatches: () -> Unit + onShowPatches: () -> Unit, + modifier: Modifier = Modifier ) { val totalPatches = appliedPatches?.values?.sumOf { it.size } ?: 0 + val context = LocalContext.current + + // APK size from sourceDir + val apkSize = remember(installedApp.currentPackageName) { + try { + val pm = context.packageManager + val info = pm.getPackageInfo(installedApp.currentPackageName, 0) + + val bytes = File( + info.applicationInfo?.sourceDir ?: return@remember null + ).length() - MorpheCard( - cornerRadius = 16.dp, - elevation = 2.dp + formatBytes(bytes) + } catch (_: Exception) { null } + } + + Column( + modifier = modifier + .fillMaxWidth() + .padding(horizontal = 20.dp), + verticalArrangement = Arrangement.spacedBy(2.dp) ) { - Column( - modifier = Modifier - .fillMaxWidth() - .padding(16.dp), - verticalArrangement = Arrangement.spacedBy(12.dp) - ) { - // Package name + InfoRow( + icon = Icons.Outlined.Inventory2, + label = stringResource(R.string.package_name), + value = installedApp.currentPackageName + ) + + if (installedApp.originalPackageName != installedApp.currentPackageName) { InfoRow( - label = stringResource(R.string.package_name), - value = installedApp.currentPackageName + icon = Icons.Outlined.Category, + label = stringResource(R.string.home_app_info_original_package_name), + value = installedApp.originalPackageName ) + } - // Original package (if different) - if (installedApp.originalPackageName != installedApp.currentPackageName) { - MorpheSettingsDivider(fullWidth = true) - InfoRow( - label = stringResource(R.string.home_app_info_original_package_name), - value = installedApp.originalPackageName - ) - } + if (apkSize != null) { + InfoRow( + icon = Icons.Outlined.SdCard, + label = stringResource(R.string.home_app_info_apk_size), + value = apkSize + ) + } - MorpheSettingsDivider(fullWidth = true) + if (totalPatches > 0) { + InfoRowWithAction( + icon = Icons.Outlined.DoneAll, + label = stringResource(R.string.home_app_info_applied_patches), + value = pluralStringResource(R.plurals.patch_count, totalPatches, totalPatches), + onAction = onShowPatches + ) + } - // Install type + if (bundlesUsedSummary.isNotBlank()) { InfoRow( - label = stringResource(R.string.home_app_info_install_type), - value = stringResource(installedApp.installType.stringResource) + icon = Icons.Outlined.Source, + label = stringResource(R.string.home_app_info_patch_source_used), + value = bundlesUsedSummary ) + } + } +} - // Patched date (if available) - installedApp.patchedAt?.let { timestamp -> - MorpheSettingsDivider(fullWidth = true) - InfoRow( - label = stringResource(R.string.home_app_info_patched_at), - value = getRelativeTimeString(timestamp) - ) - } - - // Applied patches with icon button - if (totalPatches > 0) { - MorpheSettingsDivider(fullWidth = true) - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically - ) { - Column( - modifier = Modifier.weight(1f), - verticalArrangement = Arrangement.spacedBy(4.dp) - ) { - Text( - text = stringResource(R.string.home_app_info_applied_patches), - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onSurfaceVariant - ) - Text( - text = pluralStringResource( - R.plurals.patch_count, - totalPatches, - totalPatches - ), - style = MaterialTheme.typography.bodySmall, - fontWeight = FontWeight.Medium, - ) - } - ActionPillButton( - onClick = onShowPatches, - icon = Icons.AutoMirrored.Outlined.List, - contentDescription = stringResource(R.string.view) - ) - } - } +@Composable +private fun InfoRow( + icon: ImageVector, + label: String, + value: String, +) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 8.dp), + horizontalArrangement = Arrangement.spacedBy(14.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Icon( + imageVector = icon, + contentDescription = null, + modifier = Modifier.size(18.dp), + tint = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.4f) + ) + Column( + modifier = Modifier.weight(1f), + verticalArrangement = Arrangement.spacedBy(1.dp) + ) { + Text( + text = label, + style = MaterialTheme.typography.labelSmall, + color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.45f), + fontWeight = FontWeight.Medium + ) + Text( + text = value, + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurface, + ) + } + } +} - // Bundles used - if (bundlesUsedSummary.isNotBlank()) { - MorpheSettingsDivider(fullWidth = true) - InfoRow( - label = stringResource(R.string.home_app_info_patch_source_used), - value = bundlesUsedSummary - ) - } +@Composable +private fun InfoRowWithAction( + icon: ImageVector, + label: String, + value: String, + onAction: () -> Unit, +) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 8.dp), + horizontalArrangement = Arrangement.spacedBy(14.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Icon( + imageVector = icon, + contentDescription = null, + modifier = Modifier.size(18.dp), + tint = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.4f) + ) + Column( + modifier = Modifier.weight(1f), + verticalArrangement = Arrangement.spacedBy(1.dp) + ) { + Text( + text = label, + style = MaterialTheme.typography.labelSmall, + color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.45f), + fontWeight = FontWeight.Medium + ) + Text( + text = value, + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurface, + ) } + ActionPillButton( + onClick = onAction, + icon = Icons.AutoMirrored.Outlined.List, + contentDescription = stringResource(R.string.view) + ) } } @@ -565,11 +1031,13 @@ private fun ActionsSection( isInstalling: Boolean, mountOperation: InstallViewModel.MountOperation?, hasUpdate: Boolean, + accentColor: Color, onPatchClick: () -> Unit, onUninstall: () -> Unit, onDelete: () -> Unit, onExport: () -> Unit, - onShowMountWarning: (action: () -> Unit) -> Unit + onShowMountWarning: (action: () -> Unit) -> Unit, + modifier: Modifier = Modifier ) { // Collect all available actions val primaryActions = mutableListOf() @@ -610,8 +1078,14 @@ private fun ActionsSection( ) } + // Show install/reinstall from saved copy when: + // - installType is SAVED (normal saved app flow), or + // - app was deleted from device but a saved patched APK still exists + val showInstallFromSaved = viewModel.hasSavedCopy && + (installedApp.installType == InstallType.SAVED || viewModel.isAppDeleted) + when { - installedApp.installType == InstallType.SAVED && viewModel.hasSavedCopy -> { + showInstallFromSaved -> { val installText = if (viewModel.isInstalledOnDevice) { stringResource(R.string.reinstall) } else { @@ -653,7 +1127,12 @@ private fun ActionsSection( ) ) } - installedApp.installType == InstallType.MOUNT -> { + installedApp.installType == InstallType.SAVED -> Unit // hasSavedCopy is false, nothing to show + else -> Unit + } + + when (installedApp.installType) { + InstallType.MOUNT -> { val isMountLoading = mountOperation != null if (viewModel.isMounted) { // Remount button @@ -700,6 +1179,7 @@ private fun ActionsSection( ) } } + else -> Unit } // Destructive actions @@ -725,20 +1205,35 @@ private fun ActionsSection( ) } - Column(verticalArrangement = Arrangement.spacedBy(8.dp)) { + Column(modifier = modifier.animateContentSize(animationSpec = tween(220)), verticalArrangement = Arrangement.spacedBy(10.dp)) { // Primary actions row if (primaryActions.isNotEmpty()) { - ActionButtonsRow(actions = primaryActions, isPrimary = true) - } - - // Secondary actions row - if (secondaryActions.isNotEmpty()) { - ActionButtonsRow(actions = secondaryActions, isPrimary = false) + primaryActions.forEach { action -> + PrimaryActionButton( + action = action, + accentColor = accentColor, + modifier = Modifier.fillMaxWidth() + ) + } } - // Destructive actions row - if (destructiveActions.isNotEmpty()) { - ActionButtonsRow(actions = destructiveActions, isPrimary = false) + // Secondary + destructive - compact tile grid (2 per row) + val tileActions = secondaryActions + destructiveActions + if (tileActions.isNotEmpty()) { + tileActions.chunked(2).forEach { rowActions -> + Row( + horizontalArrangement = Arrangement.spacedBy(10.dp), + modifier = Modifier.fillMaxWidth() + ) { + rowActions.forEachIndexed { _, action -> + TileActionButton( + action = action, + modifier = if (rowActions.size == 1) Modifier.fillMaxWidth() + else Modifier.weight(1f) + ) + } + } + } } } } @@ -752,92 +1247,96 @@ private data class ActionItem( val isLoading: Boolean = false ) +/** Shared loading/icon content used by action buttons. */ @Composable -private fun ActionButtonsRow( - actions: List, - isPrimary: Boolean +private fun LoadingOrIcon(isLoading: Boolean, action: ActionItem, tint: Color) { + if (isLoading) { + CircularProgressIndicator( + modifier = Modifier.size(22.dp), + strokeWidth = 2.dp, + color = tint + ) + } else { + Icon(action.icon, null, modifier = Modifier.size(22.dp)) + } +} + +/** Full-width primary button. */ +@Composable +private fun PrimaryActionButton( + action: ActionItem, + accentColor: Color, + modifier: Modifier = Modifier ) { - Column(verticalArrangement = Arrangement.spacedBy(8.dp)) { - actions.chunked(2).forEach { rowActions -> - Row( - horizontalArrangement = Arrangement.spacedBy(8.dp), - modifier = Modifier.fillMaxWidth() - ) { - rowActions.forEach { action -> - ActionButton( - text = action.text, - icon = action.icon, - onClick = action.onClick, - enabled = action.enabled, - isDestructive = action.isDestructive, - isPrimary = isPrimary && !action.isDestructive, - isLoading = action.isLoading, - modifier = Modifier.weight(1f) - ) - } - } + // Match the header background level: tinted surface instead of full accent + val isExtremeAccent = accentColor.luminance() !in 0.04f..0.92f + val buttonColor = if (isExtremeAccent) { + MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.9f) + } else { + accentColor.copy(alpha = 0.18f) + } + val contentColor = MaterialTheme.colorScheme.onSurface + Surface( + onClick = action.onClick, + enabled = action.enabled && !action.isLoading, + modifier = modifier.height(56.dp), + shape = RoundedCornerShape(16.dp), + color = buttonColor, + contentColor = contentColor + ) { + Row( + modifier = Modifier.fillMaxSize().padding(horizontal = 24.dp), + horizontalArrangement = Arrangement.Center, + verticalAlignment = Alignment.CenterVertically + ) { + LoadingOrIcon(action.isLoading, action, contentColor) + Spacer(Modifier.width(10.dp)) + Text( + text = action.text, + style = MaterialTheme.typography.titleSmall, + fontWeight = FontWeight.Bold, + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) } } } +/** Square-ish tile button - icon on top, label below. Used for secondary/destructive actions. */ @Composable -private fun ActionButton( - text: String, - icon: ImageVector, - onClick: () -> Unit, - modifier: Modifier = Modifier, - enabled: Boolean = true, - isDestructive: Boolean = false, - isPrimary: Boolean = false, - isLoading: Boolean = false, - isHighlighted: Boolean = false +private fun TileActionButton( + action: ActionItem, + modifier: Modifier = Modifier ) { val containerColor = when { - isHighlighted -> MaterialTheme.colorScheme.primary - isDestructive -> MaterialTheme.colorScheme.errorContainer.copy(alpha = 0.5f) - isPrimary -> MaterialTheme.colorScheme.primaryContainer - !enabled -> MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.5f) - else -> MaterialTheme.colorScheme.secondaryContainer.copy(alpha = 0.7f) + action.isDestructive -> MaterialTheme.colorScheme.errorContainer.copy(alpha = 0.45f) + !action.enabled -> MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.4f) + else -> MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.6f) } - val contentColor = when { - isHighlighted -> MaterialTheme.colorScheme.onPrimary - isDestructive -> MaterialTheme.colorScheme.error - isPrimary -> MaterialTheme.colorScheme.onPrimaryContainer - !enabled -> MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.5f) - else -> MaterialTheme.colorScheme.onSecondaryContainer + action.isDestructive -> MaterialTheme.colorScheme.error + !action.enabled -> MaterialTheme.colorScheme.onSurface.copy(alpha = 0.35f) + else -> MaterialTheme.colorScheme.onSurface.copy(alpha = 0.85f) } Surface( - onClick = onClick, - enabled = enabled && !isLoading, - modifier = modifier.height(52.dp), - shape = RoundedCornerShape(14.dp), + onClick = action.onClick, + enabled = action.enabled && !action.isLoading, + modifier = modifier.height(56.dp), + shape = RoundedCornerShape(16.dp), color = containerColor, contentColor = contentColor ) { - Row( - modifier = Modifier.fillMaxSize().padding(horizontal = 8.dp), - horizontalArrangement = Arrangement.Center, - verticalAlignment = Alignment.CenterVertically + Column( + modifier = Modifier.fillMaxSize().padding(vertical = 6.dp, horizontal = 8.dp), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center ) { - if (isLoading) { - CircularProgressIndicator( - modifier = Modifier.size(20.dp), - strokeWidth = 2.dp, - color = contentColor - ) - } else { - Icon( - imageVector = icon, - contentDescription = null, - modifier = Modifier.size(20.dp) - ) - } - Spacer(modifier = Modifier.width(6.dp)) + LoadingOrIcon(action.isLoading, action, contentColor) + Spacer(Modifier.height(5.dp)) Text( - text = text, - style = MaterialTheme.typography.labelLarge, + text = action.text, + style = MaterialTheme.typography.labelMedium, fontWeight = FontWeight.SemiBold, maxLines = 1, overflow = TextOverflow.Ellipsis diff --git a/app/src/main/java/app/morphe/manager/ui/screen/home/ManagerUpdateAvailableDialog.kt b/app/src/main/java/app/morphe/manager/ui/screen/home/ManagerUpdateAvailableDialog.kt index a6eafaec4..82666739d 100644 --- a/app/src/main/java/app/morphe/manager/ui/screen/home/ManagerUpdateAvailableDialog.kt +++ b/app/src/main/java/app/morphe/manager/ui/screen/home/ManagerUpdateAvailableDialog.kt @@ -90,7 +90,7 @@ fun ManagerUpdateDetailsDialog( } UpdateViewModel.State.DOWNLOADING -> { - MorpheDialogButton( + MorpheDialogOutlinedButton( text = stringResource(R.string.close), onClick = { onDismiss() }, modifier = Modifier.fillMaxWidth() diff --git a/app/src/main/java/app/morphe/manager/ui/screen/home/SectionsLayout.kt b/app/src/main/java/app/morphe/manager/ui/screen/home/SectionsLayout.kt index 8a407447f..9964f7cdb 100644 --- a/app/src/main/java/app/morphe/manager/ui/screen/home/SectionsLayout.kt +++ b/app/src/main/java/app/morphe/manager/ui/screen/home/SectionsLayout.kt @@ -8,15 +8,19 @@ package app.morphe.manager.ui.screen.home import android.annotation.SuppressLint import android.content.pm.PackageInfo import android.view.HapticFeedbackConstants +import androidx.activity.compose.BackHandler import androidx.compose.animation.* import androidx.compose.animation.core.* import androidx.compose.foundation.* +import androidx.compose.foundation.gestures.detectHorizontalDragGestures import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.interaction.collectIsPressedAsState import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons import androidx.compose.material.icons.outlined.* @@ -26,12 +30,21 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.drawWithContent +import androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.geometry.CornerRadius import androidx.compose.ui.geometry.Offset -import androidx.compose.ui.graphics.* +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.Shadow import androidx.compose.ui.graphics.drawscope.Stroke +import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.input.pointer.pointerInput +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.platform.LocalSoftwareKeyboardController import androidx.compose.ui.platform.LocalView +import androidx.compose.ui.res.pluralStringResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.semantics.* import androidx.compose.ui.text.font.FontWeight @@ -39,16 +52,25 @@ import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp import app.morphe.manager.R import app.morphe.manager.data.room.apps.installed.InstalledApp import app.morphe.manager.domain.repository.PatchBundleRepository +import app.morphe.manager.patcher.patch.PatchInfo import app.morphe.manager.ui.model.HomeAppItem import app.morphe.manager.ui.screen.shared.* import app.morphe.manager.ui.viewmodel.BundleUpdateStatus import app.morphe.manager.util.AppDataSource import app.morphe.manager.util.KnownApps import kotlinx.coroutines.delay +import kotlinx.coroutines.launch + +/** Data describing one side of a swipe action - icon, label, and colors. */ +private data class SwipeActionConfig( + val icon: ImageVector, + val label: String, + val containerColor: Color, + val contentColor: Color +) /** * Home screen layout with dynamic app buttons: @@ -68,14 +90,17 @@ fun SectionsLayout( onShowUpdateDetails: () -> Unit, // Greeting section - greetingMessage: String, + greetingMessage: String?, // Dynamic app items homeAppItems: List, onAppClick: (HomeAppItem) -> Unit, - onInstalledAppClick: (InstalledApp) -> Unit, onHideApp: (String) -> Unit, + onHideMultiple: (Set) -> Unit = {}, onUnhideApp: (String) -> Unit, + onShowPatches: (HomeAppItem) -> Unit, + showGestureHint: Boolean, + onGestureHintShown: () -> Unit, hiddenAppItems: List = emptyList(), installedAppsLoading: Boolean = false, @@ -120,9 +145,12 @@ fun SectionsLayout( greetingMessage = greetingMessage, homeAppItems = homeAppItems, onAppClick = onAppClick, - onInstalledAppClick = onInstalledAppClick, onHideApp = onHideApp, + onHideMultiple = onHideMultiple, onUnhideApp = onUnhideApp, + onShowPatches = onShowPatches, + showGestureHint = showGestureHint, + onGestureHintShown = onGestureHintShown, hiddenAppItems = hiddenAppItems, installedAppsLoading = installedAppsLoading, showSearchButton = showSearchButton, @@ -138,7 +166,7 @@ fun SectionsLayout( ) } - // Section 5: Bottom action bar — тільки для одноколонкового (portrait) режиму + // Section 5: Bottom action bar - тільки для одноколонкового (portrait) режиму if (!windowSize.useTwoColumnLayout) { HomeBottomActionBar( onBundlesClick = onBundlesClick, @@ -172,12 +200,15 @@ fun SectionsLayout( @Composable private fun AdaptiveContent( windowSize: WindowSize, - greetingMessage: String, + greetingMessage: String?, homeAppItems: List, onAppClick: (HomeAppItem) -> Unit, - onInstalledAppClick: (InstalledApp) -> Unit, onHideApp: (String) -> Unit, + onHideMultiple: (Set) -> Unit = {}, onUnhideApp: (String) -> Unit, + onShowPatches: (HomeAppItem) -> Unit, + showGestureHint: Boolean, + onGestureHintShown: () -> Unit, hiddenAppItems: List = emptyList(), installedAppsLoading: Boolean, showSearchButton: Boolean = false, @@ -250,9 +281,12 @@ private fun AdaptiveContent( homeAppItems = homeAppItems, itemSpacing = itemSpacing, onAppClick = onAppClick, - onInstalledAppClick = onInstalledAppClick, onHideApp = onHideApp, + onHideMultiple = onHideMultiple, onUnhideApp = onUnhideApp, + onShowPatches = onShowPatches, + showGestureHint = showGestureHint, + onGestureHintShown = onGestureHintShown, hiddenAppItems = hiddenAppItems, installedAppsLoading = installedAppsLoading, searchVisible = searchVisible, @@ -265,8 +299,8 @@ private fun AdaptiveContent( // Section 4: Other apps - hidden when no apps available or bundles loading AnimatedVisibility( visible = !isAppsEmpty && showOtherAppsButton, - enter = fadeIn(tween(MorpheDefaults.ANIMATION_DURATION)) + expandVertically(tween(MorpheDefaults.ANIMATION_DURATION)), - exit = fadeOut(tween(MorpheDefaults.ANIMATION_DURATION)) + shrinkVertically(tween(MorpheDefaults.ANIMATION_DURATION)) + enter = MorpheAnimations.expandFadeEnter, + exit = MorpheAnimations.shrinkFadeExit ) { OtherAppsSection( onClick = onOtherAppsClick, @@ -281,10 +315,14 @@ private fun AdaptiveContent( modifier = Modifier.fillMaxSize(), verticalArrangement = Arrangement.Center ) { - // Section 2: Greeting - GreetingSection(message = greetingMessage) - - Spacer(modifier = Modifier.height(itemSpacing)) + // Section 2: Greeting - when disabled, show a small top spacer so + // the app cards don't sit flush against the top of the screen + if (!greetingMessage.isNullOrEmpty()) { + GreetingSection(message = greetingMessage) + Spacer(modifier = Modifier.height(itemSpacing)) + } else { + Spacer(modifier = Modifier.height(24.dp)) + } // Section 3: Scrollable app buttons Box(modifier = Modifier.weight(1f, fill = false)) { @@ -292,9 +330,12 @@ private fun AdaptiveContent( homeAppItems = homeAppItems, itemSpacing = itemSpacing, onAppClick = onAppClick, - onInstalledAppClick = onInstalledAppClick, onHideApp = onHideApp, + onHideMultiple = onHideMultiple, onUnhideApp = onUnhideApp, + onShowPatches = onShowPatches, + showGestureHint = showGestureHint, + onGestureHintShown = onGestureHintShown, hiddenAppItems = hiddenAppItems, installedAppsLoading = installedAppsLoading, searchVisible = searchVisible, @@ -308,8 +349,8 @@ private fun AdaptiveContent( // Section 4: Other apps - hidden when no apps available or bundles loading AnimatedVisibility( visible = !isAppsEmpty && showOtherAppsButton, - enter = fadeIn(tween(MorpheDefaults.ANIMATION_DURATION)) + expandVertically(tween(MorpheDefaults.ANIMATION_DURATION)), - exit = fadeOut(tween(MorpheDefaults.ANIMATION_DURATION)) + shrinkVertically(tween(MorpheDefaults.ANIMATION_DURATION)) + enter = MorpheAnimations.expandFadeEnter, + exit = MorpheAnimations.shrinkFadeExit ) { Column { Spacer(modifier = Modifier.height(itemSpacing)) @@ -397,12 +438,8 @@ fun ManagerUpdateSnackbar( AnimatedVisibility( visible = visible && !dismissed, - enter = slideInVertically( - initialOffsetY = { -it }, - animationSpec = tween(MorpheDefaults.ANIMATION_DURATION)) + fadeIn(tween(MorpheDefaults.ANIMATION_DURATION)), - exit = slideOutVertically( - targetOffsetY = { -it }, - animationSpec = tween(MorpheDefaults.ANIMATION_DURATION)) + fadeOut(tween(MorpheDefaults.ANIMATION_DURATION)), + enter = MorpheAnimations.slideUpFadeEnter, + exit = MorpheAnimations.slideUpFadeExit, modifier = modifier ) { SwipeToDismissBox( @@ -486,12 +523,8 @@ fun BundleUpdateSnackbar( AnimatedVisibility( visible = visible && !dismissed, - enter = slideInVertically( - initialOffsetY = { -it }, - animationSpec = tween(MorpheDefaults.ANIMATION_DURATION)) + fadeIn(tween(MorpheDefaults.ANIMATION_DURATION)), - exit = slideOutVertically( - targetOffsetY = { -it }, - animationSpec = tween(MorpheDefaults.ANIMATION_DURATION)) + fadeOut(tween(MorpheDefaults.ANIMATION_DURATION)), + enter = MorpheAnimations.slideUpFadeEnter, + exit = MorpheAnimations.slideUpFadeExit, modifier = modifier ) { SwipeToDismissBox( @@ -671,8 +704,9 @@ private fun BundleUpdateSnackbarContent( */ @Composable fun GreetingSection( - message: String + message: String? ) { + if (message.isNullOrEmpty()) return Box(contentAlignment = Alignment.Center) { AnimatedContent( targetState = message, @@ -701,40 +735,63 @@ fun GreetingSection( /** * Section 3: Dynamic scrollable app buttons list. */ +@SuppressLint("FrequentlyChangingValue") @Composable fun MainAppsSection( + modifier: Modifier = Modifier, homeAppItems: List, itemSpacing: Dp = 16.dp, onAppClick: (HomeAppItem) -> Unit, - onInstalledAppClick: (InstalledApp) -> Unit, onHideApp: (String) -> Unit, + onHideMultiple: (Set) -> Unit = {}, onUnhideApp: (String) -> Unit, + onShowPatches: (HomeAppItem) -> Unit, + showGestureHint: Boolean, + onGestureHintShown: () -> Unit, hiddenAppItems: List = emptyList(), installedAppsLoading: Boolean = false, searchVisible: Boolean = false, searchQuery: String = "", onSearchQueryChange: (String) -> Unit = {}, - onBundlesClick: () -> Unit = {}, - @SuppressLint("ModifierParameter") - modifier: Modifier = Modifier + onBundlesClick: () -> Unit = {} ) { - // Track if data was ever loaded, never show shimmer again on resume - var hasEverLoaded by remember { mutableStateOf(homeAppItems.isNotEmpty()) } + // Multi-select state - set of packageNames chosen for bulk hide + var selectedPackages by remember { mutableStateOf(emptySet()) } + val isMultiSelectMode = selectedPackages.isNotEmpty() + + // Back gesture/button cancels multi-select instead of navigating back + BackHandler(enabled = isMultiSelectMode) { + selectedPackages = emptySet() + } + + // Exit multi-select when items list changes + LaunchedEffect(homeAppItems) { + selectedPackages = selectedPackages.filter { pkg -> + homeAppItems.any { it.packageName == pkg } + }.toSet() + } + + // Track if real content has ever arrived so we never re-show the shimmer on resume + var hasEverLoaded by remember { + mutableStateOf(homeAppItems.isNotEmpty() || hiddenAppItems.isNotEmpty()) + } - // Stable loading state with debounce to prevent flickering. - // Only shows shimmer on genuine cold start (data never arrived). + // Stable loading state - drives shimmer visibility. + // Starts as true when there is nothing to show yet; once content arrives it latches to false + // and never goes back to true (we don't want shimmer on every recomposition). var stableLoadingState by remember { mutableStateOf(!hasEverLoaded) } - LaunchedEffect(installedAppsLoading, homeAppItems.isEmpty()) { - if (homeAppItems.isNotEmpty()) { - hasEverLoaded = true - } - // Once hasEverLoaded is true, never re-trigger shimmer regardless of list state - val shouldLoad = !hasEverLoaded || installedAppsLoading - if (shouldLoad) { + LaunchedEffect(installedAppsLoading, homeAppItems.size, hiddenAppItems.size) { + val hasItems = homeAppItems.isNotEmpty() || hiddenAppItems.isNotEmpty() + if (hasItems) hasEverLoaded = true + + val shouldShowShimmer = !hasEverLoaded && installedAppsLoading + if (shouldShowShimmer) { stableLoadingState = true } else { - delay(300) + // Small delay so Compose has one frame to lay out the real cards before the + // shimmer fades out - prevents a single-frame empty gap. + if (stableLoadingState) delay(50) stableLoadingState = false } } @@ -749,11 +806,15 @@ fun MainAppsSection( HiddenAppsDialog( hiddenAppItems = hiddenAppItems, onUnhide = onUnhideApp, + onUnhideMultiple = { packages -> + packages.forEach { onUnhideApp(it) } + }, + onShowPatches = onShowPatches, onDismiss = { showHiddenAppsDialog.value = false } ) } - // Filtered items based on search query + // Filtered visible items based on search query val filteredItems = remember(homeAppItems, searchQuery) { if (searchQuery.isBlank()) homeAppItems else homeAppItems.filter { item -> @@ -762,14 +823,26 @@ fun MainAppsSection( } } + // Hidden items that match the search query + val filteredHiddenItems = remember(hiddenAppItems, searchQuery) { + if (searchQuery.isBlank()) emptyList() + else hiddenAppItems.filter { item -> + item.displayName.contains(searchQuery, ignoreCase = true) || + item.packageName.contains(searchQuery, ignoreCase = true) + } + } + val listState = rememberLazyListState() val fadeSize = 24.dp - // True empty state: loaded but no apps at all: all bundles disabled or no sources - val isEmptyState = !stableLoadingState && homeAppItems.isEmpty() - // Search empty state: items exist but nothing matches query + // True empty state: loaded, no apps from any bundle (no sources / all disabled) + val isNoSourcesState = !stableLoadingState && homeAppItems.isEmpty() && hiddenAppItems.isEmpty() + // All-hidden state: apps exist but all are hidden + val isAllHiddenState = !stableLoadingState && homeAppItems.isEmpty() && hiddenAppItems.isNotEmpty() + val isEmptyState = isNoSourcesState || isAllHiddenState + // Search empty state: items exist but nothing matches query (including hidden) val isSearchEmpty = !stableLoadingState && homeAppItems.isNotEmpty() && - searchQuery.isNotBlank() && filteredItems.isEmpty() + searchQuery.isNotBlank() && filteredItems.isEmpty() && filteredHiddenItems.isEmpty() Box( modifier = modifier.fillMaxWidth(), @@ -783,141 +856,233 @@ fun MainAppsSection( label = "home_empty_state" ) { empty -> if (empty) { - HomeEmptyState(onBundlesClick = onBundlesClick) + if (isAllHiddenState) { + MorpheEmptyState( + icon = Icons.Outlined.VisibilityOff, + title = stringResource(R.string.home_all_apps_hidden_title), + subtitle = stringResource(R.string.home_all_apps_hidden_subtitle), + actionIcon = Icons.Outlined.Visibility, + actionLabel = pluralStringResource(R.plurals.home_app_show_hidden_count, hiddenAppItems.size, hiddenAppItems.size.toString()), + onAction = { showHiddenAppsDialog.value = true } + ) + } else { + MorpheEmptyState( + icon = Icons.Outlined.Inbox, + title = stringResource(R.string.home_no_apps_title), + subtitle = stringResource(R.string.home_no_apps_subtitle, stringResource(R.string.sources_management_title)), + actionIcon = Icons.Outlined.Source, + actionLabel = stringResource(R.string.sources_management_title), + onAction = onBundlesClick + ) + } } else { - Column( + Box( modifier = Modifier .widthIn(max = 500.dp) .fillMaxWidth() ) { - // Search bar - AnimatedVisibility( - visible = searchVisible, - enter = expandVertically(tween(MorpheDefaults.ANIMATION_DURATION)) + fadeIn(tween(MorpheDefaults.ANIMATION_DURATION)), - exit = shrinkVertically(tween(MorpheDefaults.ANIMATION_DURATION)) + fadeOut(tween(MorpheDefaults.ANIMATION_DURATION)) - ) { - HomeSearchTextField( - value = searchQuery, - onValueChange = onSearchQueryChange, - modifier = Modifier.padding(bottom = 8.dp) - ) - } + Column(modifier = Modifier.fillMaxWidth()) { + // Search bar + AnimatedVisibility( + visible = searchVisible, + enter = MorpheAnimations.expandFadeEnter, + exit = MorpheAnimations.shrinkFadeExit + ) { + HomeSearchTextField( + value = searchQuery, + onValueChange = onSearchQueryChange, + requestFocus = searchVisible, + modifier = Modifier.padding(bottom = 8.dp) + ) + } - LazyColumn( - state = listState, - modifier = Modifier - .fillMaxWidth() - .graphicsLayer { compositingStrategy = CompositingStrategy.Offscreen } - .drawWithContent { - drawContent() - val fadePx = fadeSize.toPx() - val canScrollUp = listState.firstVisibleItemIndex > 0 || - listState.firstVisibleItemScrollOffset > 0 - if (canScrollUp) { - drawRect( - brush = Brush.verticalGradient( - colors = listOf(Color.Transparent, Color.Black), - startY = 0f, - endY = fadePx - ), - blendMode = BlendMode.DstIn - ) - } - val canScrollDown = listState.canScrollForward - if (canScrollDown) { - drawRect( - brush = Brush.verticalGradient( - colors = listOf(Color.Black, Color.Transparent), - startY = size.height - fadePx, - endY = size.height - ), - blendMode = BlendMode.DstIn - ) - } - }, - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.spacedBy(itemSpacing), - contentPadding = PaddingValues(vertical = 8.dp) - ) { - // Cold start: homeAppItems still empty - show placeholder shimmer cards - if (stableLoadingState && homeAppItems.isEmpty()) { - items(3, key = { "placeholder_$it" }) { index -> - AppLoadingCard( - gradientColors = placeholderGradients[index % placeholderGradients.size], - modifier = Modifier.animateItem() - ) - } - } else { - items( - items = filteredItems, - key = { it.packageName } - ) { item -> - DynamicAppCard( - item = item, - isLoading = stableLoadingState, - hasUpdate = item.hasUpdate, - onAppClick = { onAppClick(item) }, - onInstalledAppClick = onInstalledAppClick, - onHide = { onHideApp(item.packageName) }, - modifier = Modifier.animateItem() + // LazyColumn has no Offscreen compositing so graphicsLayer { translationX } + // on individual cards is not clipped during swipe. + // The vertical fade is drawn as a pointer-transparent overlay on top. + Box(modifier = Modifier.fillMaxWidth()) { + LazyColumn( + state = listState, + modifier = Modifier.fillMaxWidth(), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(itemSpacing), + contentPadding = PaddingValues( + // Extra bottom padding so cards aren't hidden behind the multi-select bar + bottom = if (isMultiSelectMode) 96.dp else 0.dp ) - } - - // Search empty result - if (isSearchEmpty) { - item(key = "search_empty") { - Column( - modifier = Modifier - .fillMaxWidth() - .padding(vertical = 32.dp) - .animateItem(), - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.spacedBy(8.dp) - ) { - Icon( - imageVector = Icons.Outlined.SearchOff, - contentDescription = null, - modifier = Modifier.size(40.dp), - tint = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.4f) - ) - Text( - text = stringResource(R.string.search_no_results), - style = MaterialTheme.typography.titleMedium, - fontWeight = FontWeight.SemiBold, - color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.6f) + ) { + // Cold start: homeAppItems still empty - show placeholder shimmer cards + if (stableLoadingState && homeAppItems.isEmpty()) { + items(3, key = { "placeholder_$it" }) { index -> + AppLoadingCard( + gradientColors = placeholderGradients[index % placeholderGradients.size], + modifier = Modifier.animateItem() ) - Text( - text = stringResource(R.string.home_no_apps_search_subtitle, searchQuery), - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.4f), - textAlign = TextAlign.Center + } + } else { + itemsIndexed( + items = filteredItems, + key = { _, item -> item.packageName } + ) { index, item -> + val isSelected = item.packageName in selectedPackages + DynamicAppCard( + item = item, + isLoading = stableLoadingState, + hasUpdate = item.hasUpdate, + onAppClick = { + if (isMultiSelectMode) { + // In multi-select mode taps toggle selection + selectedPackages = if (isSelected) + selectedPackages - item.packageName + else + selectedPackages + item.packageName + } else { + onAppClick(item) + } + }, + onHide = { onHideApp(item.packageName) }, + onShowPatches = { onShowPatches(item) }, + // Hint plays only on the first card + showGestureHint = index == 0 && showGestureHint, + onGestureHintShown = onGestureHintShown, + isSelected = isSelected, + isMultiSelectMode = isMultiSelectMode, + onLongPress = { + // Long-press enters multi-select and toggles this card + selectedPackages = if (isSelected) + selectedPackages - item.packageName + else + selectedPackages + item.packageName + }, + modifier = Modifier.animateItem() ) } - } - } - // "Show hidden apps" button if there are hidden apps - if (hiddenAppItems.isNotEmpty()) { - item(key = "show_hidden") { - TextButton( - onClick = { showHiddenAppsDialog.value = true }, - modifier = Modifier.padding(top = 4.dp) - ) { - Icon( - imageVector = Icons.Outlined.Visibility, - contentDescription = null, - modifier = Modifier.size(18.dp) - ) - Spacer(modifier = Modifier.width(8.dp)) - Text( - text = stringResource(R.string.home_app_show_hidden), - style = MaterialTheme.typography.bodyMedium - ) + // Hidden apps that match the search query + if (filteredHiddenItems.isNotEmpty()) { + item(key = "search_hidden_header") { + Text( + text = stringResource(R.string.hidden), + style = MaterialTheme.typography.labelMedium, + color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.5f), + modifier = Modifier + .fillMaxWidth() + .padding(top = 8.dp, bottom = 4.dp) + .animateItem() + ) + } + itemsIndexed( + items = filteredHiddenItems, + key = { _, item -> "hidden_${item.packageName}" } + ) { _, item -> + HiddenSearchAppCard( + item = item, + onUnhide = { onUnhideApp(item.packageName) }, + onAppClick = { onAppClick(item) }, + onShowPatches = { onShowPatches(item) }, + modifier = Modifier.animateItem() + ) + } + } + + // Search empty result + if (isSearchEmpty) { + item(key = "search_empty") { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 32.dp) + .animateItem(), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(8.dp) + ) { + Icon( + imageVector = Icons.Outlined.SearchOff, + contentDescription = null, + modifier = Modifier.size(40.dp), + tint = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.4f) + ) + Text( + text = stringResource(R.string.search_no_results), + style = MaterialTheme.typography.titleMedium, + fontWeight = FontWeight.SemiBold, + color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.6f) + ) + Text( + text = stringResource(R.string.home_no_apps_search_subtitle, searchQuery), + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.4f), + textAlign = TextAlign.Center + ) + } + } + } + + // "Show hidden apps" button + if (hiddenAppItems.isNotEmpty() && searchQuery.isBlank()) { + item(key = "show_hidden") { + ShowHiddenAppsButton( + count = hiddenAppItems.size, + onClick = { showHiddenAppsDialog.value = true }, + modifier = Modifier.animateItem( + fadeInSpec = tween(MorpheDefaults.ANIMATION_DURATION), + fadeOutSpec = tween(MorpheDefaults.ANIMATION_DURATION), + placementSpec = spring(stiffness = Spring.StiffnessMediumLow) + ) + ) + } } } } + + // Vertical fade overlay drawn on top of LazyColumn. + // Does NOT use Offscreen compositing so the LazyColumn items + // (translated via graphicsLayer) are never clipped horizontally. + val canScrollUp = listState.firstVisibleItemIndex > 0 || + listState.firstVisibleItemScrollOffset > 0 + val canScrollDown = listState.canScrollForward + if (canScrollUp || canScrollDown) { + val bgColor = MaterialTheme.colorScheme.background + val fadePx = with(LocalDensity.current) { fadeSize.toPx() } + Box( + modifier = Modifier + .matchParentSize() + .drawWithContent { + drawContent() + if (canScrollUp) { + drawRect( + brush = Brush.verticalGradient( + colors = listOf(bgColor, Color.Transparent), + startY = 0f, + endY = fadePx + ) + ) + } + if (canScrollDown) { + drawRect( + brush = Brush.verticalGradient( + colors = listOf(Color.Transparent, bgColor), + startY = size.height - fadePx, + endY = size.height + ) + ) + } + } + ) + } } } + + // Multi-select confirmation bar - slides up from bottom + MultiSelectBar( + selectedCount = selectedPackages.size, + visible = isMultiSelectMode, + onHide = { + onHideMultiple(selectedPackages) + selectedPackages = emptySet() + }, + onCancel = { selectedPackages = emptySet() }, + modifier = Modifier.align(Alignment.BottomCenter) + ) } } } @@ -925,13 +1090,35 @@ fun MainAppsSection( } /** - * Empty state shown when no apps are available from any bundle. - * Typically, seen when all sources are disabled or none added yet. + * Pill-shaped button that appears at the bottom of the app list when hidden apps exist. + * Styled consistently with [OtherAppsSection] - frosted glass surface with border. */ @Composable -private fun HomeEmptyState( - onBundlesClick: () -> Unit, +private fun ShowHiddenAppsButton( + count: Int, + onClick: () -> Unit, modifier: Modifier = Modifier +) { + HomeGlassPillButton( + onClick = onClick, + modifier = modifier, + icon = Icons.Outlined.Visibility, + text = pluralStringResource(R.plurals.home_app_show_hidden_count, count, count.toString()) + ) +} + +/** + * Generic empty state with icon, title, optional subtitle and optional action button. + */ +@Composable +internal fun MorpheEmptyState( + modifier: Modifier = Modifier, + icon: ImageVector, + title: String, + subtitle: String? = null, + actionIcon: ImageVector? = null, + actionLabel: String? = null, + onAction: (() -> Unit)? = null ) { Column( modifier = modifier @@ -942,48 +1129,64 @@ private fun HomeEmptyState( verticalArrangement = Arrangement.spacedBy(12.dp) ) { Icon( - imageVector = Icons.Outlined.Inbox, + imageVector = icon, contentDescription = null, modifier = Modifier.size(56.dp), tint = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.35f) ) Text( - text = stringResource(R.string.home_no_apps_title), + text = title, style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.SemiBold, color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.6f), textAlign = TextAlign.Center ) - Text( - text = stringResource(R.string.home_no_apps_subtitle, stringResource(R.string.sources_management_title)), - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.4f), - textAlign = TextAlign.Center - ) - Spacer(modifier = Modifier.height(4.dp)) - FilledTonalButton(onClick = onBundlesClick) { - Icon( - imageVector = Icons.Outlined.Source, - contentDescription = null, - modifier = Modifier.size(18.dp) + if (subtitle != null) { + Text( + text = subtitle, + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.4f), + textAlign = TextAlign.Center ) - Spacer(modifier = Modifier.width(8.dp)) - Text(stringResource(R.string.sources_management_title)) + } + if (onAction != null && actionLabel != null) { + Spacer(modifier = Modifier.height(4.dp)) + FilledTonalButton(onClick = onAction) { + if (actionIcon != null) { + Icon( + imageVector = actionIcon, + contentDescription = null, + modifier = Modifier.size(18.dp) + ) + Spacer(modifier = Modifier.width(8.dp)) + } + Text(actionLabel) + } } } } /** - * Standalone search field for the home screen. * Wraps [MorpheDialogTextField] with [LocalDialogTextColor] set to onSurface * so it renders correctly outside a dialog context. */ @Composable private fun HomeSearchTextField( + modifier: Modifier = Modifier, value: String, onValueChange: (String) -> Unit, - modifier: Modifier = Modifier + requestFocus: Boolean = false ) { + val focusRequester = remember { FocusRequester() } + val keyboardController = LocalSoftwareKeyboardController.current + + LaunchedEffect(requestFocus) { + if (requestFocus) { + focusRequester.requestFocus() + keyboardController?.show() + } + } + CompositionLocalProvider(LocalDialogTextColor provides MaterialTheme.colorScheme.onSurface) { MorpheDialogTextField( value = value, @@ -996,67 +1199,209 @@ private fun HomeSearchTextField( ) }, showClearButton = true, - modifier = modifier + modifier = modifier.focusRequester(focusRequester) ) } } /** - * Single dynamic app card with long-press action. + * App card with optional selection overlay - shared between [DynamicAppCard] and [HiddenAppsDialog]. + * + * Renders [AppCardLayout] with the given [content], and overlays an animated checkmark badge + * when [isSelected] is true. Dims the card when [isMultiSelectMode] is active but this card + * is not selected. + */ +@Composable +private fun SelectableAppCard( + modifier: Modifier = Modifier, + isSelected: Boolean, + isMultiSelectMode: Boolean, + content: @Composable () -> Unit +) { + val checkScale by animateFloatAsState( + targetValue = if (isSelected) 1f else 0f, + animationSpec = spring(dampingRatio = Spring.DampingRatioMediumBouncy, stiffness = Spring.StiffnessMedium), + label = "check_scale" + ) + val cardAlpha by animateFloatAsState( + targetValue = if (isMultiSelectMode && !isSelected) 0.55f else 1f, + animationSpec = tween(200), + label = "card_alpha" + ) + + Box(modifier = modifier) { + Box(modifier = Modifier.graphicsLayer { alpha = cardAlpha }) { + content() + } + + // Animated checkmark badge - top-right corner + if (checkScale > 0f) { + Box( + modifier = Modifier + .align(Alignment.TopEnd) + .padding(top = 8.dp, end = 8.dp) + .graphicsLayer { scaleX = checkScale; scaleY = checkScale } + ) { + Surface( + shape = CircleShape, + color = MaterialTheme.colorScheme.primary, + modifier = Modifier.size(28.dp) + ) { + Box(contentAlignment = Alignment.Center) { + Icon( + imageVector = Icons.Outlined.Check, + contentDescription = null, + tint = MaterialTheme.colorScheme.onPrimary, + modifier = Modifier.size(16.dp) + ) + } + } + } + } + } +} + +/** + * Single dynamic app card with horizontal swipe gestures: + * - Swipe LEFT → reveal hide action + * - Swipe RIGHT → reveal patches dialog + * + * On first appearance plays a one-time nudge hint animation. */ @Composable private fun DynamicAppCard( + modifier: Modifier = Modifier, item: HomeAppItem, isLoading: Boolean, hasUpdate: Boolean, onAppClick: () -> Unit, - onInstalledAppClick: (InstalledApp) -> Unit, onHide: () -> Unit, - modifier: Modifier = Modifier + onShowPatches: () -> Unit, + showGestureHint: Boolean, + onGestureHintShown: () -> Unit, + isSelected: Boolean = false, + isMultiSelectMode: Boolean = false, + onLongPress: () -> Unit = {} ) { - var showContextMenu by remember { mutableStateOf(false) } + var showHideDialog by remember { mutableStateOf(false) } + val density = LocalDensity.current + val view = LocalView.current + + val actionThresholdPx = with(density) { 90.dp.toPx() } + val offsetX = remember { Animatable(0f) } + + // When entering multi-select mode snap card back to center (no swipe visible) + LaunchedEffect(isMultiSelectMode) { + if (isMultiSelectMode) offsetX.animateTo(0f, tween(200)) + } + + // Hint animation: nudge right then left, once (only first card) + LaunchedEffect(showGestureHint, isLoading) { + if (!showGestureHint || isLoading) return@LaunchedEffect + delay(800) + val nudge = with(density) { 72.dp.toPx() } + offsetX.animateTo(nudge, tween(500, easing = FastOutSlowInEasing)) + offsetX.animateTo(0f, tween(400, easing = FastOutSlowInEasing)) + delay(250) + offsetX.animateTo(-nudge, tween(500, easing = FastOutSlowInEasing)) + offsetX.animateTo(0f, tween(400, easing = FastOutSlowInEasing)) + onGestureHintShown() + } + + val hideLabel = stringResource(R.string.hide) + val patchesLabel = stringResource(R.string.patches) + val errorContainer = MaterialTheme.colorScheme.errorContainer + val onErrorContainer = MaterialTheme.colorScheme.onErrorContainer + val primaryContainer = MaterialTheme.colorScheme.primaryContainer + val onPrimaryContainer = MaterialTheme.colorScheme.onPrimaryContainer + + val leftConfig = remember(hideLabel, errorContainer, onErrorContainer) { + SwipeActionConfig( + icon = Icons.Outlined.VisibilityOff, + label = hideLabel, + containerColor = errorContainer, + contentColor = onErrorContainer + ) + } + val rightConfig = remember(patchesLabel, primaryContainer, onPrimaryContainer) { + SwipeActionConfig( + icon = Icons.Outlined.Extension, + label = patchesLabel, + containerColor = primaryContainer, + contentColor = onPrimaryContainer + ) + } Box(modifier = modifier.fillMaxWidth()) { - Crossfade( - targetState = isLoading, - animationSpec = tween(300), - label = "app_card_crossfade_${item.packageName}" - ) { loading -> - if (loading) { - AppLoadingCard(gradientColors = item.gradientColors) - } else { - if (item.installedApp != null) { - InstalledAppCard( - installedApp = item.installedApp, - packageInfo = item.packageInfo, - displayName = item.displayName, - gradientColors = item.gradientColors, - onClick = { onInstalledAppClick(item.installedApp) }, - hasUpdate = hasUpdate, - isAppDeleted = item.isDeleted, - onLongClick = { showContextMenu = true } - ) - } else { - AppButton( - packageName = item.packageName, - displayName = item.displayName, - packageInfo = item.packageInfo, - gradientColors = item.gradientColors, - onClick = onAppClick, - onLongClick = { showContextMenu = true } - ) + SwipeableCardContainer( + offsetX = offsetX, + actionThresholdPx = actionThresholdPx, + onLeftSwipe = { showHideDialog = true }, + onRightSwipe = onShowPatches, + enabled = !isMultiSelectMode, + background = { leftProgress, rightProgress -> + SwipeBackground( + leftProgress = leftProgress, + rightProgress = rightProgress, + leftConfig = leftConfig, + rightConfig = rightConfig, + modifier = Modifier + .matchParentSize() + .clip(RoundedCornerShape(24.dp)) + ) + } + ) { + SelectableAppCard( + isSelected = isSelected, + isMultiSelectMode = isMultiSelectMode + ) { + Crossfade( + targetState = isLoading, + animationSpec = tween(300), + label = "app_card_crossfade_${item.packageName}" + ) { loading -> + if (loading) { + AppLoadingCard(gradientColors = item.gradientColors) + } else { + if (item.installedApp != null) { + InstalledAppCard( + installedApp = item.installedApp, + packageInfo = item.packageInfo, + displayName = item.displayName, + gradientColors = item.gradientColors, + onClick = onAppClick, + hasUpdate = hasUpdate, + isAppDeleted = item.isDeleted, + onLongClick = { + view.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS) + onLongPress() + } + ) + } else { + AppButton( + packageName = item.packageName, + displayName = item.displayName, + packageInfo = item.packageInfo, + gradientColors = item.gradientColors, + onClick = onAppClick, + onLongClick = { + view.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS) + onLongPress() + } + ) + } + } } } } - // Hide confirmation dialog - if (showContextMenu) { + if (showHideDialog) { HideAppDialog( item = item, - onDismiss = { showContextMenu = false }, + onDismiss = { showHideDialog = false }, onHide = { onHide() - showContextMenu = false + showHideDialog = false } ) } @@ -1064,122 +1409,1022 @@ private fun DynamicAppCard( } /** - * Confirmation dialog asking user whether to hide the app. + * Animated confirmation bar that slides up from the bottom of the card list + * when the user is in multi-select (bulk-hide) mode. + * + * Shows how many apps are selected and exposes "Hide" and "Cancel" actions. */ @Composable -internal fun HideAppDialog( - item: HomeAppItem, - onDismiss: () -> Unit, - onHide: () -> Unit +private fun MultiSelectBar( + selectedCount: Int, + visible: Boolean, + onHide: () -> Unit, + onCancel: () -> Unit, + modifier: Modifier = Modifier ) { - MorpheDialog( - onDismissRequest = onDismiss, - title = stringResource(R.string.home_app_hide_title), - footer = { - MorpheDialogButtonRow( - primaryText = stringResource(R.string.hide), - primaryIcon = Icons.Outlined.VisibilityOff, - onPrimaryClick = onHide, - secondaryText = stringResource(android.R.string.cancel), - onSecondaryClick = onDismiss - ) - }, - compactPadding = true + AnimatedVisibility( + visible = visible, + enter = MorpheAnimations.springSlideUpEnter, + exit = MorpheAnimations.springSlideDownExit, + modifier = modifier ) { - Column( - modifier = Modifier.fillMaxWidth(), - verticalArrangement = Arrangement.spacedBy(20.dp) + Surface( + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 8.dp), + shape = RoundedCornerShape(24.dp), + color = MaterialTheme.colorScheme.surfaceContainerHigh, + shadowElevation = 8.dp, + tonalElevation = 4.dp ) { - // Original app card preview - AppCardLayout( - gradientColors = item.gradientColors, - enabled = true, - onClick = {}, - modifier = Modifier.fillMaxWidth() + Row( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp, vertical = 12.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(12.dp) ) { - AppCardContent( - packageName = item.packageName, - packageInfo = item.packageInfo, - displayName = item.displayName, - subtitle = stringResource(R.string.home_app_will_be_hidden), - gradientColors = item.gradientColors, - iconSource = AppDataSource.PATCHED_APK + // Selected count badge + Surface( + shape = CircleShape, + color = MaterialTheme.colorScheme.primaryContainer, + modifier = Modifier.size(36.dp) + ) { + Box(contentAlignment = Alignment.Center) { + AnimatedContent( + targetState = selectedCount, + transitionSpec = { + (fadeIn(tween(150)) + slideInVertically(tween(150)) { -it }) + .togetherWith(fadeOut(tween(100)) + slideOutVertically(tween(100)) { it }) + }, + label = "selected_count" + ) { count -> + Text( + text = count.toString(), + style = MaterialTheme.typography.labelLarge, + fontWeight = FontWeight.Bold, + color = MaterialTheme.colorScheme.onPrimaryContainer + ) + } + } + } + + // Label + Text( + text = stringResource(R.string.selected), + style = MaterialTheme.typography.bodyMedium, + fontWeight = FontWeight.Medium, + color = MaterialTheme.colorScheme.onSurface, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + modifier = Modifier.weight(1f) ) - } - // Explanation text - Text( - text = stringResource( - R.string.home_app_hide_message, - stringResource(R.string.home_app_show_hidden) - ), - style = MaterialTheme.typography.bodyLarge, - color = LocalDialogSecondaryTextColor.current, - textAlign = TextAlign.Center - ) + // Cancel + ActionPillButton( + onClick = onCancel, + icon = Icons.Outlined.Close, + contentDescription = stringResource(android.R.string.cancel) + ) + + // Hide action + ActionPillButton( + onClick = onHide, + icon = Icons.Outlined.VisibilityOff, + contentDescription = stringResource(R.string.hide), + colors = IconButtonDefaults.filledTonalIconButtonColors( + containerColor = MaterialTheme.colorScheme.errorContainer, + contentColor = MaterialTheme.colorScheme.onErrorContainer + ) + ) + } } } } /** - * Card dialog that lists hidden apps. + * Semi-transparent background that reveals contextual action icons as the user drags the card. */ @Composable -internal fun HiddenAppsDialog( - hiddenAppItems: List, - onUnhide: (String) -> Unit, - onDismiss: () -> Unit +private fun SwipeBackground( + leftProgress: Float, + rightProgress: Float, + leftConfig: SwipeActionConfig?, + rightConfig: SwipeActionConfig?, + modifier: Modifier = Modifier ) { - MorpheDialog( - onDismissRequest = onDismiss, - dismissOnClickOutside = true, - title = stringResource(R.string.home_app_hidden_apps_title), - footer = { - MorpheDialogButton( - text = stringResource(R.string.close), - onClick = onDismiss, - modifier = Modifier.fillMaxWidth() - ) - }, - compactPadding = true - ) { - if (hiddenAppItems.isEmpty()) { + Box(modifier = modifier) { + // Left edge + if (leftConfig != null && leftProgress > 0.01f) { Box( modifier = Modifier + .fillMaxHeight() .fillMaxWidth() - .padding(vertical = 24.dp), - contentAlignment = Alignment.Center - ) { - Text( - text = stringResource(R.string.home_app_no_hidden), - style = MaterialTheme.typography.bodyMedium, - color = LocalDialogSecondaryTextColor.current, - textAlign = TextAlign.Center - ) - } - } else { - Column(verticalArrangement = Arrangement.spacedBy(4.dp)) { - hiddenAppItems.forEach { item -> - // Original app card preview - AppCardLayout( - gradientColors = item.gradientColors, - enabled = true, - onClick = { onUnhide(item.packageName) }, - modifier = Modifier.fillMaxWidth() - ) { - AppCardContent( - packageName = item.packageName, - packageInfo = item.packageInfo, - displayName = item.displayName, - subtitle = stringResource(R.string.home_app_hidden_apps_hint), - gradientColors = item.gradientColors, - iconSource = AppDataSource.PATCHED_APK + .align(Alignment.CenterEnd) + .background( + Brush.horizontalGradient( + 0f to leftConfig.containerColor.copy(alpha = 0f), + 1f to leftConfig.containerColor.copy(alpha = 0.85f * leftProgress) ) - } - } - } - } - } + ), + contentAlignment = Alignment.CenterEnd + ) { + Column( + modifier = Modifier + .padding(end = 20.dp) + .graphicsLayer { alpha = leftProgress }, + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(4.dp) + ) { + Icon( + imageVector = leftConfig.icon, + contentDescription = null, + tint = leftConfig.contentColor, + modifier = Modifier.size(22.dp) + ) + Text( + text = leftConfig.label, + style = MaterialTheme.typography.labelSmall, + color = leftConfig.contentColor, + fontWeight = FontWeight.SemiBold + ) + } + } + } + + // Right edge + if (rightConfig != null && rightProgress > 0.01f) { + Box( + modifier = Modifier + .fillMaxHeight() + .fillMaxWidth() + .align(Alignment.CenterStart) + .background( + Brush.horizontalGradient( + 0f to rightConfig.containerColor.copy(alpha = 0.85f * rightProgress), + 1f to rightConfig.containerColor.copy(alpha = 0f) + ) + ), + contentAlignment = Alignment.CenterStart + ) { + Column( + modifier = Modifier + .padding(start = 20.dp) + .graphicsLayer { alpha = rightProgress }, + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(4.dp) + ) { + Icon( + imageVector = rightConfig.icon, + contentDescription = null, + tint = rightConfig.contentColor, + modifier = Modifier.size(22.dp) + ) + Text( + text = rightConfig.label, + style = MaterialTheme.typography.labelSmall, + color = rightConfig.contentColor, + fontWeight = FontWeight.SemiBold + ) + } + } + } + } +} + +/** + * Shared container that handles horizontal swipe gestures and drives the + * [SwipeBackground] reveal animation. + */ +@Composable +private fun SwipeableCardContainer( + modifier: Modifier = Modifier, + offsetX: Animatable, + actionThresholdPx: Float, + onLeftSwipe: () -> Unit, + onRightSwipe: () -> Unit, + leftHaptic: Int = HapticFeedbackConstants.LONG_PRESS, + rightHaptic: Int = HapticFeedbackConstants.VIRTUAL_KEY, + enabled: Boolean = true, + background: @Composable BoxScope.(leftProgress: Float, rightProgress: Float) -> Unit, + content: @Composable () -> Unit +) { + val view = LocalView.current + val scope = rememberCoroutineScope() + + // Progress values for background reveal [0..1] + val leftProgress by remember { derivedStateOf { (-offsetX.value / actionThresholdPx).coerceIn(0f, 1f) } } + val rightProgress by remember { derivedStateOf { (offsetX.value / actionThresholdPx).coerceIn(0f, 1f) } } + + Box(modifier = modifier.fillMaxWidth()) { + background(leftProgress, rightProgress) + + Box( + modifier = Modifier + .graphicsLayer { translationX = offsetX.value } + .then( + if (enabled) Modifier.pointerInput(Unit) { + detectHorizontalDragGestures( + onDragEnd = { + scope.launch { + when { + offsetX.value < -actionThresholdPx -> { + view.performHapticFeedback(leftHaptic) + offsetX.animateTo(0f, tween(200)) + onLeftSwipe() + } + offsetX.value > actionThresholdPx -> { + view.performHapticFeedback(rightHaptic) + offsetX.animateTo(0f, tween(200)) + onRightSwipe() + } + else -> offsetX.animateTo( + 0f, + spring( + dampingRatio = Spring.DampingRatioMediumBouncy, + stiffness = Spring.StiffnessMedium + ) + ) + } + } + }, + onDragCancel = { + scope.launch { + offsetX.animateTo( + 0f, + spring( + dampingRatio = Spring.DampingRatioMediumBouncy, + stiffness = Spring.StiffnessMedium + ) + ) + } + }, + onHorizontalDrag = { change, dragAmount -> + change.consume() + scope.launch { + val clamped = (offsetX.value + dragAmount) + .coerceIn(-actionThresholdPx * 1.5f, actionThresholdPx * 1.5f) + offsetX.snapTo(clamped) + } + } + ) + } else Modifier + ) + ) { + content() + } + } +} + +/** + * App card for hidden apps shown in search results. + * - Swipe LEFT → Patches dialog + * - Swipe RIGHT → Unhide + * + * Rendered at reduced opacity to signal the hidden state. + */ +@Composable +private fun HiddenSearchAppCard( + modifier: Modifier = Modifier, + item: HomeAppItem, + onUnhide: () -> Unit, + onAppClick: () -> Unit, + onShowPatches: () -> Unit +) { + val density = LocalDensity.current + val actionThresholdPx = with(density) { 90.dp.toPx() } + val offsetX = remember { Animatable(0f) } + + val patchesLabel = stringResource(R.string.patches) + val unhideLabel = stringResource(R.string.unhide) + val primaryContainer = MaterialTheme.colorScheme.primaryContainer + val onPrimaryContainer = MaterialTheme.colorScheme.onPrimaryContainer + val tertiaryContainer = MaterialTheme.colorScheme.tertiaryContainer + val onTertiaryContainer = MaterialTheme.colorScheme.onTertiaryContainer + + val leftConfig = remember(unhideLabel, tertiaryContainer, onTertiaryContainer) { + SwipeActionConfig( + icon = Icons.Outlined.Visibility, + label = unhideLabel, + containerColor = tertiaryContainer, + contentColor = onTertiaryContainer + ) + } + val rightConfig = remember(patchesLabel, primaryContainer, onPrimaryContainer) { + SwipeActionConfig( + icon = Icons.Outlined.Extension, + label = patchesLabel, + containerColor = primaryContainer, + contentColor = onPrimaryContainer + ) + } + + Box( + modifier = modifier + .fillMaxWidth() + .graphicsLayer { alpha = 0.6f } + ) { + SwipeableCardContainer( + offsetX = offsetX, + actionThresholdPx = actionThresholdPx, + onLeftSwipe = onUnhide, + onRightSwipe = onShowPatches, + leftHaptic = HapticFeedbackConstants.LONG_PRESS, + rightHaptic = HapticFeedbackConstants.VIRTUAL_KEY, + background = { leftProgress, rightProgress -> + SwipeBackground( + leftProgress = leftProgress, + rightProgress = rightProgress, + leftConfig = leftConfig, + rightConfig = rightConfig, + modifier = Modifier + .matchParentSize() + .clip(RoundedCornerShape(24.dp)) + ) + } + ) { + if (item.installedApp != null) { + InstalledAppCard( + installedApp = item.installedApp, + packageInfo = item.packageInfo, + displayName = item.displayName, + gradientColors = item.gradientColors, + onClick = onAppClick, + hasUpdate = item.hasUpdate, + isAppDeleted = item.isDeleted, + onLongClick = {} + ) + } else { + AppButton( + packageName = item.packageName, + displayName = item.displayName, + packageInfo = item.packageInfo, + gradientColors = item.gradientColors, + onClick = onAppClick, + onLongClick = {} + ) + } + } + } +} + +/** + * Dialog that shows available patches for a specific app. + * Shown when the user swipes right on a home app card. + * Uses the shared [PatchItemCard] component from [BundlePatchesDialog] + * to display rich patch info with search and (multi-bundle) filter support. + */ +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun AppPatchesDialog( + item: HomeAppItem, + patchesByBundle: Map>, + bundleNames: Map, + onDismiss: () -> Unit +) { + // Flatten to a list of (bundleUid, patch). + // Bundle ordering: bundles with at least one specific patch come first (by name), + // then bundles with only universal patches (by name). + // Within each bundle: specific patches first (alphabetically), universal patches last (alphabetically). + val allPatches = remember(patchesByBundle, bundleNames) { + patchesByBundle.entries + .sortedWith( + compareBy( + { (_, patches) -> patches.all { it.compatiblePackages == null } }, + { (uid, _) -> bundleNames[uid] ?: uid.toString() } + ) + ) + .flatMap { (uid, patches) -> + val (universal, specific) = patches.partition { it.compatiblePackages == null } + (specific.sortedBy { it.name } + universal.sortedBy { it.name }) + .map { patch -> uid to patch } + } + } + + val isMultiBundle = patchesByBundle.size > 1 + + // Per-bundle accent color for multi-bundle mode only. + // Generated deterministically from uid via multiplicative hash → HSL, + // so the same uid always produces the same color. + // Returns null for single-bundle (no coloring needed). + val bundleAccentColors: Map = remember(patchesByBundle, isMultiBundle) { + if (!isMultiBundle) return@remember emptyMap() + patchesByBundle.keys.associateWith { uid -> + val hue = ((uid.hashCode() * 2654435761L) and 0xFFFFFFFFL).toFloat() % 360f + Color.hsl(hue = hue, saturation = 0.55f, lightness = 0.60f) + } + } + var searchQuery by remember { mutableStateOf("") } + var selectedBundle by remember { mutableStateOf(null) } + val showFilterSheet = remember { mutableStateOf(false) } + val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true) + + val filteredPatches = remember(allPatches, searchQuery, selectedBundle) { + allPatches.filter { (uid, patch) -> + val bundleMatch = selectedBundle == null || uid == selectedBundle + val queryMatch = searchQuery.isBlank() || + patch.name.contains(searchQuery, ignoreCase = true) || + patch.description?.contains(searchQuery, ignoreCase = true) == true + bundleMatch && queryMatch + } + } + + val isFiltering = searchQuery.isNotBlank() || selectedBundle != null + val totalCount = allPatches.size + + // Pre-compute per-bundle markers once so items{} can do O(1) lookups instead of O(n) scans + val firstPatchPerBundle: Map = remember(filteredPatches) { + buildMap { + filteredPatches.forEach { (uid, patch) -> putIfAbsent(uid, patch) } + } + } + val firstUniversalPerBundle: Map = remember(filteredPatches) { + buildMap { + filteredPatches.forEach { (uid, patch) -> + if (patch.compatiblePackages == null) putIfAbsent(uid, patch) + } + } + } + val bundlesWithSpecificPatches: Set = remember(filteredPatches) { + filteredPatches + .filter { (_, patch) -> patch.compatiblePackages != null } + .map { it.first } + .toSet() + } + + MorpheDialog( + onDismissRequest = onDismiss, + dismissOnClickOutside = true, + title = null, + compactPadding = true, + scrollable = false, + footer = { + MorpheDialogOutlinedButton( + text = stringResource(R.string.close), + onClick = onDismiss, + modifier = Modifier.fillMaxWidth() + ) + } + ) { + LazyColumn( + modifier = Modifier.fillMaxWidth(), + verticalArrangement = Arrangement.spacedBy(8.dp) + ) { + // App header + item { + Surface( + modifier = Modifier.fillMaxWidth(), + shape = RoundedCornerShape(16.dp), + color = MaterialTheme.colorScheme.primaryContainer.copy(alpha = 0.3f) + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(16.dp), + horizontalArrangement = Arrangement.spacedBy(14.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Surface( + shape = CircleShape, + color = MaterialTheme.colorScheme.primary.copy(alpha = 0.25f), + modifier = Modifier.size(56.dp) + ) { + Box(contentAlignment = Alignment.Center) { + Icon( + imageVector = Icons.Outlined.Extension, + contentDescription = null, + tint = MaterialTheme.colorScheme.primary, + modifier = Modifier.size(28.dp) + ) + } + } + + Column( + modifier = Modifier.weight(1f), + verticalArrangement = Arrangement.spacedBy(4.dp) + ) { + Text( + text = item.displayName, + style = MaterialTheme.typography.titleLarge, + fontWeight = FontWeight.Bold, + color = LocalDialogTextColor.current, + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + Row( + horizontalArrangement = Arrangement.spacedBy(6.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Icon( + imageVector = Icons.Outlined.Widgets, + contentDescription = null, + tint = MaterialTheme.colorScheme.primary, + modifier = Modifier.size(16.dp) + ) + val countText = if (isFiltering) + "${filteredPatches.size}/$totalCount" + else + "$totalCount" + val patchesLabel = stringResource(R.string.patches).lowercase() + AnimatedContent( + targetState = countText, + transitionSpec = { + (fadeIn(tween(200)) + slideInVertically(tween(200)) { -it / 2 }) + .togetherWith(fadeOut(tween(150)) + slideOutVertically(tween(150)) { it / 2 }) + }, + label = "app_patch_count" + ) { count -> + Text( + text = "$count $patchesLabel", + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.primary, + fontWeight = FontWeight.Medium + ) + } + } + } + } + } + } + + // Search + filter row (filter button visible only for multi-bundle) + stickyHeader { + Surface( + modifier = Modifier.fillMaxWidth(), + color = MaterialTheme.colorScheme.surface + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(bottom = 4.dp), + horizontalArrangement = Arrangement.spacedBy(8.dp), + verticalAlignment = Alignment.Bottom + ) { + MorpheDialogTextField( + value = searchQuery, + onValueChange = { searchQuery = it }, + label = { Text(stringResource(R.string.expert_mode_search)) }, + leadingIcon = { + Icon( + imageVector = Icons.Outlined.Search, + contentDescription = null + ) + }, + showClearButton = true, + modifier = Modifier.weight(1f) + ) + + if (isMultiBundle) { + FilledTonalIconButton( + onClick = { showFilterSheet.value = true }, + modifier = Modifier.padding(bottom = 4.dp), + colors = IconButtonDefaults.filledTonalIconButtonColors( + containerColor = if (selectedBundle != null) + MaterialTheme.colorScheme.primaryContainer + else + MaterialTheme.colorScheme.surfaceVariant, + contentColor = if (selectedBundle != null) + MaterialTheme.colorScheme.onPrimaryContainer + else + MaterialTheme.colorScheme.onSurfaceVariant + ) + ) { + Icon( + imageVector = Icons.Outlined.FilterList, + contentDescription = stringResource(R.string.filter), + modifier = Modifier.size(20.dp) + ) + } + } + } + } + } + + // Active bundle filter badge + empty state + item(key = "filter_badges_and_empty") { + Column { + AnimatedVisibility( + visible = selectedBundle != null, + enter = MorpheAnimations.expandFadeEnter, + exit = MorpheAnimations.shrinkFadeExit + ) { + selectedBundle?.let { uid -> + FlowRow( + modifier = Modifier.padding(bottom = 4.dp), + horizontalArrangement = Arrangement.spacedBy(8.dp) + ) { + InputChip( + selected = true, + onClick = { selectedBundle = null }, + label = { Text(bundleNames[uid] ?: uid.toString()) }, + trailingIcon = { + Icon( + imageVector = Icons.Outlined.Close, + contentDescription = stringResource(R.string.remove), + modifier = Modifier.size(16.dp) + ) + } + ) + } + } + } + + AnimatedVisibility( + visible = filteredPatches.isEmpty(), + enter = MorpheAnimations.fadeScaleIn, + exit = MorpheAnimations.fadeScaleOut + ) { + Box( + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 48.dp), + contentAlignment = Alignment.Center + ) { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(12.dp) + ) { + Icon( + imageVector = Icons.Outlined.SearchOff, + contentDescription = null, + modifier = Modifier.size(48.dp), + tint = MaterialTheme.colorScheme.onSurfaceVariant + ) + Text( + text = stringResource(R.string.expert_mode_no_results), + style = MaterialTheme.typography.bodyLarge, + color = MaterialTheme.colorScheme.onSurfaceVariant, + textAlign = TextAlign.Center + ) + } + } + } + } + } + + // Patch cards + items( + filteredPatches, + key = { (uid, patch) -> + "$uid:${patch.name}:${patch.compatiblePackages?.joinToString { it.packageName.orEmpty() }.orEmpty()}" + } + ) { entry -> + val uid: Int = entry.first + val patch: PatchInfo = entry.second + val isUniversal = patch.compatiblePackages == null + Column( + modifier = Modifier.animateItem( + fadeInSpec = tween(220), + fadeOutSpec = tween(180), + placementSpec = spring(stiffness = 400f, dampingRatio = 0.8f) + ) + ) { + // Bundle section label - only for multi-bundle, at first patch of each bundle + if (isMultiBundle) { + val isFirstOfBundle = firstPatchPerBundle[uid] == patch + if (isFirstOfBundle) { + InfoBadge( + text = bundleNames[uid] ?: uid.toString(), + style = InfoBadgeStyle.Primary, + icon = Icons.Outlined.Layers, + isExpanded = true, + modifier = Modifier.padding(bottom = 6.dp, top = 8.dp) + ) + } + } + + // Universal patches divider - shown before the first universal patch of each bundle + val isFirstUniversalOfBundle = isUniversal && firstUniversalPerBundle[uid] == patch + if (isFirstUniversalOfBundle) { + val hasSpecificAbove = uid in bundlesWithSpecificPatches + Row( + modifier = Modifier + .fillMaxWidth() + .padding(top = if (hasSpecificAbove) 8.dp else 0.dp, bottom = 4.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(6.dp) + ) { + Icon( + imageVector = Icons.Outlined.Public, + contentDescription = null, + tint = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.size(14.dp) + ) + Text( + text = stringResource(R.string.expert_mode_universal_patches), + style = MaterialTheme.typography.labelMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + HorizontalDivider( + modifier = Modifier.weight(1f), + color = MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.5f), + thickness = 0.5.dp + ) + } + } + + PatchItemCard( + patch = patch, + saveStateKey = "app_patches_${item.packageName}_$uid", + accentColor = bundleAccentColors[uid], + ) + } + } + } + } + + // Bundle filter bottom sheet (multi-bundle only) + if (showFilterSheet.value && isMultiBundle) { + MorpheBottomSheet( + onDismissRequest = { showFilterSheet.value = false }, + sheetState = sheetState, + containerColor = MaterialTheme.colorScheme.surfaceContainerHigh + ) { + Column( + modifier = Modifier + .fillMaxWidth() + .navigationBarsPadding() + .padding(horizontal = 16.dp) + ) { + Text( + text = stringResource(R.string.filter), + style = MaterialTheme.typography.titleLarge, + fontWeight = FontWeight.Bold, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + Spacer(Modifier.height(8.dp)) + FlowRow( + modifier = Modifier.padding(bottom = 16.dp), + horizontalArrangement = Arrangement.spacedBy(8.dp) + ) { + // "All" chip + FilterChip( + selected = selectedBundle == null, + onClick = { selectedBundle = null }, + label = { Text(stringResource(R.string.all)) }, + leadingIcon = if (selectedBundle == null) { + { Icon(Icons.Outlined.DoneAll, null, Modifier.size(16.dp)) } + } else null + ) + // Per-bundle chips + bundleNames.entries + .sortedBy { it.value } + .forEach { (uid, name) -> + val isSelected = uid == selectedBundle + FilterChip( + selected = isSelected, + onClick = { + selectedBundle = if (isSelected) null else uid + showFilterSheet.value = false + }, + label = { Text(name) }, + leadingIcon = if (isSelected) { + { Icon(Icons.Outlined.Done, null, Modifier.size(16.dp)) } + } else null + ) + } + } + } + } + } +} + +/** + * Confirmation dialog asking user whether to hide the app. + */ +@Composable +internal fun HideAppDialog( + item: HomeAppItem, + onDismiss: () -> Unit, + onHide: () -> Unit +) { + MorpheDialog( + onDismissRequest = onDismiss, + title = stringResource(R.string.home_app_hide_title), + footer = { + MorpheDialogButtonRow( + primaryText = stringResource(R.string.hide), + primaryIcon = Icons.Outlined.VisibilityOff, + onPrimaryClick = onHide, + secondaryText = stringResource(android.R.string.cancel), + onSecondaryClick = onDismiss + ) + }, + compactPadding = true + ) { + Column( + modifier = Modifier.fillMaxWidth(), + verticalArrangement = Arrangement.spacedBy(20.dp) + ) { + // Original app card preview + AppCardLayout( + gradientColors = item.gradientColors, + enabled = true, + onClick = {}, + modifier = Modifier.fillMaxWidth() + ) { + AppCardContent( + packageName = item.packageName, + packageInfo = item.packageInfo, + displayName = item.displayName, + subtitle = stringResource(R.string.home_app_will_be_hidden), + gradientColors = item.gradientColors, + iconSource = AppDataSource.PATCHED_APK + ) + } + + // Explanation text + Text( + text = stringResource( + R.string.home_app_hide_message, + stringResource(R.string.home_app_show_hidden) + ), + style = MaterialTheme.typography.bodyLarge, + color = LocalDialogSecondaryTextColor.current, + textAlign = TextAlign.Center + ) + } + } +} + +/** + * Dialog listing all hidden apps. + * + * Swipe gestures (disabled in multi-select mode): + * - Swipe LEFT → Patches dialog + * - Swipe RIGHT → Unhide + * + * Long-press enters multi-select; bulk unhide via footer button. + */ +@Composable +internal fun HiddenAppsDialog( + hiddenAppItems: List, + onUnhide: (String) -> Unit, + onUnhideMultiple: (Set) -> Unit = {}, + onShowPatches: (HomeAppItem) -> Unit, + onDismiss: () -> Unit +) { + var selectedPackages by remember { mutableStateOf(emptySet()) } + val isMultiSelectMode = selectedPackages.isNotEmpty() + + // Clear selection when items change + LaunchedEffect(hiddenAppItems) { + selectedPackages = selectedPackages.filter { pkg -> + hiddenAppItems.any { it.packageName == pkg } + }.toSet() + } + + val view = LocalView.current + val density = LocalDensity.current + val actionThresholdPx = with(density) { 90.dp.toPx() } + + val patchesLabel = stringResource(R.string.patches) + val unhideLabel = stringResource(R.string.unhide) + val primaryContainer = MaterialTheme.colorScheme.primaryContainer + val onPrimaryContainer = MaterialTheme.colorScheme.onPrimaryContainer + val tertiaryContainer = MaterialTheme.colorScheme.tertiaryContainer + val onTertiaryContainer = MaterialTheme.colorScheme.onTertiaryContainer + + val leftConfig = remember(unhideLabel, tertiaryContainer, onTertiaryContainer) { + SwipeActionConfig( + icon = Icons.Outlined.Visibility, + label = unhideLabel, + containerColor = tertiaryContainer, + contentColor = onTertiaryContainer + ) + } + val rightConfig = remember(patchesLabel, primaryContainer, onPrimaryContainer) { + SwipeActionConfig( + icon = Icons.Outlined.Extension, + label = patchesLabel, + containerColor = primaryContainer, + contentColor = onPrimaryContainer + ) + } + + MorpheDialog( + onDismissRequest = { + if (isMultiSelectMode) selectedPackages = emptySet() + else onDismiss() + }, + dismissOnClickOutside = !isMultiSelectMode, + title = stringResource(R.string.home_app_hidden_apps_title), + footer = { + if (isMultiSelectMode) { + MorpheDialogButtonRow( + primaryText = pluralStringResource( + R.plurals.home_app_show_selected, + selectedPackages.size, + selectedPackages.size.toString() + ), + primaryIcon = Icons.Outlined.Visibility, + onPrimaryClick = { + onUnhideMultiple(selectedPackages) + selectedPackages = emptySet() + }, + secondaryText = stringResource(android.R.string.cancel), + onSecondaryClick = { selectedPackages = emptySet() } + ) + } else { + MorpheDialogOutlinedButton( + text = stringResource(R.string.close), + onClick = onDismiss, + modifier = Modifier.fillMaxWidth() + ) + } + }, + compactPadding = true, + scrollable = false + ) { + if (hiddenAppItems.isEmpty()) { + MorpheEmptyState( + icon = Icons.Outlined.Visibility, + title = stringResource(R.string.home_app_no_hidden) + ) + } else { + LazyColumn( + modifier = Modifier.fillMaxWidth(), + verticalArrangement = Arrangement.spacedBy(16.dp) + ) { + items( + items = hiddenAppItems, + key = { it.packageName } + ) { item -> + val isSelected = item.packageName in selectedPackages + val offsetX = remember(item.packageName) { Animatable(0f) } + + // Snap card back when entering multi-select + LaunchedEffect(isMultiSelectMode) { + if (isMultiSelectMode) offsetX.animateTo(0f, tween(200)) + } + + SelectableAppCard( + modifier = Modifier.animateItem( + fadeInSpec = tween(220), + fadeOutSpec = tween(180), + placementSpec = spring(stiffness = 400f, dampingRatio = 0.8f) + ), + isSelected = isSelected, + isMultiSelectMode = isMultiSelectMode + ) { + SwipeableCardContainer( + offsetX = offsetX, + actionThresholdPx = actionThresholdPx, + onLeftSwipe = { onUnhide(item.packageName) }, + onRightSwipe = { onShowPatches(item) }, + leftHaptic = HapticFeedbackConstants.LONG_PRESS, + rightHaptic = HapticFeedbackConstants.VIRTUAL_KEY, + enabled = !isMultiSelectMode, + background = { leftProgress, rightProgress -> + SwipeBackground( + leftProgress = leftProgress, + rightProgress = rightProgress, + leftConfig = leftConfig, + rightConfig = rightConfig, + modifier = Modifier + .matchParentSize() + .clip(RoundedCornerShape(24.dp)) + ) + } + ) { + AppCardLayout( + gradientColors = item.gradientColors, + enabled = true, + onClick = { + if (isMultiSelectMode) { + selectedPackages = if (isSelected) + selectedPackages - item.packageName + else + selectedPackages + item.packageName + } else { + onUnhide(item.packageName) + } + }, + onLongClick = { + view.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS) + selectedPackages = if (isSelected) + selectedPackages - item.packageName + else + selectedPackages + item.packageName + }, + modifier = Modifier.fillMaxWidth() + ) { + AppCardContent( + packageName = item.packageName, + packageInfo = item.packageInfo, + displayName = item.displayName, + subtitle = if (isMultiSelectMode) null + else stringResource(R.string.home_app_hidden_apps_hint), + gradientColors = item.gradientColors, + iconSource = AppDataSource.PATCHED_APK + ) + } + } + } + } + } + } + } } /** @@ -1390,8 +2635,8 @@ fun InstalledAppCard( AnimatedVisibility( visible = hasUpdate && !isAppDeleted, - enter = fadeIn(tween(MorpheDefaults.ANIMATION_DURATION)) + expandHorizontally(tween(MorpheDefaults.ANIMATION_DURATION)), - exit = fadeOut(tween(MorpheDefaults.ANIMATION_DURATION)) + shrinkHorizontally(tween(MorpheDefaults.ANIMATION_DURATION)) + enter = MorpheAnimations.expandHorizFadeIn, + exit = MorpheAnimations.shrinkHorizFadeOut ) { GlassChip( text = stringResource(R.string.update), @@ -1464,11 +2709,30 @@ fun AppButton( fun OtherAppsSection( onClick: () -> Unit, modifier: Modifier = Modifier +) { + HomeGlassPillButton( + onClick = onClick, + modifier = modifier.padding(bottom = 12.dp), + text = stringResource(R.string.home_other_apps) + ) +} + +/** + * Shared frosted-glass pill button used by [OtherAppsSection] and [ShowHiddenAppsButton]. + * + * Renders a rounded pill with semi-transparent surface background, border, press-scale + * animation, and haptic feedback. Content is either icon+text or text-only. + */ +@Composable +private fun HomeGlassPillButton( + onClick: () -> Unit, + text: String, + modifier: Modifier = Modifier, + icon: ImageVector? = null ) { val view = LocalView.current val shape = RoundedCornerShape(20.dp) val isDark = isSystemInDarkTheme() - val backgroundAlpha = if (isDark) 0.35f else 0.6f val borderAlpha = if (isDark) 0.4f else 0.6f @@ -1478,26 +2742,20 @@ fun OtherAppsSection( targetValue = if (isPressed) 0.97f else 1f, animationSpec = spring( dampingRatio = Spring.DampingRatioMediumBouncy, - stiffness = Spring.StiffnessMedium + stiffness = Spring.StiffnessMedium ), - label = "other_apps_press_scale" + label = "pill_press_scale" ) Box( modifier = modifier .fillMaxWidth() - .padding(bottom = 12.dp) .height(48.dp) - .graphicsLayer { scaleX = scale; scaleY = scale } + .graphicsLayer { scaleX = scale; scaleY = scale; clip = true } .clip(shape) - .background( - color = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = backgroundAlpha) - ) + .background(MaterialTheme.colorScheme.surfaceVariant.copy(alpha = backgroundAlpha)) .border( - BorderStroke( - width = 1.dp, - color = MaterialTheme.colorScheme.outline.copy(alpha = borderAlpha) - ), + BorderStroke(1.dp, MaterialTheme.colorScheme.outline.copy(alpha = borderAlpha)), shape = shape ) .clickable( @@ -1509,13 +2767,25 @@ fun OtherAppsSection( }, contentAlignment = Alignment.Center ) { - Text( - text = stringResource(R.string.home_other_apps), - style = MaterialTheme.typography.titleMedium, - fontWeight = FontWeight.SemiBold, - fontSize = 18.sp, - color = MaterialTheme.colorScheme.onSurfaceVariant - ) + Row( + horizontalArrangement = Arrangement.spacedBy(8.dp), + verticalAlignment = Alignment.CenterVertically + ) { + if (icon != null) { + Icon( + imageVector = icon, + contentDescription = null, + modifier = Modifier.size(16.dp), + tint = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + Text( + text = text, + style = MaterialTheme.typography.titleMedium, + fontWeight = FontWeight.SemiBold, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } } } @@ -1533,12 +2803,11 @@ fun OtherAppsSection( @OptIn(ExperimentalFoundationApi::class) @Composable private fun AppCardLayout( + modifier: Modifier = Modifier, gradientColors: List, enabled: Boolean, onClick: () -> Unit, onLongClick: (() -> Unit)? = null, - @SuppressLint("ModifierParameter") - modifier: Modifier = Modifier, content: @Composable RowScope.() -> Unit ) { val shape = RoundedCornerShape(24.dp) diff --git a/app/src/main/java/app/morphe/manager/ui/screen/home/SourceManagementDialogs.kt b/app/src/main/java/app/morphe/manager/ui/screen/home/SourceManagementDialogs.kt index 96992f754..4a1c0f76d 100644 --- a/app/src/main/java/app/morphe/manager/ui/screen/home/SourceManagementDialogs.kt +++ b/app/src/main/java/app/morphe/manager/ui/screen/home/SourceManagementDialogs.kt @@ -5,6 +5,9 @@ package app.morphe.manager.ui.screen.home +import android.annotation.SuppressLint +import android.graphics.Color.argb +import android.graphics.Color.colorToHSV import androidx.compose.animation.* import androidx.compose.animation.core.animateFloatAsState import androidx.compose.animation.core.spring @@ -271,7 +274,7 @@ fun BundleDeleteConfirmDialog( } /** - * Dialog for renaming a bundle + * Dialog for renaming a bundle. */ @Composable fun RenameBundleDialog( @@ -347,6 +350,7 @@ fun RenameBundleDialog( /** * Dialog displaying patches from a bundle with search field and chips. */ +@SuppressLint("LocalContextGetResourceValueCall") @OptIn(ExperimentalMaterial3Api::class) @Composable fun BundlePatchesDialog( @@ -393,6 +397,16 @@ fun BundlePatchesDialog( .sortedBy { it.name } } + // Per-patch accent color: first non-null appIconColor across all compatible packages, + // converted from 0xRRGGBB to a full-opacity Compose Color. Null falls back to surfaceVariant. + val patchAccentColors: Map = remember(patches) { + patches.associate { patch -> + val rgb = patch.compatiblePackages + ?.firstNotNullOfOrNull { it.appIconColor } + patch.name to if (rgb != null) Color(rgb or (0xFF shl 24)) else Color.Unspecified + } + } + val isFiltering = searchQuery.isNotBlank() || selectedPackages.isNotEmpty() MorpheDialog( @@ -560,8 +574,8 @@ fun BundlePatchesDialog( Column { AnimatedVisibility( visible = selectedPackages.isNotEmpty(), - enter = expandVertically(tween(MorpheDefaults.ANIMATION_DURATION)) + fadeIn(tween(MorpheDefaults.ANIMATION_DURATION)), - exit = shrinkVertically(tween(MorpheDefaults.ANIMATION_DURATION)) + fadeOut(tween(MorpheDefaults.ANIMATION_DURATION)) + enter = MorpheAnimations.expandFadeEnter, + exit = MorpheAnimations.shrinkFadeExit ) { FlowRow( modifier = Modifier.padding(bottom = 4.dp), @@ -587,8 +601,8 @@ fun BundlePatchesDialog( AnimatedVisibility( visible = filteredPatches.isEmpty(), - enter = fadeIn(tween(MorpheDefaults.ANIMATION_DURATION)) + scaleIn(tween(MorpheDefaults.ANIMATION_DURATION), initialScale = 0.92f), - exit = fadeOut(tween(MorpheDefaults.ANIMATION_DURATION)) + scaleOut(tween(MorpheDefaults.ANIMATION_DURATION), targetScale = 0.92f) + enter = MorpheAnimations.fadeScaleIn, + exit = MorpheAnimations.fadeScaleOut ) { Box( modifier = Modifier @@ -626,22 +640,15 @@ fun BundlePatchesDialog( } ) { patch -> val context = LocalContext.current - var expandVersions by rememberSaveable(src.uid, patch.name, "versions") { - mutableStateOf(false) - } - var expandOptions by rememberSaveable(src.uid, patch.name, "options") { - mutableStateOf(false) - } - + val accentColor = patchAccentColors[patch.name] + ?.takeIf { it != Color.Unspecified } PatchItemCard( patch = patch, - expandVersions = expandVersions, - onExpandVersions = { expandVersions = !expandVersions }, - expandOptions = expandOptions, - onExpandOptions = { expandOptions = !expandOptions }, + saveStateKey = "bundle_${src.uid}", onExpertBadgeClick = if (!patch.include) { { context.toast(context.getString(R.string.sources_patch_expert_badge_tooltip)) } } else null, + accentColor = accentColor, modifier = Modifier.animateItem( fadeInSpec = tween(220), fadeOutSpec = tween(180), @@ -715,39 +722,61 @@ fun BundlePatchesDialog( } /** - * Patch item card + * Patch item card. */ @OptIn(ExperimentalLayoutApi::class) @Composable -private fun PatchItemCard( +fun PatchItemCard( + modifier: Modifier = Modifier, patch: PatchInfo, - expandVersions: Boolean, - onExpandVersions: () -> Unit, - expandOptions: Boolean, - onExpandOptions: () -> Unit, + saveStateKey: String, onExpertBadgeClick: (() -> Unit)? = null, - modifier: Modifier = Modifier + accentColor: Color? = null ) { val textColor = LocalDialogTextColor.current val secondaryColor = LocalDialogSecondaryTextColor.current + var expandVersions by rememberSaveable(saveStateKey, patch.name, "versions") { + mutableStateOf(false) + } + var expandOptions by rememberSaveable(saveStateKey, patch.name, "options") { + mutableStateOf(false) + } + val rotationAngle by animateFloatAsState( targetValue = if (expandOptions) 180f else 0f, animationSpec = tween(300), label = "expand_rotation" ) + // Cache the card background color: colorToHSV is a native call that allocates a FloatArray + val cardColor = remember(accentColor) { + if (accentColor != null) { + val hsv = FloatArray(3) + colorToHSV( + argb( + 255, + (accentColor.red * 255).toInt(), + (accentColor.green * 255).toInt(), + (accentColor.blue * 255).toInt() + ), + hsv + ) + Color.hsl(hue = hsv[0], saturation = 0.35f, lightness = 0.55f, alpha = 0.2f) + } else null + } + Surface( modifier = modifier .fillMaxWidth() .clip(RoundedCornerShape(14.dp)) .then( if (!patch.options.isNullOrEmpty()) { - Modifier.clickable(onClick = onExpandOptions) + Modifier.clickable { expandOptions = !expandOptions } } else Modifier ), shape = RoundedCornerShape(14.dp), - color = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.3f) + color = cardColor ?: MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.3f) ) { Column( modifier = Modifier.padding(16.dp), @@ -868,7 +897,7 @@ private fun PatchItemCard( modifier = Modifier .align(Alignment.CenterVertically) .clip(RoundedCornerShape(6.dp)) - .clickable(onClick = onExpandVersions) + .clickable { expandVersions = !expandVersions } ) } } @@ -892,8 +921,8 @@ private fun PatchItemCard( if (!patch.options.isNullOrEmpty()) { AnimatedVisibility( visible = expandOptions, - enter = expandVertically(tween(MorpheDefaults.ANIMATION_DURATION)) + fadeIn(tween(MorpheDefaults.ANIMATION_DURATION)), - exit = shrinkVertically(tween(MorpheDefaults.ANIMATION_DURATION)) + fadeOut(tween(MorpheDefaults.ANIMATION_DURATION)) + enter = MorpheAnimations.expandFadeEnter, + exit = MorpheAnimations.shrinkFadeExit ) { Column( modifier = Modifier.padding(top = 4.dp), @@ -1129,7 +1158,7 @@ private fun String.sanitizePatchChangelogMarkdown(): String = } /** - * Normalizes a URL by adding https:// if no protocol is specified + * Normalizes a URL by adding https:// if no protocol is specified. */ private fun normalizeUrl(url: String): String { val trimmed = url.trim() diff --git a/app/src/main/java/app/morphe/manager/ui/screen/home/SourceManagementSheet.kt b/app/src/main/java/app/morphe/manager/ui/screen/home/SourceManagementSheet.kt index bf757572b..1f94b3429 100644 --- a/app/src/main/java/app/morphe/manager/ui/screen/home/SourceManagementSheet.kt +++ b/app/src/main/java/app/morphe/manager/ui/screen/home/SourceManagementSheet.kt @@ -11,7 +11,6 @@ import androidx.compose.animation.* import androidx.compose.animation.core.Spring import androidx.compose.animation.core.animateFloatAsState import androidx.compose.animation.core.spring -import androidx.compose.animation.core.tween import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.Image import androidx.compose.foundation.clickable @@ -445,8 +444,8 @@ private fun BundleManagementCard( // Expanded content AnimatedVisibility( visible = expanded, - enter = expandVertically(tween(MorpheDefaults.ANIMATION_DURATION)), - exit = shrinkVertically(tween(MorpheDefaults.ANIMATION_DURATION)) + enter = MorpheAnimations.expandVertEnter, + exit = MorpheAnimations.shrinkVertExit ) { Column( modifier = Modifier @@ -541,8 +540,8 @@ private fun BundleManagementCard( AnimatedVisibility( visible = hasExperimentalVersions && onExperimentalVersionsToggle != null && (onPrereleasesToggle == null || currentUsePrerelease), - enter = expandVertically(tween(MorpheDefaults.ANIMATION_DURATION)) + fadeIn(tween(MorpheDefaults.ANIMATION_DURATION)), - exit = shrinkVertically(tween(MorpheDefaults.ANIMATION_DURATION)) + fadeOut(tween(MorpheDefaults.ANIMATION_DURATION)) + enter = MorpheAnimations.expandFadeEnter, + exit = MorpheAnimations.shrinkFadeExit ) { Row( modifier = Modifier @@ -751,8 +750,8 @@ private fun BundleCardHeader( // Disabled badge AnimatedVisibility( visible = !enabled, - enter = fadeIn(tween(MorpheDefaults.ANIMATION_DURATION)) + expandHorizontally(tween(MorpheDefaults.ANIMATION_DURATION)), - exit = fadeOut(tween(MorpheDefaults.ANIMATION_DURATION)) + shrinkHorizontally(tween(MorpheDefaults.ANIMATION_DURATION)) + enter = MorpheAnimations.expandHorizFadeIn, + exit = MorpheAnimations.shrinkHorizFadeOut ) { InfoBadge( text = stringResource(R.string.disabled), diff --git a/app/src/main/java/app/morphe/manager/ui/screen/patcher/ExpertPatchingProgress.kt b/app/src/main/java/app/morphe/manager/ui/screen/patcher/ExpertPatchingProgress.kt index edeb88a15..004c9b129 100644 --- a/app/src/main/java/app/morphe/manager/ui/screen/patcher/ExpertPatchingProgress.kt +++ b/app/src/main/java/app/morphe/manager/ui/screen/patcher/ExpertPatchingProgress.kt @@ -5,7 +5,6 @@ package app.morphe.manager.ui.screen.patcher -import android.annotation.SuppressLint import androidx.compose.animation.* import androidx.compose.animation.core.* import androidx.compose.foundation.background @@ -55,11 +54,7 @@ import app.morphe.manager.patcher.worker.PatcherWorker.Companion.LOG_WORKER_PREF import app.morphe.manager.patcher.worker.PatcherWorker.Companion.LOG_WORKER_PREFIX_RUNTIME import app.morphe.manager.patcher.worker.PatcherWorker.Companion.LOG_WORKER_PREFIX_SUCCEEDED import app.morphe.manager.ui.model.State -import app.morphe.manager.ui.screen.shared.MorpheDefaults -import app.morphe.manager.ui.screen.shared.contentPadding -import app.morphe.manager.ui.screen.shared.itemSpacing -import app.morphe.manager.ui.screen.shared.rememberWindowSize -import app.morphe.manager.ui.screen.shared.useTwoColumnLayout +import app.morphe.manager.ui.screen.shared.* import app.morphe.manager.ui.viewmodel.PatcherViewModel import kotlinx.coroutines.delay @@ -473,7 +468,7 @@ private fun ExpertProgressHeader( val heapLimitMb = patcherViewModel.heapLimitMb AnimatedVisibility( visible = heapSamples.isNotEmpty(), - enter = fadeIn(tween(MorpheDefaults.ANIMATION_DURATION)) + expandVertically(tween(MorpheDefaults.ANIMATION_DURATION)), + enter = MorpheAnimations.expandFadeEnter ) { HeapUsageGraph( samples = heapSamples, @@ -736,11 +731,10 @@ private fun ExpertStepPipeline(patcherViewModel: PatcherViewModel) { */ @Composable private fun ExpertLogPanel( + modifier: Modifier = Modifier, patcherViewModel: PatcherViewModel, listState: LazyListState, - patcherSucceeded: Boolean? = null, - @SuppressLint("ModifierParameter") - modifier: Modifier = Modifier + patcherSucceeded: Boolean? = null ) { val rawLogs = patcherViewModel.logs // Convert the full list in one stateful pass so banner cards can aggregate metadata from auxiliary lines @@ -915,14 +909,24 @@ private fun PatcherInfoCard( @Composable private fun StartBannerCard(item: LogItem.StartBanner) { PatcherInfoCard(title = "Patching started", variant = CardVariant.Start) { - BannerFieldCell("Package", item.packageName, Modifier.fillMaxWidth()) + BannerFieldCell( + label = "Package", + value = item.packageName, + modifier = Modifier.weight(1f) + ) Row( modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(12.dp) ) { - BannerFieldCell("App version", item.version, Modifier.weight(1f)) - BannerFieldCell("Patches version", item.bundleVersion ?: "?", Modifier.weight(1f)) + BannerFieldCell( + label = "App version", + value = item.version, + modifier = Modifier.weight(1f)) + BannerFieldCell( + label = "Patches version", + value = item.bundleVersion ?: "?", + modifier = Modifier.weight(1f)) } HorizontalDivider( @@ -934,8 +938,14 @@ private fun StartBannerCard(item: LogItem.StartBanner) { modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(12.dp) ) { - BannerFieldCell("APK size", item.apkSizeMb, Modifier.weight(1f)) - BannerFieldCell("Patches", item.patchCount.toString(), Modifier.weight(1f)) + BannerFieldCell( + label = "APK size", + value = item.apkSizeMb, + modifier = Modifier.weight(1f)) + BannerFieldCell( + label = "Patches", + value = item.patchCount.toString(), + modifier = Modifier.weight(1f)) BannerFieldCell( label = "Split APK", value = if (item.isSplit) "yes" else "no", @@ -950,7 +960,10 @@ private fun StartBannerCard(item: LogItem.StartBanner) { modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(12.dp) ) { - BannerFieldCell("Runtime", runtimeLabel, Modifier.weight(1f)) + BannerFieldCell( + label = "Runtime", + value = runtimeLabel, + modifier = Modifier.weight(1f)) if (item.runtimeMemoryLimitMb != null) { BannerFieldCell( label = "Heap limit", @@ -973,12 +986,18 @@ private fun StartBannerCard(item: LogItem.StartBanner) { horizontalArrangement = Arrangement.spacedBy(12.dp) ) { item.androidVersion?.let { - BannerFieldCell("Android", it, Modifier.weight(1f)) + BannerFieldCell( + label = "Android", + value = it, + modifier = Modifier.weight(1f)) } if (item.deviceManufacturer != null || item.deviceModel != null) { val deviceLabel = listOfNotNull(item.deviceManufacturer, item.deviceModel) .joinToString(" ") - BannerFieldCell("Device", deviceLabel, Modifier.weight(1f)) + BannerFieldCell( + label = "Device", + value = deviceLabel, + modifier = Modifier.weight(1f)) } } @@ -1015,8 +1034,14 @@ private fun SuccessSummaryCard(item: LogItem.SuccessSummary) { modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(12.dp) ) { - BannerFieldCell("Output size", item.outputSizeMb, Modifier.weight(1f)) - BannerFieldCell("Time", item.elapsedSec, Modifier.weight(1f)) + BannerFieldCell( + label = "Output size", + value = item.outputSizeMb, + modifier = Modifier.weight(1f)) + BannerFieldCell( + label = "Time", + value = item.elapsedSec, + modifier = Modifier.weight(1f)) } if (item.processHeapAverageMb != null) { @@ -1024,8 +1049,14 @@ private fun SuccessSummaryCard(item: LogItem.SuccessSummary) { modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(12.dp) ) { - BannerFieldCell("Memory average", item.processHeapAverageMb, Modifier.weight(1f)) - BannerFieldCell("Memory max", item.processHeapMaxMb ?: "?", Modifier.weight(1f)) + BannerFieldCell( + label = "Memory average", + value = item.processHeapAverageMb, + modifier = Modifier.weight(1f)) + BannerFieldCell( + label = "Memory max", + value = item.processHeapMaxMb ?: "?", + modifier = Modifier.weight(1f)) } } } @@ -1036,10 +1067,9 @@ private fun SuccessSummaryCard(item: LogItem.SuccessSummary) { */ @Composable private fun BannerFieldCell( + modifier: Modifier = Modifier, label: String, value: String, - @SuppressLint("ModifierParameter") - modifier: Modifier = Modifier, valueColor: Color? = null ) { Column(modifier = modifier, verticalArrangement = Arrangement.spacedBy(2.dp)) { diff --git a/app/src/main/java/app/morphe/manager/ui/screen/patcher/PatcherBottomActionBar.kt b/app/src/main/java/app/morphe/manager/ui/screen/patcher/PatcherBottomActionBar.kt index 6b897b658..875751bd0 100644 --- a/app/src/main/java/app/morphe/manager/ui/screen/patcher/PatcherBottomActionBar.kt +++ b/app/src/main/java/app/morphe/manager/ui/screen/patcher/PatcherBottomActionBar.kt @@ -5,7 +5,6 @@ package app.morphe.manager.ui.screen.patcher -import android.annotation.SuppressLint import androidx.compose.foundation.layout.* import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.outlined.Article @@ -32,6 +31,8 @@ import kotlinx.coroutines.launch */ @Composable fun PatcherBottomActionBar( + modifier: Modifier = Modifier, + // Visibility control showCancelButton: Boolean = true, showHomeButton: Boolean = true, @@ -51,9 +52,7 @@ fun PatcherBottomActionBar( onInstallClick: () -> Unit = {}, // State - isSaving: Boolean = false, - @SuppressLint("ModifierParameter") - modifier: Modifier = Modifier + isSaving: Boolean = false ) { // Tracks the brief "Copied!" feedback state on the copy button var copied by remember { mutableStateOf(false) } diff --git a/app/src/main/java/app/morphe/manager/ui/screen/patcher/PatcherDialogs.kt b/app/src/main/java/app/morphe/manager/ui/screen/patcher/PatcherDialogs.kt index 7f5452070..611382932 100644 --- a/app/src/main/java/app/morphe/manager/ui/screen/patcher/PatcherDialogs.kt +++ b/app/src/main/java/app/morphe/manager/ui/screen/patcher/PatcherDialogs.kt @@ -359,11 +359,10 @@ fun PatcherErrorDialog( @Composable private fun ErrorInfoCard( + modifier: Modifier = Modifier, label: String, icon: ImageVector, errorBadge: String? = null, - @SuppressLint("ModifierParameter") - modifier: Modifier = Modifier, content: @Composable ColumnScope.() -> Unit ) { MorpheCard( diff --git a/app/src/main/java/app/morphe/manager/ui/screen/patcher/PatcherStates.kt b/app/src/main/java/app/morphe/manager/ui/screen/patcher/PatcherStates.kt index 75b0848c2..12908b35a 100644 --- a/app/src/main/java/app/morphe/manager/ui/screen/patcher/PatcherStates.kt +++ b/app/src/main/java/app/morphe/manager/ui/screen/patcher/PatcherStates.kt @@ -474,8 +474,8 @@ private fun SuccessErrorMessage( ) { AnimatedVisibility( visible = errorMessage != null && installState is InstallViewModel.InstallState.Error, - enter = fadeIn(tween(MorpheDefaults.ANIMATION_DURATION)), - exit = fadeOut(tween(MorpheDefaults.ANIMATION_DURATION)) + enter = MorpheAnimations.fadeIn, + exit = MorpheAnimations.fadeOut ) { errorMessage?.let { message -> Surface( @@ -505,8 +505,8 @@ private fun SuccessRootWarning( ) { AnimatedVisibility( visible = usingMountInstall && installState is InstallViewModel.InstallState.Ready, - enter = fadeIn(tween(MorpheDefaults.ANIMATION_DURATION)), - exit = fadeOut(tween(MorpheDefaults.ANIMATION_DURATION)) + enter = MorpheAnimations.fadeIn, + exit = MorpheAnimations.fadeOut ) { InfoBadge( text = stringResource(R.string.root_gmscore_excluded), diff --git a/app/src/main/java/app/morphe/manager/ui/screen/patcher/SimplePatcherProgress.kt b/app/src/main/java/app/morphe/manager/ui/screen/patcher/SimplePatcherProgress.kt index f540b9296..a346ebf73 100644 --- a/app/src/main/java/app/morphe/manager/ui/screen/patcher/SimplePatcherProgress.kt +++ b/app/src/main/java/app/morphe/manager/ui/screen/patcher/SimplePatcherProgress.kt @@ -248,8 +248,8 @@ private fun ProgressDetailsSection( // Long step warning AnimatedVisibility( visible = showLongStepWarning, - enter = fadeIn(tween(MorpheDefaults.ANIMATION_DURATION)) + expandVertically(tween(MorpheDefaults.ANIMATION_DURATION)), - exit = fadeOut(tween(MorpheDefaults.ANIMATION_DURATION)) + shrinkVertically(tween(MorpheDefaults.ANIMATION_DURATION)) + enter = MorpheAnimations.expandFadeEnter, + exit = MorpheAnimations.shrinkFadeExit ) { InfoBadge( text = stringResource(R.string.patcher_long_step_warning), diff --git a/app/src/main/java/app/morphe/manager/ui/screen/settings/AdvancedTabContent.kt b/app/src/main/java/app/morphe/manager/ui/screen/settings/AdvancedTabContent.kt index eec970612..6fdc9340c 100644 --- a/app/src/main/java/app/morphe/manager/ui/screen/settings/AdvancedTabContent.kt +++ b/app/src/main/java/app/morphe/manager/ui/screen/settings/AdvancedTabContent.kt @@ -131,7 +131,7 @@ fun AdvancedTabContent( } ) - // Strip unused native libraries + // Strip unused native libraries + filter split APKs for device RichSettingsItem( onClick = { settingsViewModel.setStripUnusedNativeLibs(!stripUnusedNativeLibs) diff --git a/app/src/main/java/app/morphe/manager/ui/screen/settings/AppearanceTabContent.kt b/app/src/main/java/app/morphe/manager/ui/screen/settings/AppearanceTabContent.kt index d6a0148f0..c7fca9a6d 100644 --- a/app/src/main/java/app/morphe/manager/ui/screen/settings/AppearanceTabContent.kt +++ b/app/src/main/java/app/morphe/manager/ui/screen/settings/AppearanceTabContent.kt @@ -8,9 +8,6 @@ package app.morphe.manager.ui.screen.settings import android.app.Activity import android.os.Build import androidx.compose.animation.AnimatedVisibility -import androidx.compose.animation.core.tween -import androidx.compose.animation.fadeIn -import androidx.compose.animation.fadeOut import androidx.compose.foundation.layout.* import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.shape.RoundedCornerShape @@ -53,8 +50,10 @@ fun AppearanceTabContent( val context = LocalContext.current val supportsDynamicColor = Build.VERSION.SDK_INT >= Build.VERSION_CODES.S val appLanguage by themeViewModel.prefs.appLanguage.getAsState() + val showGreetingPhrases by themeViewModel.prefs.showGreetingPhrases.getAsState() val backgroundType by themeViewModel.prefs.backgroundType.getAsState() val enableParallax by themeViewModel.prefs.enableBackgroundParallax.getAsState() + val randomInterval by themeViewModel.prefs.randomBackgroundInterval.getAsState() val showLanguageDialog = remember { mutableStateOf(false) } val showTranslationInfoDialog = remember { mutableStateOf(false) } @@ -76,6 +75,31 @@ fun AppearanceTabContent( onLanguageClick = { showTranslationInfoDialog.value = true } ) + // Home Screen Section + SectionTitle( + text = stringResource(R.string.settings_appearance_home_screen), + icon = Icons.Outlined.Dashboard + ) + + RichSettingsItem( + onClick = { themeViewModel.toggleShowGreetingPhrases(showGreetingPhrases) }, + showBorder = true, + title = stringResource(R.string.settings_appearance_greeting_phrases), + subtitle = stringResource(R.string.settings_appearance_greeting_phrases_subtitle), + leadingContent = { + MorpheIcon(icon = Icons.Outlined.ChatBubbleOutline) + }, + trailingContent = { + Switch( + checked = showGreetingPhrases, + onCheckedChange = null, + modifier = Modifier.semantics { + stateDescription = if (showGreetingPhrases) enabledState else disabledState + } + ) + } + ) + // Theme Mode Section SectionTitle( text = stringResource(R.string.settings_appearance_theme), @@ -139,6 +163,10 @@ fun AppearanceTabContent( selectedBackground = backgroundType, onBackgroundSelected = { selectedType -> themeViewModel.setBackgroundType(selectedType) + }, + selectedInterval = randomInterval, + onIntervalSelected = { interval -> + themeViewModel.setRandomInterval(interval) } ) @@ -176,8 +204,8 @@ fun AppearanceTabContent( // Translation Info Dialog AnimatedVisibility( visible = showTranslationInfoDialog.value, - enter = fadeIn(tween(MorpheDefaults.ANIMATION_DURATION)), - exit = fadeOut(tween(if (showLanguageDialog.value) 0 else MorpheDefaults.ANIMATION_DURATION)) + enter = MorpheAnimations.fadeIn, + exit = MorpheAnimations.fadeOut(if (showLanguageDialog.value) 0 else MorpheDefaults.ANIMATION_DURATION) ) { MorpheDialogWithLinks( title = stringResource(R.string.settings_appearance_translations_info_title), @@ -199,8 +227,8 @@ fun AppearanceTabContent( // Language Picker Dialog AnimatedVisibility( visible = showLanguageDialog.value, - enter = fadeIn(tween(MorpheDefaults.ANIMATION_DURATION)), - exit = fadeOut(tween(MorpheDefaults.ANIMATION_DURATION)) + enter = MorpheAnimations.fadeIn, + exit = MorpheAnimations.fadeOut ) { LanguagePickerDialog( currentLanguage = appLanguage, diff --git a/app/src/main/java/app/morphe/manager/ui/screen/settings/SystemTabContent.kt b/app/src/main/java/app/morphe/manager/ui/screen/settings/SystemTabContent.kt index d7198391d..bd7d318eb 100644 --- a/app/src/main/java/app/morphe/manager/ui/screen/settings/SystemTabContent.kt +++ b/app/src/main/java/app/morphe/manager/ui/screen/settings/SystemTabContent.kt @@ -30,6 +30,7 @@ import app.morphe.manager.ui.screen.shared.* import app.morphe.manager.ui.viewmodel.ImportExportViewModel import app.morphe.manager.ui.viewmodel.SettingsViewModel import app.morphe.manager.util.toast +import app.morphe.patcher.dex.BytecodeMode /** * System tab content. @@ -54,8 +55,10 @@ fun SystemTabContent( val useExpertMode by prefs.useExpertMode.getAsState() val useProcessRuntime by prefs.useProcessRuntime.getAsState() val memoryLimit by prefs.patcherProcessMemoryLimit.getAsState() + val bytecodeMode by prefs.bytecodeModePreference.getAsState() val showProcessRuntimeDialog = remember { mutableStateOf(false) } + val showBytecodeDialog = remember { mutableStateOf(false) } val showApkManagementDialog = remember { mutableStateOf(null) } val showPatchSelectionDialog = remember { mutableStateOf(false) } @@ -77,6 +80,14 @@ fun SystemTabContent( ) } + if (showBytecodeDialog.value) { + BytecodeModeDialog( + current = bytecodeMode, + onDismiss = { showBytecodeDialog.value = false }, + onSelect = { settingsViewModel.setBytecodeMode(it) } + ) + } + // APK management dialog showApkManagementDialog.value?.let { type -> ApkManagementDialog( @@ -122,42 +133,57 @@ fun SystemTabContent( ) SectionCard { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { - RichSettingsItem( - onClick = { showProcessRuntimeDialog.value = true }, - title = stringResource(R.string.settings_system_process_runtime), - subtitle = if (useProcessRuntime) - stringResource(R.string.settings_system_process_runtime_enabled_description, memoryLimit) - else stringResource(R.string.settings_system_process_runtime_disabled_description), - leadingContent = { - MorpheIcon(icon = Icons.Outlined.Memory) - }, - trailingContent = { - Row( - horizontalArrangement = Arrangement.spacedBy(8.dp), - verticalAlignment = Alignment.CenterVertically - ) { - InfoBadge( - text = if (useProcessRuntime) stringResource(R.string.enabled) - else stringResource(R.string.disabled), - style = if (useProcessRuntime) InfoBadgeStyle.Primary else InfoBadgeStyle.Default, - isCompact = true + Column { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + RichSettingsItem( + onClick = { showProcessRuntimeDialog.value = true }, + title = stringResource(R.string.settings_system_process_runtime), + subtitle = if (useProcessRuntime) + stringResource( + R.string.settings_system_process_runtime_enabled_description, + memoryLimit ) - MorpheIcon(icon = Icons.Outlined.ChevronRight) + else stringResource(R.string.settings_system_process_runtime_disabled_description), + leadingContent = { + MorpheIcon(icon = Icons.Outlined.Memory) + }, + trailingContent = { + Row( + horizontalArrangement = Arrangement.spacedBy(8.dp), + verticalAlignment = Alignment.CenterVertically + ) { + InfoBadge( + text = if (useProcessRuntime) stringResource(R.string.enabled) + else stringResource(R.string.disabled), + style = if (useProcessRuntime) InfoBadgeStyle.Primary else InfoBadgeStyle.Default, + isCompact = true + ) + MorpheIcon(icon = Icons.Outlined.ChevronRight) + } } - } - ) - } else { - IconTextRow( - modifier = Modifier.padding(16.dp), - leadingContent = { - MorpheIcon( - icon = Icons.Outlined.Memory, - tint = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.38f) - ) - }, - title = stringResource(R.string.settings_system_process_runtime), - description = stringResource(R.string.settings_system_process_runtime_description_not_available) + ) + } else { + IconTextRow( + modifier = Modifier.padding(16.dp), + leadingContent = { + MorpheIcon( + icon = Icons.Outlined.Memory, + tint = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.38f) + ) + }, + title = stringResource(R.string.settings_system_process_runtime), + description = stringResource(R.string.settings_system_process_runtime_description_not_available) + ) + } + + MorpheSettingsDivider() + + RichSettingsItem( + onClick = { showBytecodeDialog.value = true }, + title = stringResource(R.string.settings_advanced_bytecode_mode), + subtitle = stringResource(bytecodeMode.labelRes()), + leadingContent = { MorpheIcon(icon = Icons.Outlined.Code) }, + trailingContent = { MorpheIcon(icon = Icons.Outlined.ChevronRight) } ) } } @@ -342,3 +368,11 @@ fun SystemTabContent( } } } + +/** Maps a [BytecodeMode] to its short display label string resource. */ +private fun BytecodeMode.labelRes(): Int = when (this) { + BytecodeMode.NONE -> R.string.settings_advanced_bytecode_mode_strip_fast + BytecodeMode.STRIP_SAFE -> R.string.settings_advanced_bytecode_mode_strip_fast + BytecodeMode.STRIP_FAST -> R.string.settings_advanced_bytecode_mode_strip_fast + BytecodeMode.FULL -> R.string.settings_advanced_bytecode_mode_full +} diff --git a/app/src/main/java/app/morphe/manager/ui/screen/settings/advanced/UpdatesSection.kt b/app/src/main/java/app/morphe/manager/ui/screen/settings/advanced/UpdatesSection.kt index 74df18809..1da5aedcc 100644 --- a/app/src/main/java/app/morphe/manager/ui/screen/settings/advanced/UpdatesSection.kt +++ b/app/src/main/java/app/morphe/manager/ui/screen/settings/advanced/UpdatesSection.kt @@ -9,8 +9,7 @@ import android.Manifest import android.os.Build import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.result.contract.ActivityResultContracts -import androidx.compose.animation.* -import androidx.compose.animation.core.tween +import androidx.compose.animation.AnimatedVisibility import androidx.compose.foundation.layout.* import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons @@ -142,8 +141,8 @@ fun UpdatesSettingsItem( // Check frequency interval selector (non-GMS only) AnimatedVisibility( visible = backgroundUpdateNotifications && !settingsViewModel.hasGms, - enter = expandVertically(tween(MorpheDefaults.ANIMATION_DURATION)) + fadeIn(tween(MorpheDefaults.ANIMATION_DURATION)), - exit = shrinkVertically(tween(MorpheDefaults.ANIMATION_DURATION)) + fadeOut(tween(MorpheDefaults.ANIMATION_DURATION)) + enter = MorpheAnimations.expandFadeEnter, + exit = MorpheAnimations.shrinkFadeExit ) { RichSettingsItem( onClick = { showIntervalDialog.value = true }, diff --git a/app/src/main/java/app/morphe/manager/ui/screen/settings/appearance/BackgroundSelector.kt b/app/src/main/java/app/morphe/manager/ui/screen/settings/appearance/BackgroundSelector.kt index 6e4645680..495c464bd 100644 --- a/app/src/main/java/app/morphe/manager/ui/screen/settings/appearance/BackgroundSelector.kt +++ b/app/src/main/java/app/morphe/manager/ui/screen/settings/appearance/BackgroundSelector.kt @@ -5,6 +5,7 @@ package app.morphe.manager.ui.screen.settings.appearance +import androidx.compose.animation.AnimatedVisibility import androidx.compose.foundation.layout.* import androidx.compose.material.icons.Icons import androidx.compose.material.icons.outlined.* @@ -13,18 +14,20 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp -import app.morphe.manager.ui.screen.shared.BackgroundType -import app.morphe.manager.ui.screen.shared.SectionCard -import app.morphe.manager.ui.screen.shared.WindowWidthSizeClass -import app.morphe.manager.ui.screen.shared.rememberWindowSize +import app.morphe.manager.R +import app.morphe.manager.ui.screen.shared.* +import app.morphe.manager.ui.viewmodel.RandomInterval /** * Background animation selector with adaptive grid. + * Includes a RANDOM option that reveals an interval selector when active. */ @Composable fun BackgroundSelector( selectedBackground: BackgroundType, - onBackgroundSelected: (BackgroundType) -> Unit + onBackgroundSelected: (BackgroundType) -> Unit, + selectedInterval: RandomInterval, + onIntervalSelected: (RandomInterval) -> Unit ) { val windowSize = rememberWindowSize() val columns = when (windowSize.widthSizeClass) { @@ -33,33 +36,82 @@ fun BackgroundSelector( WindowWidthSizeClass.Expanded -> 5 } + // All types except RANDOM — shown in the main grid + val gridTypes = BackgroundType.entries.filter { it != BackgroundType.RANDOM } + SectionCard { Column( modifier = Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(8.dp) ) { - BackgroundType.entries.chunked(columns).forEach { row -> + // Main background grid (all except RANDOM) + gridTypes.chunked(columns).forEach { row -> Row( modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(8.dp) ) { row.forEach { bgType -> - val icon = getBackgroundIcon(bgType) - ModernIconOptionCard( selected = selectedBackground == bgType, onClick = { onBackgroundSelected(bgType) }, - icon = icon, + icon = getBackgroundIcon(bgType), label = stringResource(bgType.displayNameResId), modifier = Modifier.weight(1f) ) } - // Fill remaining space if row is incomplete repeat(columns - row.size) { Spacer(modifier = Modifier.weight(1f)) } } } + + // RANDOM option — full-width compact card at the bottom + CompactOptionCard( + selected = selectedBackground == BackgroundType.RANDOM, + onClick = { onBackgroundSelected(BackgroundType.RANDOM) }, + icon = Icons.Outlined.Shuffle, + label = stringResource(R.string.settings_appearance_background_random), + modifier = Modifier.fillMaxWidth() + ) + + // Interval selector — visible only when RANDOM is active + AnimatedVisibility( + visible = selectedBackground == BackgroundType.RANDOM, + enter = MorpheAnimations.expandFadeEnter, + exit = MorpheAnimations.shrinkFadeExit + ) { + RandomIntervalSelector( + selectedInterval = selectedInterval, + onIntervalSelected = onIntervalSelected, + modifier = Modifier.fillMaxWidth() + ) + } + } + } +} + +/** + * Horizontal row of interval options shown when RANDOM background is selected. + * Uses ModernIconOptionCard (vertical layout) so labels never truncate. + */ +@Composable +private fun RandomIntervalSelector( + selectedInterval: RandomInterval, + onIntervalSelected: (RandomInterval) -> Unit, + modifier: Modifier = Modifier +) { + Row( + modifier = modifier, + horizontalArrangement = Arrangement.spacedBy(8.dp) + ) { + RandomInterval.entries.forEach { interval -> + ModernIconOptionCard( + selected = selectedInterval == interval, + onClick = { onIntervalSelected(interval) }, + icon = getIntervalIcon(interval), + label = stringResource(interval.labelResId), + modifier = Modifier.weight(1f) + ) } } } @@ -77,4 +129,14 @@ private fun getBackgroundIcon(type: BackgroundType): ImageVector = when (type) { BackgroundType.GRID -> Icons.Outlined.Apps BackgroundType.PARTICLES -> Icons.Outlined.BubbleChart BackgroundType.NONE -> Icons.Outlined.VisibilityOff + BackgroundType.RANDOM -> Icons.Outlined.Shuffle +} + +/** + * Get icon for random interval option. + */ +private fun getIntervalIcon(interval: RandomInterval): ImageVector = when (interval) { + RandomInterval.ON_LAUNCH -> Icons.Outlined.PlayCircleOutline + RandomInterval.DAILY -> Icons.Outlined.Today + RandomInterval.EVERY_3_DAYS -> Icons.Outlined.DateRange } diff --git a/app/src/main/java/app/morphe/manager/ui/screen/settings/system/BytecodeModeDialog.kt b/app/src/main/java/app/morphe/manager/ui/screen/settings/system/BytecodeModeDialog.kt new file mode 100644 index 000000000..d1f12b932 --- /dev/null +++ b/app/src/main/java/app/morphe/manager/ui/screen/settings/system/BytecodeModeDialog.kt @@ -0,0 +1,92 @@ +/* + * Copyright 2026 Morphe. + * https://github.com/MorpheApp/morphe-manager + */ + +package app.morphe.manager.ui.screen.settings.system + +import androidx.compose.foundation.layout.* +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.* +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import app.morphe.manager.R +import app.morphe.manager.ui.screen.shared.* +import app.morphe.patcher.dex.BytecodeMode + +/** + * Dialog for selecting the bytecode processing mode. + */ +@Composable +fun BytecodeModeDialog( + current: BytecodeMode, + onDismiss: () -> Unit, + onSelect: (BytecodeMode) -> Unit, +) { + MorpheDialog( + onDismissRequest = onDismiss, + title = stringResource(R.string.settings_advanced_bytecode_mode), + footer = { + MorpheDialogOutlinedButton( + text = stringResource(R.string.close), + onClick = onDismiss, + modifier = Modifier.fillMaxWidth() + ) + } + ) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 16.dp), + verticalArrangement = Arrangement.spacedBy(12.dp) + ) { + Text( + text = stringResource(R.string.settings_advanced_bytecode_mode_dialog_description), + style = MaterialTheme.typography.bodyMedium, + color = LocalDialogSecondaryTextColor.current, + ) + + BytecodeModeOption( + titleRes = R.string.settings_advanced_bytecode_mode_strip_fast_label, + subtitleRes = R.string.settings_advanced_bytecode_mode_strip_fast_description, + isSelected = current == BytecodeMode.STRIP_FAST, + onSelect = { onSelect(BytecodeMode.STRIP_FAST) }, + ) + + BytecodeModeOption( + titleRes = R.string.settings_advanced_bytecode_mode_full, + subtitleRes = R.string.settings_advanced_bytecode_mode_full_description, + isSelected = current == BytecodeMode.FULL, + onSelect = { onSelect(BytecodeMode.FULL) }, + ) + } + } +} + +@Composable +private fun BytecodeModeOption( + titleRes: Int, + subtitleRes: Int, + isSelected: Boolean, + onSelect: () -> Unit, +) { + RichSettingsItem( + onClick = onSelect, + showBorder = true, + leadingContent = { + MorpheIcon( + icon = if (isSelected) Icons.Outlined.RadioButtonChecked + else Icons.Outlined.RadioButtonUnchecked, + tint = if (isSelected) MaterialTheme.colorScheme.primary + else MaterialTheme.colorScheme.onSurfaceVariant, + ) + }, + trailingContent = null, + title = stringResource(titleRes), + subtitle = stringResource(subtitleRes), + ) +} diff --git a/app/src/main/java/app/morphe/manager/ui/screen/settings/system/KeystoreCredentialsDialog.kt b/app/src/main/java/app/morphe/manager/ui/screen/settings/system/KeystoreCredentialsDialog.kt index e7a7bd8a5..dd917efdd 100644 --- a/app/src/main/java/app/morphe/manager/ui/screen/settings/system/KeystoreCredentialsDialog.kt +++ b/app/src/main/java/app/morphe/manager/ui/screen/settings/system/KeystoreCredentialsDialog.kt @@ -10,16 +10,14 @@ import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.FolderZip import androidx.compose.material.icons.outlined.Key import androidx.compose.material.icons.outlined.Person import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.* import androidx.compose.runtime.saveable.rememberSaveable -import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.input.KeyboardType @@ -27,6 +25,7 @@ import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import app.morphe.manager.R import app.morphe.manager.ui.screen.shared.* +import app.morphe.manager.util.KeystoreInputFormat /** * Keystore Credentials Dialog. @@ -35,10 +34,12 @@ import app.morphe.manager.ui.screen.shared.* @Composable fun KeystoreCredentialsDialog( onDismiss: () -> Unit, - onSubmit: (String, String) -> Unit + initialFormat: KeystoreInputFormat = KeystoreInputFormat.KEYSTORE, + onSubmit: (alias: String, password: String, format: KeystoreInputFormat) -> Unit ) { var alias by rememberSaveable { mutableStateOf("") } var pass by rememberSaveable { mutableStateOf("") } + var format by rememberSaveable(initialFormat) { mutableStateOf(initialFormat) } MorpheDialog( onDismissRequest = onDismiss, @@ -46,7 +47,7 @@ fun KeystoreCredentialsDialog( footer = { MorpheDialogButtonRow( primaryText = stringResource(R.string.settings_system_import_keystore_dialog_button), - onPrimaryClick = { onSubmit(alias, pass) }, + onPrimaryClick = { onSubmit(alias, pass, format) }, secondaryText = stringResource(android.R.string.cancel), onSecondaryClick = onDismiss ) @@ -66,6 +67,26 @@ fun KeystoreCredentialsDialog( textAlign = TextAlign.Center ) + // Format selector + val formatItems = remember { + KeystoreInputFormat.entries.associate { it.displayName to it.name } + } + MorpheDialogDropdownTextField( + value = format.name, + onValueChange = { name -> + format = KeystoreInputFormat.entries.firstOrNull { it.name == name } ?: format + }, + dropdownItems = formatItems, + label = { Text(stringResource(R.string.settings_system_import_keystore_dialog_format_field)) }, + leadingIcon = { + Icon( + imageVector = Icons.Outlined.FolderZip, + contentDescription = null, + tint = textColor.copy(alpha = 0.7f) + ) + } + ) + // Alias Input MorpheDialogTextField( value = alias, diff --git a/app/src/main/java/app/morphe/manager/ui/screen/settings/system/PatchSelectionManagementDialog.kt b/app/src/main/java/app/morphe/manager/ui/screen/settings/system/PatchSelectionManagementDialog.kt index 0fd11b2ae..5cf5cdb3e 100644 --- a/app/src/main/java/app/morphe/manager/ui/screen/settings/system/PatchSelectionManagementDialog.kt +++ b/app/src/main/java/app/morphe/manager/ui/screen/settings/system/PatchSelectionManagementDialog.kt @@ -409,8 +409,8 @@ private fun PackageSelectionItem( // Expanded content AnimatedVisibility( visible = expanded, - enter = fadeIn() + expandVertically(expandFrom = Alignment.Top), - exit = fadeOut() + shrinkVertically(shrinkTowards = Alignment.Top) + enter = MorpheAnimations.expandTopFadeIn, + exit = MorpheAnimations.shrinkTopFadeOut ) { Column( modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp), @@ -822,7 +822,7 @@ private fun PatchDetailsDialog( onDismissRequest = onDismiss, title = stringResource(R.string.settings_system_patch_details_title), footer = { - MorpheDialogButton( + MorpheDialogOutlinedButton( text = stringResource(R.string.close), onClick = onDismiss, modifier = Modifier.fillMaxWidth() diff --git a/app/src/main/java/app/morphe/manager/ui/screen/shared/AdaptiveIconCreatorDialog.kt b/app/src/main/java/app/morphe/manager/ui/screen/shared/AdaptiveIconCreatorDialog.kt index bd9a375d8..e8a6cc56e 100644 --- a/app/src/main/java/app/morphe/manager/ui/screen/shared/AdaptiveIconCreatorDialog.kt +++ b/app/src/main/java/app/morphe/manager/ui/screen/shared/AdaptiveIconCreatorDialog.kt @@ -11,8 +11,7 @@ import android.graphics.* import android.net.Uri import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.result.contract.ActivityResultContracts -import androidx.compose.animation.* -import androidx.compose.animation.core.tween +import androidx.compose.animation.AnimatedVisibility import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.Canvas import androidx.compose.foundation.background @@ -235,8 +234,8 @@ fun AdaptiveIconCreatorDialog( // Explanation text AnimatedVisibility( visible = foregroundBitmap != null, - enter = fadeIn(tween(MorpheDefaults.ANIMATION_DURATION)) + expandVertically(tween(MorpheDefaults.ANIMATION_DURATION)), - exit = fadeOut(tween(MorpheDefaults.ANIMATION_DURATION)) + shrinkVertically(tween(MorpheDefaults.ANIMATION_DURATION)) + enter = MorpheAnimations.expandFadeEnter, + exit = MorpheAnimations.shrinkFadeExit ) { InfoBadge( text = stringResource( diff --git a/app/src/main/java/app/morphe/manager/ui/screen/shared/AnimatedBackground.kt b/app/src/main/java/app/morphe/manager/ui/screen/shared/AnimatedBackground.kt index 47bb199c0..3deace554 100644 --- a/app/src/main/java/app/morphe/manager/ui/screen/shared/AnimatedBackground.kt +++ b/app/src/main/java/app/morphe/manager/ui/screen/shared/AnimatedBackground.kt @@ -28,25 +28,40 @@ enum class BackgroundType(val displayNameResId: Int) { SNOW(R.string.settings_appearance_background_snow), GRID(R.string.settings_appearance_background_grid), PARTICLES(R.string.settings_appearance_background_particles), - NONE(R.string.settings_appearance_background_none); + NONE(R.string.settings_appearance_background_none), + RANDOM(R.string.settings_appearance_background_random); companion object { val DEFAULT = CIRCLES + + /** All types that can be picked when RANDOM is active (excludes NONE and RANDOM itself). */ + val RANDOMIZABLE: List = entries.filter { it != NONE && it != RANDOM } } } /** * Animated background with multiple visual styles. * Creates subtle floating effects that can be used across all screens. + * + * When [type] is [BackgroundType.RANDOM], [resolvedType] must be provided — + * it holds the already-resolved random type from the ViewModel so the choice + * stays stable for the current session/interval. */ @Composable @SuppressLint("ModifierParameter") fun AnimatedBackground( type: BackgroundType = BackgroundType.CIRCLES, + resolvedType: BackgroundType? = null, enableParallax: Boolean = true, speedMultiplier: Float = 1f, patchingCompleted: Boolean = false ) { + val effectiveType = if (type == BackgroundType.RANDOM) { + resolvedType ?: BackgroundType.CIRCLES + } else { + type + } + Box( modifier = Modifier .fillMaxSize() @@ -55,7 +70,7 @@ fun AnimatedBackground( compositingStrategy = CompositingStrategy.Offscreen } ) { - when (type) { + when (effectiveType) { BackgroundType.CIRCLES -> CirclesBackground( modifier = Modifier.fillMaxSize(), enableParallax = enableParallax, @@ -105,6 +120,8 @@ fun AnimatedBackground( patchingCompleted = patchingCompleted ) BackgroundType.NONE -> Unit + // effectiveType is never RANDOM (resolved above), but the branch is required for exhaustiveness + BackgroundType.RANDOM -> Unit } } } diff --git a/app/src/main/java/app/morphe/manager/ui/screen/shared/AppIcon.kt b/app/src/main/java/app/morphe/manager/ui/screen/shared/AppIcon.kt index 21c506188..ea5651ad7 100644 --- a/app/src/main/java/app/morphe/manager/ui/screen/shared/AppIcon.kt +++ b/app/src/main/java/app/morphe/manager/ui/screen/shared/AppIcon.kt @@ -1,7 +1,7 @@ package app.morphe.manager.ui.screen.shared -import android.annotation.SuppressLint import android.content.pm.PackageInfo +import android.graphics.drawable.Drawable import androidx.compose.foundation.Image import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons @@ -32,11 +32,10 @@ import org.koin.compose.koinInject */ @Composable fun AppIcon( + modifier: Modifier = Modifier, packageInfo: PackageInfo? = null, packageName: String? = null, contentDescription: String?, - @SuppressLint("ModifierParameter") - modifier: Modifier = Modifier, preferredSource: AppDataSource = AppDataSource.INSTALLED, placeholderGradientColors: List? = null, placeholderInnerPadding: Dp = 0.dp @@ -118,12 +117,15 @@ private fun ResolvedAppIcon( val appDataResolver: AppDataResolver = koinInject() var resolvedPackageInfo by remember(packageName) { mutableStateOf(null) } + var resolvedDrawable by remember(packageName) { mutableStateOf(null) } var isLoading by remember(packageName) { mutableStateOf(true) } LaunchedEffect(packageName, preferredSource) { // Use resolveAppData to get complete data in one call val resolvedData = appDataResolver.resolveAppData(packageName, preferredSource) resolvedPackageInfo = resolvedData.packageInfo + // Fall back to raw Drawable when packageInfo is unavailable + resolvedDrawable = resolvedData.icon.takeIf { resolvedData.packageInfo == null } isLoading = false } @@ -150,6 +152,14 @@ private fun ResolvedAppIcon( modifier = modifier ) } + resolvedDrawable != null -> { + // packageInfo unavailable but raw Drawable was resolved (rare path) + DrawableAppIcon( + drawable = resolvedDrawable!!, + contentDescription = contentDescription, + modifier = modifier + ) + } placeholderGradientColors != null -> { // No icon found - show glass placeholder tinted to card colors GlassPlaceholderIcon( @@ -167,6 +177,24 @@ private fun ResolvedAppIcon( } } +/** + * Icon display from a raw [Drawable] - used when packageInfo is unavailable but + * the resolver produced a Drawable directly. + */ +@Composable +private fun DrawableAppIcon( + drawable: Drawable, + contentDescription: String?, + modifier: Modifier = Modifier +) { + // Coil can load Drawable directly without needing PackageInfo + AsyncImage( + model = drawable, + contentDescription = contentDescription, + modifier = modifier + ) +} + /** * Fallback Android icon when no package info is available and no gradient colors are given. */ diff --git a/app/src/main/java/app/morphe/manager/ui/screen/shared/AppLabel.kt b/app/src/main/java/app/morphe/manager/ui/screen/shared/AppLabel.kt index 4e5408607..c31f4010f 100644 --- a/app/src/main/java/app/morphe/manager/ui/screen/shared/AppLabel.kt +++ b/app/src/main/java/app/morphe/manager/ui/screen/shared/AppLabel.kt @@ -1,6 +1,5 @@ package app.morphe.manager.ui.screen.shared -import android.annotation.SuppressLint import android.content.pm.PackageInfo import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.LocalTextStyle @@ -24,17 +23,16 @@ import kotlinx.coroutines.withContext import org.koin.compose.koinInject /** - * Universal app label component + * Universal app label component. * * Automatically resolves label from available sources: - * installed app → original APK → patched APK → constants → package name fallback + * installed app → original APK → patched APK → constants → package name fallback. */ @Composable fun AppLabel( + modifier: Modifier = Modifier, packageInfo: PackageInfo? = null, packageName: String? = null, - @SuppressLint("ModifierParameter") - modifier: Modifier = Modifier, style: TextStyle = LocalTextStyle.current, defaultText: String? = stringResource(R.string.not_installed), preferredSource: AppDataSource = AppDataSource.INSTALLED, @@ -79,7 +77,7 @@ fun AppLabel( } /** - * Simple label display when PackageInfo is already available + * Simple label display when PackageInfo is already available. */ @Composable private fun SimpleAppLabel( @@ -91,10 +89,11 @@ private fun SimpleAppLabel( overflow: TextOverflow = TextOverflow.Clip ) { val context = LocalContext.current - var label: String? by rememberSaveable { mutableStateOf(null) } - LaunchedEffect(packageInfo) { - label = withContext(Dispatchers.IO) { + // Attempt a cheap synchronous resolution first so we never show a shimmer + // when the label is already available in memory + val initialLabel = remember(packageInfo.packageName) { + runCatching { packageInfo.applicationInfo?.loadLabel(context.packageManager) ?.toString() ?.let { raw -> @@ -103,12 +102,32 @@ private fun SimpleAppLabel( } ?: packageInfo.applicationInfo?.nonLocalizedLabel?.toString() ?.takeIf { it.isNotBlank() } - ?: defaultText + }.getOrNull() + } + + var label: String? by rememberSaveable(packageInfo.packageName) { + mutableStateOf(initialLabel) + } + + // Only dispatch to IO when the synchronous attempt didn't produce a result + if (label == null) { + LaunchedEffect(packageInfo.packageName) { + label = withContext(Dispatchers.IO) { + packageInfo.applicationInfo?.loadLabel(context.packageManager) + ?.toString() + ?.let { raw -> + val cleaned = cleanWeirdLabel(raw, packageInfo.packageName) + cleaned.takeIf { it.isNotBlank() && cleaned != packageInfo.packageName } + } + ?: packageInfo.applicationInfo?.nonLocalizedLabel?.toString() + ?.takeIf { it.isNotBlank() } + ?: defaultText + } } } Text( - label ?: stringResource(R.string.loading), + label ?: defaultText ?: stringResource(R.string.loading), modifier = Modifier .placeholder( visible = label == null, @@ -123,7 +142,7 @@ private fun SimpleAppLabel( } /** - * Resolved label from any available source when only package name is known + * Resolved label from any available source when only package name is known. */ @Composable private fun ResolvedAppLabel( @@ -143,7 +162,7 @@ private fun ResolvedAppLabel( LaunchedEffect(packageName, preferredSource) { // Use resolveAppData to get complete data in one call val resolvedData = appDataResolver.resolveAppData(packageName, preferredSource) - // If resolved name is same as package name and we have a default, use default + // If resolved name is same as package name, and we have a default, use default label = if (resolvedData.displayName == packageName && defaultText != null) { defaultText } else { @@ -173,7 +192,7 @@ private fun ResolvedAppLabel( } /** - * Clean weird labels that contain package name or other artifacts + * Clean weird labels that contain package name or other artifacts. */ private fun cleanWeirdLabel(raw: String, packageName: String?): String { val trimmed = raw.trim() diff --git a/app/src/main/java/app/morphe/manager/ui/screen/shared/HeaderCreatorDialog.kt b/app/src/main/java/app/morphe/manager/ui/screen/shared/HeaderCreatorDialog.kt index 8b1e6727d..32343415e 100644 --- a/app/src/main/java/app/morphe/manager/ui/screen/shared/HeaderCreatorDialog.kt +++ b/app/src/main/java/app/morphe/manager/ui/screen/shared/HeaderCreatorDialog.kt @@ -7,16 +7,11 @@ package app.morphe.manager.ui.screen.shared import android.annotation.SuppressLint import android.content.Context -import android.graphics.Bitmap -import android.graphics.BitmapFactory -import android.graphics.Canvas -import android.graphics.Paint -import android.graphics.RectF +import android.graphics.* import android.net.Uri import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.result.contract.ActivityResultContracts -import androidx.compose.animation.* -import androidx.compose.animation.core.tween +import androidx.compose.animation.AnimatedVisibility import androidx.compose.foundation.Canvas import androidx.compose.foundation.background import androidx.compose.foundation.border @@ -240,8 +235,8 @@ fun HeaderCreatorDialog( // Explanation text AnimatedVisibility( visible = canCreate, - enter = fadeIn(tween(MorpheDefaults.ANIMATION_DURATION)) + expandVertically(tween(MorpheDefaults.ANIMATION_DURATION)), - exit = fadeOut(tween(MorpheDefaults.ANIMATION_DURATION)) + shrinkVertically(tween(MorpheDefaults.ANIMATION_DURATION)) + enter = MorpheAnimations.expandFadeEnter, + exit = MorpheAnimations.shrinkFadeExit ) { InfoBadge( text = stringResource( diff --git a/app/src/main/java/app/morphe/manager/ui/screen/shared/InfoBadge.kt b/app/src/main/java/app/morphe/manager/ui/screen/shared/InfoBadge.kt index d5ee8adde..f09a1fcbf 100644 --- a/app/src/main/java/app/morphe/manager/ui/screen/shared/InfoBadge.kt +++ b/app/src/main/java/app/morphe/manager/ui/screen/shared/InfoBadge.kt @@ -5,7 +5,6 @@ package app.morphe.manager.ui.screen.shared -import android.annotation.SuppressLint import androidx.compose.foundation.layout.* import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.MaterialTheme @@ -55,14 +54,13 @@ enum class InfoBadgeStyle { */ @Composable fun InfoBadge( + modifier: Modifier = Modifier, text: String, style: InfoBadgeStyle = InfoBadgeStyle.Default, icon: ImageVector? = null, isCompact: Boolean = false, isExpanded: Boolean = false, - isCentered: Boolean = false, - @SuppressLint("ModifierParameter") - modifier: Modifier = Modifier + isCentered: Boolean = false ) { val (containerColor, contentColor) = style.colors() diff --git a/app/src/main/java/app/morphe/manager/ui/screen/shared/MorpheDialog.kt b/app/src/main/java/app/morphe/manager/ui/screen/shared/MorpheDialog.kt index 3dde3137e..40930a379 100644 --- a/app/src/main/java/app/morphe/manager/ui/screen/shared/MorpheDialog.kt +++ b/app/src/main/java/app/morphe/manager/ui/screen/shared/MorpheDialog.kt @@ -6,8 +6,6 @@ package app.morphe.manager.ui.screen.shared import androidx.compose.animation.* -import androidx.compose.animation.core.FastOutSlowInEasing -import androidx.compose.animation.core.tween import androidx.compose.foundation.background import androidx.compose.foundation.gestures.detectTapGestures import androidx.compose.foundation.layout.* @@ -45,6 +43,7 @@ val LocalDialogSecondaryTextColor = compositionLocalOf { Color.White.copy(alpha * @param dismissOnClickOutside Whether clicking outside dismisses the dialog * @param scrollable Whether to wrap content in verticalScroll. Set to false for LazyColumn. Default is true. * @param compactPadding Whether to use compact padding. Default is false. + * @param noPadding Whether to remove all padding and system bar insets. Default is false. * @param content Dialog content */ @Composable @@ -56,6 +55,8 @@ fun MorpheDialog( dismissOnClickOutside: Boolean = false, scrollable: Boolean = true, compactPadding: Boolean = false, + noPadding: Boolean = false, + onEntered: (() -> Unit)? = null, content: @Composable ColumnScope.() -> Unit ) { val isDarkTheme = MaterialTheme.colorScheme.background.isDarkBackground() @@ -63,6 +64,11 @@ fun MorpheDialog( LaunchedEffect(Unit) { visible = true + // Notify caller once the enter animation has completed + if (onEntered != null) { + kotlinx.coroutines.delay(MorpheDefaults.ANIMATION_DURATION.toLong()) + onEntered() + } } Dialog( @@ -98,16 +104,8 @@ fun MorpheDialog( AnimatedVisibility( visible = visible, - enter = fadeIn(animationSpec = tween(MorpheDefaults.ANIMATION_DURATION)) + - scaleIn( - initialScale = 0.95f, - animationSpec = tween(MorpheDefaults.ANIMATION_DURATION, easing = FastOutSlowInEasing) - ), - exit = fadeOut(animationSpec = tween(MorpheDefaults.ANIMATION_DURATION)) + - scaleOut( - targetScale = 0.95f, - animationSpec = tween(MorpheDefaults.ANIMATION_DURATION) - ), + enter = MorpheAnimations.dialogEnter, + exit = MorpheAnimations.dialogExit, modifier = Modifier.fillMaxSize() ) { Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { @@ -118,6 +116,7 @@ fun MorpheDialog( isDarkTheme = isDarkTheme, scrollable = scrollable, compactPadding = compactPadding, + noPadding = noPadding, content = content ) } @@ -137,6 +136,7 @@ private fun DialogContent( isDarkTheme: Boolean, scrollable: Boolean, compactPadding: Boolean, + noPadding: Boolean, content: @Composable ColumnScope.() -> Unit ) { val isLandscape = isLandscape() @@ -146,6 +146,23 @@ private fun DialogContent( val secondaryTextColor = if (isDarkTheme) Color.White.copy(alpha = 0.7f) else Color.Black.copy(alpha = 0.7f) + // noPadding mode: fill entire screen, no insets, caller handles layout + if (noPadding) { + CompositionLocalProvider( + LocalDialogTextColor provides textColor, + LocalDialogSecondaryTextColor provides secondaryTextColor + ) { + Column( + modifier = Modifier + .fillMaxSize() + .pointerInput(Unit) { detectTapGestures { /* Consume clicks */ } } + ) { + content() + } + } + return + } + Box( modifier = Modifier .fillMaxSize() diff --git a/app/src/main/java/app/morphe/manager/ui/screen/shared/MorpheDialogTextField.kt b/app/src/main/java/app/morphe/manager/ui/screen/shared/MorpheDialogTextField.kt index c781e6fe9..e238231c9 100644 --- a/app/src/main/java/app/morphe/manager/ui/screen/shared/MorpheDialogTextField.kt +++ b/app/src/main/java/app/morphe/manager/ui/screen/shared/MorpheDialogTextField.kt @@ -5,8 +5,10 @@ package app.morphe.manager.ui.screen.shared -import android.annotation.SuppressLint +import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.shape.RoundedCornerShape @@ -14,19 +16,14 @@ import androidx.compose.foundation.text.KeyboardActions import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Check -import androidx.compose.material.icons.outlined.Clear -import androidx.compose.material.icons.outlined.FolderOpen -import androidx.compose.material.icons.outlined.Visibility -import androidx.compose.material.icons.outlined.VisibilityOff +import androidx.compose.material.icons.outlined.* import androidx.compose.material3.* -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember +import androidx.compose.runtime.* import androidx.compose.runtime.saveable.rememberSaveable -import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.input.PasswordVisualTransformation import androidx.compose.ui.text.input.VisualTransformation @@ -34,11 +31,12 @@ import androidx.compose.ui.unit.dp import app.morphe.manager.R /** - * Styled OutlinedTextField for dialogs with proper theming - * Supports password visibility toggle and clear button + * Styled OutlinedTextField for dialogs with proper theming. + * Supports password visibility toggle and clear button. */ @Composable fun MorpheDialogTextField( + modifier: Modifier = Modifier, value: String, onValueChange: (String) -> Unit, label: @Composable (() -> Unit)? = null, @@ -52,9 +50,7 @@ fun MorpheDialogTextField( showClearButton: Boolean = false, onFolderPickerClick: (() -> Unit)? = null, keyboardOptions: KeyboardOptions = KeyboardOptions.Default, - keyboardActions: KeyboardActions = KeyboardActions.Default, - @SuppressLint("ModifierParameter") - modifier: Modifier = Modifier + keyboardActions: KeyboardActions = KeyboardActions.Default ) { var passwordVisible by rememberSaveable { mutableStateOf(false) } val textColor = LocalDialogTextColor.current @@ -147,12 +143,17 @@ fun MorpheDialogTextField( } /** - * Styled OutlinedTextField with dropdown menu support for dialogs - * Combines text input, folder picker, clear button, and dropdown selection + * Styled OutlinedTextField with dropdown menu support for dialogs. + * Combines text input, folder picker, clear button, and dropdown selection. + * + * Tap behavior: + * - 1st tap: opens dropdown list (field stays read-only, no keyboard) + * - 2nd tap (after closing dropdown without selecting): opens keyboard for manual input + * - selecting an item or dismissing resets back to 1st-tap behavior */ -@OptIn(ExperimentalMaterial3Api::class) @Composable fun MorpheDialogDropdownTextField( + modifier: Modifier = Modifier, value: String, onValueChange: (String) -> Unit, dropdownItems: Map, // Display name -> Value @@ -164,20 +165,26 @@ fun MorpheDialogDropdownTextField( enabled: Boolean = true, singleLine: Boolean = true, keyboardOptions: KeyboardOptions = KeyboardOptions.Default, - keyboardActions: KeyboardActions = KeyboardActions.Default, - @SuppressLint("ModifierParameter") - modifier: Modifier = Modifier + keyboardActions: KeyboardActions = KeyboardActions.Default ) { var dropdownExpanded by remember { mutableStateOf(false) } + // true = first tap opens dropdown; false = second tap opens keyboard + var readOnly by remember { mutableStateOf(true) } + val focusRequester = remember { FocusRequester() } val textColor = LocalDialogTextColor.current // Show display name from map only if value exists in map, otherwise show raw value val displayValue = dropdownItems.entries.find { it.value == value }?.key ?: value - ExposedDropdownMenuBox( - expanded = dropdownExpanded, - onExpandedChange = { dropdownExpanded = it } - ) { + // When readOnly becomes false the field is now editable - request focus so + // the system knows to show the keyboard on the next tap (or immediately) + LaunchedEffect(readOnly) { + if (!readOnly) { + focusRequester.requestFocus() + } + } + + Box(modifier = modifier.fillMaxWidth()) { OutlinedTextField( value = displayValue, onValueChange = { newDisplayValue -> @@ -185,7 +192,7 @@ fun MorpheDialogDropdownTextField( val newValue = dropdownItems[newDisplayValue] ?: newDisplayValue onValueChange(newValue) }, - readOnly = false, + readOnly = readOnly, label = label, placeholder = placeholder, leadingIcon = leadingIcon, @@ -218,15 +225,27 @@ fun MorpheDialogDropdownTextField( } // Dropdown arrow - ExposedDropdownMenuDefaults.TrailingIcon(expanded = dropdownExpanded) + IconButton(onClick = { + dropdownExpanded = !dropdownExpanded + if (!dropdownExpanded) readOnly = true + }) { + Icon( + imageVector = if (dropdownExpanded) + Icons.Outlined.ExpandLess + else + Icons.Outlined.ExpandMore, + contentDescription = null, + tint = textColor.copy(alpha = 0.7f) + ) + } } }, enabled = enabled, keyboardOptions = keyboardOptions, keyboardActions = keyboardActions, - modifier = modifier + modifier = Modifier .fillMaxWidth() - .menuAnchor(ExposedDropdownMenuAnchorType.PrimaryEditable), + .focusRequester(focusRequester), shape = RoundedCornerShape(12.dp), colors = OutlinedTextFieldDefaults.colors( focusedTextColor = textColor, @@ -247,9 +266,27 @@ fun MorpheDialogDropdownTextField( ) ) - ExposedDropdownMenu( + // Invisible overlay that captures the first tap to open dropdown, + // then removes itself so the second tap reaches the real TextField. + if (readOnly) { + Box( + modifier = Modifier + .matchParentSize() + .clickable( + indication = null, + interactionSource = remember { MutableInteractionSource() } + ) { + dropdownExpanded = true + } + ) + } + + DropdownMenu( expanded = dropdownExpanded, - onDismissRequest = { dropdownExpanded = false } + onDismissRequest = { + dropdownExpanded = false + readOnly = false // dismissed without selecting -> unlock keyboard + } ) { dropdownItems.forEach { (displayName, itemValue) -> DropdownMenuItem( @@ -257,6 +294,7 @@ fun MorpheDialogDropdownTextField( onClick = { onValueChange(itemValue) dropdownExpanded = false + readOnly = true // selected -> reset to dropdown-first behavior }, leadingIcon = if (itemValue == value) { { diff --git a/app/src/main/java/app/morphe/manager/ui/screen/shared/MorpheSettingComponents.kt b/app/src/main/java/app/morphe/manager/ui/screen/shared/MorpheSettingComponents.kt index 70c8bc025..39d67dc35 100644 --- a/app/src/main/java/app/morphe/manager/ui/screen/shared/MorpheSettingComponents.kt +++ b/app/src/main/java/app/morphe/manager/ui/screen/shared/MorpheSettingComponents.kt @@ -5,9 +5,13 @@ package app.morphe.manager.ui.screen.shared -import android.annotation.SuppressLint import androidx.compose.animation.* +import androidx.compose.animation.core.Easing import androidx.compose.animation.core.animateFloatAsState +import androidx.compose.animation.core.FastOutSlowInEasing +import androidx.compose.animation.core.LinearOutSlowInEasing +import androidx.compose.animation.core.Spring +import androidx.compose.animation.core.spring import androidx.compose.animation.core.tween import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.background @@ -45,9 +49,97 @@ object MorpheDefaults { val SettingsCornerRadius = 14.dp val SectionCornerRadius = 18.dp val IconSize = 24.dp - const val ANIMATION_DURATION = 300 val ContentPadding = 16.dp val ItemSpacing = 12.dp + + // Animation durations + /** Duration used for dialog enter/exit and overlay transitions. */ + const val ANIMATION_DURATION = 220 + /** Shorter fade duration used inside spring-based exit transitions. */ + const val ANIMATION_DURATION_SHORT = 180 + /** Duration used for screen-level enter transitions (navigation push). */ + const val SCREEN_ENTER_DURATION = 320 + + // Dialog animation scale + /** Initial/target scale for dialog enter/exit scale animation. */ + const val DIALOG_SCALE = 0.95f +} + +/** + * Shared [EnterTransition] and [ExitTransition] for all MorpheDialog instances and + * dialog-level AnimatedVisibility wrappers. Changing these values updates every dialog + * animation in the app at once. + */ +object MorpheAnimations { + // Private helper to avoid repeating tween specifications + private fun defaultTween( + duration: Int = MorpheDefaults.ANIMATION_DURATION, + easing: Easing = LinearOutSlowInEasing + ) = tween(duration, easing = easing) + + // Base animations used for composition + val fadeIn = fadeIn(animationSpec = defaultTween()) + val fadeOut = fadeOut(animationSpec = defaultTween()) + + // Dialog Transitions + val dialogEnter = fadeIn + scaleIn( + initialScale = MorpheDefaults.DIALOG_SCALE, + animationSpec = defaultTween(easing = FastOutSlowInEasing) + ) + val dialogExit = fadeOut + scaleOut( + targetScale = MorpheDefaults.DIALOG_SCALE, + animationSpec = defaultTween() + ) + + // Overlays (no scale needed) + val overlayEnter = fadeIn + val overlayExit = fadeOut + + // Screen Transitions + // Enter uses a longer duration; exit is identical to dialogExit so we reuse it directly. + val screenEnter = fadeIn(defaultTween(MorpheDefaults.SCREEN_ENTER_DURATION)) + + scaleIn( + initialScale = MorpheDefaults.DIALOG_SCALE, + animationSpec = defaultTween(MorpheDefaults.SCREEN_ENTER_DURATION, FastOutSlowInEasing) + ) + val screenExit = dialogExit + + // Vertical Expand/Shrink + val expandFadeEnter = expandVertically(defaultTween()) + fadeIn + val shrinkFadeExit = shrinkVertically(defaultTween()) + fadeOut + + val expandVertEnter = expandVertically(defaultTween()) + val shrinkVertExit = shrinkVertically(defaultTween()) + + // Horizontal Expand/Shrink + val expandHorizFadeIn = expandHorizontally(defaultTween()) + fadeIn + val shrinkHorizFadeOut = shrinkHorizontally(defaultTween()) + fadeOut + + // Slide Transitions + val slideUpFadeEnter = slideInVertically(defaultTween()) { -it } + fadeIn + val slideUpFadeExit = slideOutVertically(defaultTween()) { -it } + fadeOut + + // Spring & Custom Transitions + val springSlideUpEnter = slideInVertically( + animationSpec = spring(Spring.DampingRatioMediumBouncy, Spring.StiffnessMedium), + initialOffsetY = { it } + ) + fadeIn(tween(MorpheDefaults.ANIMATION_DURATION_SHORT)) + + val springSlideDownExit = slideOutVertically( + animationSpec = defaultTween(easing = FastOutSlowInEasing), + targetOffsetY = { it } + ) + fadeOut(tween(MorpheDefaults.ANIMATION_DURATION_SHORT)) + + // Scale Transitions + val fadeScaleIn = fadeIn + scaleIn(defaultTween(), initialScale = MorpheDefaults.DIALOG_SCALE) + val fadeScaleOut = fadeOut + scaleOut(defaultTween(), targetScale = MorpheDefaults.DIALOG_SCALE) + + // Alignment-based Transitions + val expandTopFadeIn = fadeIn + expandVertically(defaultTween(), expandFrom = Alignment.Top) + val shrinkTopFadeOut = fadeOut + shrinkVertically(defaultTween(), shrinkTowards = Alignment.Top) + + // Functional Helpers + fun fadeOut(duration: Int): ExitTransition = fadeOut(tween(duration)) } /** @@ -252,12 +344,11 @@ fun BaseSettingsItem( */ @Composable fun SettingsItem( + modifier: Modifier = Modifier, icon: ImageVector, title: String, description: String? = null, onClick: () -> Unit, - @SuppressLint("ModifierParameter") - modifier: Modifier = Modifier, showBorder: Boolean = false ) { BaseSettingsItem( @@ -349,11 +440,10 @@ fun SectionTitle( */ @Composable fun CardHeader( + modifier: Modifier = Modifier, icon: ImageVector, title: String, - description: String? = null, - @SuppressLint("ModifierParameter") - modifier: Modifier = Modifier + description: String? = null ) { Column(modifier = modifier.fillMaxWidth()) { Surface( @@ -378,13 +468,12 @@ fun CardHeader( */ @Composable fun ExpandableSection( + modifier: Modifier = Modifier, icon: ImageVector? = null, title: String, description: String, expanded: Boolean, onExpandChange: (Boolean) -> Unit, - @SuppressLint("ModifierParameter") - modifier: Modifier = Modifier, content: @Composable () -> Unit ) { val rotationAngle by animateFloatAsState( @@ -423,8 +512,8 @@ fun ExpandableSection( // Content AnimatedVisibility( visible = expanded, - enter = expandVertically(tween(MorpheDefaults.ANIMATION_DURATION)) + fadeIn(tween(MorpheDefaults.ANIMATION_DURATION)), - exit = shrinkVertically(tween(MorpheDefaults.ANIMATION_DURATION)) + fadeOut(tween(MorpheDefaults.ANIMATION_DURATION)) + enter = MorpheAnimations.expandFadeEnter, + exit = MorpheAnimations.shrinkFadeExit ) { Column( modifier = Modifier diff --git a/app/src/main/java/app/morphe/manager/ui/screen/shared/backgrounds/AnimationSpecs.kt b/app/src/main/java/app/morphe/manager/ui/screen/shared/backgrounds/AnimationSpecs.kt index d4376ae13..0353efc7e 100644 --- a/app/src/main/java/app/morphe/manager/ui/screen/shared/backgrounds/AnimationSpecs.kt +++ b/app/src/main/java/app/morphe/manager/ui/screen/shared/backgrounds/AnimationSpecs.kt @@ -109,11 +109,8 @@ fun rememberParallaxState( val sensorManager = context.getSystemService(Context.SENSOR_SERVICE) as SensorManager val accelerometer = sensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER) - - if (accelerometer == null) { - // No accelerometer available + ?: // No accelerometer available return@DisposableEffect onDispose { } - } val listener = object : SensorEventListener { override fun onSensorChanged(event: SensorEvent) { diff --git a/app/src/main/java/app/morphe/manager/ui/screen/shared/backgrounds/ShapesBackground.kt b/app/src/main/java/app/morphe/manager/ui/screen/shared/backgrounds/ShapesBackground.kt index bd65479b4..e6d4e4211 100644 --- a/app/src/main/java/app/morphe/manager/ui/screen/shared/backgrounds/ShapesBackground.kt +++ b/app/src/main/java/app/morphe/manager/ui/screen/shared/backgrounds/ShapesBackground.kt @@ -182,13 +182,11 @@ private fun rotateVertex(v: Vec3, cosX: Float, sinX: Float, val z1 = v.y * sinX + v.z * cosX // Y axis (yaw) val x2 = x1 * cosY + z1 * sinY - val y2 = y1 val z2 = -x1 * sinY + z1 * cosY // Z axis (roll) - val x3 = x2 * cosZ - y2 * sinZ - val y3 = x2 * sinZ + y2 * cosZ - val z3 = z2 - return Vec3(x3, y3, z3) + val x3 = x2 * cosZ - y1 * sinZ + val y3 = x2 * sinZ + y1 * cosZ + return Vec3(x3, y3, z2) } /** Perspective-projects a rotated vertex to screen space. */ @@ -361,12 +359,12 @@ private enum class SolidType { ICOSAHEDRON -> { val phi = (1f + sqrt(5f)) / 2f val n = 1f / sqrt(1f + phi * phi) - val a = n; val b = n * phi + val b = n * phi SolidDef.build( vertices = listOf( - Vec3( 0f, a, b), Vec3( 0f, -a, b), Vec3( 0f, a, -b), Vec3( 0f, -a, -b), - Vec3( a, b, 0f), Vec3(-a, b, 0f), Vec3( a, -b, 0f), Vec3(-a, -b, 0f), - Vec3( b, 0f, a), Vec3(-b, 0f, a), Vec3( b, 0f, -a), Vec3(-b, 0f, -a) + Vec3( 0f, n, b), Vec3( 0f, -n, b), Vec3( 0f, n, -b), Vec3( 0f, -n, -b), + Vec3(n, b, 0f), Vec3(-n, b, 0f), Vec3(n, -b, 0f), Vec3(-n, -b, 0f), + Vec3( b, 0f, n), Vec3(-b, 0f, n), Vec3( b, 0f, -n), Vec3(-b, 0f, -n) ), faces = listOf( // Top cap (5 faces around vertex 4) diff --git a/app/src/main/java/app/morphe/manager/ui/screen/shared/backgrounds/SnowBackground.kt b/app/src/main/java/app/morphe/manager/ui/screen/shared/backgrounds/SnowBackground.kt index d3c50be26..5e9e6cbdb 100644 --- a/app/src/main/java/app/morphe/manager/ui/screen/shared/backgrounds/SnowBackground.kt +++ b/app/src/main/java/app/morphe/manager/ui/screen/shared/backgrounds/SnowBackground.kt @@ -167,8 +167,6 @@ fun SnowBackground( else -> baseX } - val centerY = baseY - // Get bitmap for this layer val bitmap = snowflakeBitmaps[flake.layer] val drawSize = bitmap.width.toFloat() * flake.size @@ -176,8 +174,8 @@ fun SnowBackground( // Calculate alpha with edge fade for seamless loop val depthAlpha = 0.35f + (flake.depth * 0.55f) val edgeFade = when { - centerY < 0f -> ((centerY + 50f) / 50f).coerceIn(0f, 1f) - centerY > height -> ((height + 50f - centerY) / 50f).coerceIn(0f, 1f) + baseY < 0f -> ((baseY + 50f) / 50f).coerceIn(0f, 1f) + baseY > height -> ((height + 50f - baseY) / 50f).coerceIn(0f, 1f) else -> 1f } @@ -186,10 +184,10 @@ fun SnowBackground( val finalAlpha = depthAlpha * (0.7f + flake.size * 0.3f) * edgeFade * cycleFade * burstFade // Only draw if visible - if (finalAlpha > 0.01f && centerY > -50f && centerY < height + 50f) { + if (finalAlpha > 0.01f && baseY > -50f && baseY < height + 50f) { drawIntoCanvas { canvas -> canvas.save() - canvas.translate(centerX, centerY) + canvas.translate(centerX, baseY) canvas.rotate(rotation) canvas.drawImageRect( diff --git a/app/src/main/java/app/morphe/manager/ui/screen/shared/backgrounds/SpaceBackground.kt b/app/src/main/java/app/morphe/manager/ui/screen/shared/backgrounds/SpaceBackground.kt index f7f707c68..7b0316543 100644 --- a/app/src/main/java/app/morphe/manager/ui/screen/shared/backgrounds/SpaceBackground.kt +++ b/app/src/main/java/app/morphe/manager/ui/screen/shared/backgrounds/SpaceBackground.kt @@ -101,7 +101,7 @@ fun SpaceBackground( // Regenerate stars that have passed the camera (adjustedProgress wraps to 0..1) stars.forEachIndexed { index, star -> val adjustedProgress = ((baseProgress * star.speed) + star.initialOffset) % 1f - if (adjustedProgress > 0.98f || adjustedProgress < 0.01f) { + if (adjustedProgress !in 0.01f..0.98f) { if (star.lastRegen != baseProgress.toInt()) { // Pick a new random position outside the centre exclusion zone var newX: Float; var newY: Float; var newDistance: Float @@ -158,7 +158,7 @@ fun SpaceBackground( stars.forEach { star -> val adjustedProgress = ((baseProgress * star.speed) + star.initialOffset) % 1f val z = (1f - adjustedProgress).coerceAtLeast(0.01f) - if (z < 0.05f || z > 1.5f) return@forEach + if (z !in 0.05f..1.5f) return@forEach val perspectiveFactor = 1f / z val baseX = star.x * width * 0.5f diff --git a/app/src/main/java/app/morphe/manager/ui/theme/Theme.kt b/app/src/main/java/app/morphe/manager/ui/theme/Theme.kt index 44a3a892d..7021df5eb 100644 --- a/app/src/main/java/app/morphe/manager/ui/theme/Theme.kt +++ b/app/src/main/java/app/morphe/manager/ui/theme/Theme.kt @@ -150,15 +150,14 @@ private fun applyCustomAccent( accent: Color, darkTheme: Boolean ): ColorScheme { - val primary = accent val primaryContainer = accent.adjustLightness(if (darkTheme) 0.25f else -0.25f) val secondary = accent.adjustLightness(if (darkTheme) 0.15f else -0.15f) val secondaryContainer = accent.adjustLightness(if (darkTheme) 0.35f else -0.35f) val tertiary = accent.adjustLightness(if (darkTheme) -0.1f else 0.1f) val tertiaryContainer = accent.adjustLightness(if (darkTheme) 0.4f else -0.4f) return colorScheme.copy( - primary = primary, - onPrimary = primary.contrastingForeground(), + primary = accent, + onPrimary = accent.contrastingForeground(), primaryContainer = primaryContainer, onPrimaryContainer = primaryContainer.contrastingForeground(), secondary = secondary, @@ -169,8 +168,8 @@ private fun applyCustomAccent( onTertiary = tertiary.contrastingForeground(), tertiaryContainer = tertiaryContainer, onTertiaryContainer = tertiaryContainer.contrastingForeground(), - surfaceTint = primary, - inversePrimary = primary.adjustLightness(if (darkTheme) -0.4f else 0.4f) + surfaceTint = accent, + inversePrimary = accent.adjustLightness(if (darkTheme) -0.4f else 0.4f) ) } diff --git a/app/src/main/java/app/morphe/manager/ui/viewmodel/AboutViewModel.kt b/app/src/main/java/app/morphe/manager/ui/viewmodel/AboutViewModel.kt index 9afd06401..86969663f 100644 --- a/app/src/main/java/app/morphe/manager/ui/viewmodel/AboutViewModel.kt +++ b/app/src/main/java/app/morphe/manager/ui/viewmodel/AboutViewModel.kt @@ -18,7 +18,7 @@ data class SocialLink( val preferred: Boolean = false, ) -class AboutViewModel() : ViewModel() { +class AboutViewModel : ViewModel() { companion object { val socials: List = listOf( SocialLink( diff --git a/app/src/main/java/app/morphe/manager/ui/viewmodel/HomeViewModel.kt b/app/src/main/java/app/morphe/manager/ui/viewmodel/HomeViewModel.kt index 600390f41..389c66ebf 100644 --- a/app/src/main/java/app/morphe/manager/ui/viewmodel/HomeViewModel.kt +++ b/app/src/main/java/app/morphe/manager/ui/viewmodel/HomeViewModel.kt @@ -18,10 +18,7 @@ import android.provider.OpenableColumns import android.util.Log import android.widget.Toast import androidx.annotation.RequiresApi -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableFloatStateOf -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.setValue +import androidx.compose.runtime.* import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import app.morphe.manager.R @@ -64,7 +61,6 @@ import java.io.File import java.io.FileNotFoundException import java.net.URLEncoder.encode import java.security.MessageDigest -import kotlin.collections.emptyList import kotlin.time.Clock /** Bundle update status for snackbar display. */ @@ -125,6 +121,15 @@ data class SavedApkInfo( val version: String ) +/** + * Combined home screen app state — emitted atomically so visible and hidden lists + * are always in sync and never cause a transient empty-state flash. + */ +data class HomeAppState( + val visible: List, + val hidden: List +) + /** * Manages all dialogs, user interactions, APK processing, and bundle management. */ @@ -141,17 +146,14 @@ class HomeViewModel( private val pm: PM, val rootInstaller: RootInstaller, private val filesystem: Filesystem, - val homeAppButtonPrefs: HomeAppButtonPreferences + private val homeAppButtonPrefs: HomeAppButtonPreferences, + private val appDataResolver: AppDataResolver ) : ViewModel() { val availablePatches = patchBundleRepository.bundleInfoFlow.map { it.values.sumOf { bundle -> bundle.patches.size } } val bundleUpdateProgress = patchBundleRepository.bundleUpdateProgress private val contentResolver: ContentResolver = app.contentResolver - // App data resolver for getting app info from APK files - private val appDataResolver by lazy { - AppDataResolver(app, pm, originalApkRepository, installedAppRepository, filesystem) - } - + /** Becomes true once the bundle repository has finished its initial DB load. */ /** Android 11 kills the app process after granting the "install apps" permission. */ val android11BugActive get() = Build.VERSION.SDK_INT == Build.VERSION_CODES.R && !pm.canInstallPackages() @@ -165,12 +167,55 @@ class HomeViewModel( var bundleToRename by mutableStateOf(null) var showRenameBundleDialog by mutableStateOf(false) + // Installed App Info dialog state + var showInstalledAppInfoDialog: String? by mutableStateOf(null) + private set + var installedAppDialogToken by mutableIntStateOf(0) + private set + + fun openInstalledAppInfo(packageName: String) { + showInstalledAppInfoDialog = packageName + installedAppDialogToken++ + } + + fun dismissInstalledAppInfo() { + showInstalledAppInfoDialog = null + } + // Deep link: pending bundle to add via confirmation dialog var deepLinkPendingBundle by mutableStateOf(null) private set data class DeepLinkBundle(val url: String, val name: String?) + // .mpp file opened from file manager: pending confirmation dialog + var pendingMppUri by mutableStateOf(null) + var pendingMppFileName by mutableStateOf(null) + var pendingMppManifest by mutableStateOf(null) + + fun setPendingMpp(uri: Uri) { + pendingMppUri = uri + pendingMppFileName = uri.displayName(contentResolver) + pendingMppManifest = null + viewModelScope.launch(Dispatchers.IO) { + pendingMppManifest = uri.readMppManifest(contentResolver) + } + } + + fun confirmMppImport() { + val uri = pendingMppUri ?: return + pendingMppUri = null + pendingMppFileName = null + pendingMppManifest = null + createLocalSource(uri) + } + + fun dismissMppImport() { + pendingMppUri = null + pendingMppFileName = null + pendingMppManifest = null + } + // Expert mode state var showExpertModeDialog by mutableStateOf(false) var expertModeSelectedApp by mutableStateOf(null) @@ -182,8 +227,6 @@ class HomeViewModel( var expertModeOptions by mutableStateOf(emptyMap()) // Patches that are new in the current bundle version relative to the last saved selection var expertModeNewPatches by mutableStateOf>>(emptyMap()) - // Bundle UIDs that have a non-empty saved selection in the DB for the current app - var expertModeSavedSelectionBundleUids by mutableStateOf>(emptySet()) /** * Set when ExpertModeDialog is opened from InstalledAppInfoDialog (repatch flow). @@ -210,6 +253,7 @@ class HomeViewModel( var showWrongPackageDialog by mutableStateOf(null) var showSplitApkWarningDialog by mutableStateOf(false) var showInvalidSignatureDialog by mutableStateOf(null) + var showNoCompatibleVersionsDialog by mutableStateOf(null) // packageName // Pending data during APK selection var pendingPackageName by mutableStateOf(null) @@ -273,8 +317,17 @@ class HomeViewModel( .mapNotNull { it.packageName } .toSet() + val deviceSdk = Build.VERSION.SDK_INT versionData.mapValues { (packageName, bundledTargets) -> - val targets = bundledTargets.map { it.target } + // Only consider versions whose minSdk is satisfied by the current device. + // Versions with no declared minSdk are always eligible + val compatibleTargets = bundledTargets + .map { it.target } + .filter { it.minSdk == null || deviceSdk >= it.minSdk!! } + + // Fall back to all targets if every version requires a higher SDK than this device + val targets = compatibleTargets.ifEmpty { bundledTargets.map { it.target } } + if (packageName in experimentalEnabledPackages) { // Experimental mode: prefer the highest experimental version, fallback to first targets.firstOrNull { it.isExperimental } ?: targets.first() @@ -314,17 +367,23 @@ class HomeViewModel( .toSet() } + val deviceSdk = Build.VERSION.SDK_INT versionData.mapValues { (packageName, bundledTargets) -> bundledTargets .groupBy { it.bundleUid } .mapValues { (bundleUid, targets) -> val appTargets = targets.map { it.target } + // Only consider versions compatible with the current device SDK + val compatibleTargets = appTargets + .filter { it.minSdk == null || deviceSdk >= it.minSdk!! } + // Fallback to all targets if none are SDK-compatible + val candidates = compatibleTargets.ifEmpty { appTargets } val preferExperimental = experimentalPackagesByBundle[bundleUid] ?.contains(packageName) == true if (preferExperimental) { - appTargets.firstOrNull { it.isExperimental } ?: appTargets.first() + candidates.firstOrNull { it.isExperimental } ?: candidates.first() } else { - appTargets.firstOrNull { !it.isExperimental } ?: appTargets.first() + candidates.firstOrNull { !it.isExperimental } ?: candidates.first() } } } @@ -337,6 +396,9 @@ class HomeViewModel( private val _appUpdatesAvailable = MutableStateFlow>(emptyMap()) val appUpdatesAvailable: StateFlow> = _appUpdatesAvailable.asStateFlow() + // Ticker to force homeAppState recomputation after install/uninstall without changing DB state + private val _appStateTicker = MutableStateFlow(0L) + // Track when at least one third-party source is enabled val hasThirdPartySource: StateFlow = patchBundleRepository.sources @@ -535,16 +597,27 @@ class HomeViewModel( viewModelScope.launch { combine( patchBundleRepository.bundleUpdateProgress, + patchBundleRepository.sources, installedAppRepository.getAll(), availablePatches - ) { progress, installedApps, patchCount -> + ) { progress, sources, installedApps, patchCount -> val isBundleUpdateInProgress = progress?.result == PatchBundleRepository.BundleUpdateResult.None - val hasLoadedData = installedApps.isNotEmpty() || patchCount > 0 + val hasEnabledSources = sources.any { it.enabled } + // Guard: sources list is empty on the very first emission before the DB is read. + // Treat that transient state as "still loading" so we never flash the empty-state + // UI before the real bundle configuration is known. + val sourcesInitialized = sources.isNotEmpty() || patchCount > 0 + // If no sources are enabled (and we know the DB has been read), there is nothing + // to load - this is a valid terminal state, not a loading state. + val hasLoadedData = sourcesInitialized && + (!hasEnabledSources || installedApps.isNotEmpty() || patchCount > 0) isBundleUpdateInProgress || !hasLoadedData - }.collect { loading -> - installedAppsLoading = loading } + .distinctUntilChanged() + .collect { loading -> + installedAppsLoading = loading + } } } @@ -914,6 +987,12 @@ class HomeViewModel( withContext(NonCancellable) { patchBundleRepository.createRemote(apiUrl, autoUpdate) } + patchBundleRepository.bundleUpdateProgress + .dropWhile { it == null } + .filter { it == null } + .first() + delay(1_500) + showSwipeGestureHint.value = true } /** @@ -948,45 +1027,41 @@ class HomeViewModel( } /** - * Per-package metadata aggregated from all enabled patch bundles. - * Provides display names, accent colors, APK type requirements, and valid signatures - * without relying on hardcoded constants for non-KnownApps packages. + * Metadata for all apps across enabled bundles - display names, icon colors, signatures, etc. + * Delegates to [PatchBundleRepository.appMetadata] as the single source of truth. */ val bundleAppMetadataFlow: StateFlow> = - patchBundleRepository.allBundlesInfoFlow - .map { bundleInfoMap -> BundleAppMetadata.buildFrom(bundleInfoMap) } - .stateIn(viewModelScope, SharingStarted.Eagerly, emptyMap()) - - /** - * Set of all unique package names that have patches across all enabled bundles. - * Derived from [bundleAppMetadataFlow] keys - no need to re-iterate all patches. - */ - val patchablePackagesFlow: StateFlow> = - bundleAppMetadataFlow - .map { it.keys } - .stateIn(viewModelScope, SharingStarted.Eagerly, emptySet()) + patchBundleRepository.appMetadata /** - * Hidden packages filtered to only those present in currently active bundles. - * When a bundle is disabled/removed, its packages disappear from this flow automatically. + * Combined flow that produces the sorted list of home app items. + * + * Sorting order by display name: + * 1. Patched (installed) apps first + * 2. Non-patched apps + * Hidden apps are excluded. */ - val filteredHiddenPackages: StateFlow> = combine( + val homeAppState: StateFlow = combine( + patchBundleRepository.bundleState, homeAppButtonPrefs.hiddenPackages, - patchablePackagesFlow - ) { hidden, active -> - hidden.filter { it in active }.toSet() - }.stateIn(viewModelScope, SharingStarted.Eagerly, emptySet()) + installedAppRepository.getAll(), + _appUpdatesAvailable, + _appStateTicker, + ) { bundleState, hiddenPackages, installedApps, updatesMap, _ -> + val ready = bundleState as? PatchBundleRepository.BundleState.Ready + ?: return@combine null - /** - * Hidden app items with resolved display names and package info. - */ - val hiddenAppItems: StateFlow> = combine( - filteredHiddenPackages, - bundleAppMetadataFlow - ) { hiddenPackages, metadata -> - hiddenPackages.map { packageName -> + val enabledInfo = ready.info.filter { (_, info) -> info.enabled } + val metadata = BundleAppMetadata.buildFrom(enabledInfo) + val packages = metadata.keys + + val installedMap = installedApps.associateBy { it.originalPackageName } + + suspend fun buildItem(packageName: String): HomeAppItem { + val installedApp = installedMap[packageName] val bundleMeta = metadata[packageName] val knownApp = KnownApps.fromPackage(packageName) + val gradientColors = bundleMeta?.gradientColors ?: KnownApps.DEFAULT_COLORS val resolvedData = appDataResolver.resolveAppData( packageName = packageName, preferredSource = AppDataSource.PATCHED_APK @@ -994,100 +1069,102 @@ class HomeViewModel( val displayName = resolvedData.displayName.takeIf { resolvedData.source == AppDataSource.INSTALLED || resolvedData.source == AppDataSource.PATCHED_APK } ?: bundleMeta?.displayName ?: KnownApps.getAppName(packageName) - val gradientColors = bundleMeta?.gradientColors ?: KnownApps.DEFAULT_COLORS - HomeAppItem( + val isDeleted = installedApp?.let { installed -> + val hasSavedCopy = listOf( + filesystem.getPatchedAppFile(installed.currentPackageName, installed.version), + filesystem.getPatchedAppFile(installed.originalPackageName, installed.version) + ).distinctBy { it.absolutePath }.any { it.exists() } + pm.isAppDeleted( + packageName = installed.currentPackageName, + hasSavedCopy = hasSavedCopy, + wasInstalledOnDevice = installed.installType != InstallType.SAVED + ) + } == true + val hasUpdate = installedApp?.let { + updatesMap[it.currentPackageName] == true + } == true + return HomeAppItem( packageName = packageName, displayName = displayName, gradientColors = gradientColors, - installedApp = null, + installedApp = installedApp, packageInfo = resolvedData.packageInfo, isPinnedByDefault = knownApp?.isPinnedByDefault == true, - isDeleted = false, - hasUpdate = false, + isDeleted = isDeleted, + hasUpdate = hasUpdate, patchCount = 0 ) } + + // Active bundle packages filtered to those in patchablePackages + val activeHidden = hiddenPackages.filter { it in packages } + + val visiblePackages = packages.filter { it !in hiddenPackages } + val visibleItems = ArrayList(visiblePackages.size) + for (pkg in visiblePackages) visibleItems.add(buildItem(pkg)) + val visible = visibleItems.sortedWith( + compareByDescending { it.installedApp != null } + .thenByDescending { it.isPinnedByDefault } + .thenByDescending { it.packageInfo != null } + .thenBy(String.CASE_INSENSITIVE_ORDER) { it.displayName } + ) + + val hiddenItems = ArrayList(activeHidden.size) + for (pkg in activeHidden) hiddenItems.add(buildItem(pkg)) + + HomeAppState(visible = visible, hidden = hiddenItems) } .flowOn(Dispatchers.IO) - .stateIn(viewModelScope, SharingStarted.Eagerly, emptyList()) + .stateIn(viewModelScope, SharingStarted.Eagerly, null) /** - * Combined flow that produces the sorted list of home app items. - * - * Sorting order by display name: - * 1. Patched (installed) apps first - * 2. Non-patched apps - * Hidden apps are excluded. + * Resets the swipe gesture hint after it has been shown. */ - val homeAppItems: StateFlow> = combine( - patchablePackagesFlow, - homeAppButtonPrefs.hiddenPackages, - installedAppRepository.getAll(), - _appUpdatesAvailable, - bundleAppMetadataFlow - ) { packages, hidden, installedApps, updatesMap, metadata -> - val installedMap = installedApps.associateBy { it.originalPackageName } - - packages - .filter { it !in hidden } - .map { packageName -> - val installedApp = installedMap[packageName] - val bundleMeta = metadata[packageName] - val knownApp = KnownApps.fromPackage(packageName) + fun markSwipeGestureHintShown() { + showSwipeGestureHint.value = false + } - // Gradient colors: bundle declared → default (colors come from patch bundle) - val gradientColors = bundleMeta?.gradientColors ?: KnownApps.DEFAULT_COLORS + /** + * Invalidates AppDataResolver cache for [packageName] and forces homeAppState recomputation. + * Call this after any install/uninstall operation that doesn't change the DB record. + */ + fun notifyAppStateChanged(packageName: String) { + appDataResolver.invalidate(packageName) + _appStateTicker.value = System.currentTimeMillis() + } - // Priority: PATCHED_APK → ORIGINAL_APK → INSTALLED → CONSTANTS - val resolvedData = appDataResolver.resolveAppData( - packageName = packageName, - preferredSource = AppDataSource.PATCHED_APK - ) + /** + * Snapshot of all bundle info (including disabled) as a [StateFlow] for synchronous reads. + * Used by [getPatchesForPackage] which is called from Compose (non-suspend context). + */ + private val allBundlesInfoState: StateFlow> = + patchBundleRepository.allBundlesInfoFlow + .stateIn(viewModelScope, SharingStarted.Eagerly, emptyMap()) - // Display name priority: installed/patched APK label → bundle declared → KnownApps fallback - val displayName = resolvedData.displayName.takeIf { - resolvedData.source == AppDataSource.INSTALLED || resolvedData.source == AppDataSource.PATCHED_APK - } ?: bundleMeta?.displayName ?: KnownApps.getAppName(packageName) - - // Determine deleted status - val isDeleted = installedApp?.let { installed -> - val hasSavedCopy = listOf( - filesystem.getPatchedAppFile(installed.currentPackageName, installed.version), - filesystem.getPatchedAppFile(installed.originalPackageName, installed.version) - ).distinctBy { it.absolutePath }.any { it.exists() } - pm.isAppDeleted( - packageName = installed.currentPackageName, - hasSavedCopy = hasSavedCopy, - wasInstalledOnDevice = installed.installType != InstallType.SAVED - ) - } == true - - // Determine update status - val hasUpdate = installedApp?.let { - updatesMap[it.currentPackageName] == true - } == true - - HomeAppItem( - packageName = packageName, - displayName = displayName, - gradientColors = gradientColors, - installedApp = installedApp, - packageInfo = resolvedData.packageInfo, - isPinnedByDefault = knownApp?.isPinnedByDefault == true, - isDeleted = isDeleted, - hasUpdate = hasUpdate, - patchCount = 0 - ) - } - .sortedWith( - compareByDescending { it.installedApp != null } - .thenByDescending { it.isPinnedByDefault } - .thenByDescending { it.packageInfo != null } - .thenBy(String.CASE_INSENSITIVE_ORDER) { it.displayName } - ) + /** + * Returns all patches available for [packageName] across all enabled bundles. + * Groups them as Map> for the swipe-right patches dialog. + */ + fun getPatchesForPackage(packageName: String): Map> { + val bundleInfo = allBundlesInfoState.value + return buildMap { + bundleInfo + .filter { (_, info) -> info.enabled } + .forEach { (uid, info) -> + val patches = info.patches.filter { patch -> + patch.compatiblePackages == null || + patch.compatiblePackages.any { it.packageName == packageName } + } + if (patches.isNotEmpty()) put(uid, patches) + } + } } - .flowOn(Dispatchers.IO) - .stateIn(viewModelScope, SharingStarted.Eagerly, emptyList()) + + /** + * Returns the display name of the bundle with [uid], or null. + */ + fun getBundleDisplayName(uid: Int): String? = + allBundlesInfoState.value[uid]?.name /** * Hide an app from the home screen. @@ -1115,17 +1192,20 @@ class HomeViewModel( ?.toSet() ?: emptySet() + /** Triggers the swipe gesture hint whenever a custom bundle is added. */ + val showSwipeGestureHint = MutableStateFlow(false) + /** * Whether the "Other apps" button should be visible. * Hidden while no apps are loaded; shown in expert mode or when a third-party source is active. */ val showOtherAppsButton: StateFlow = combine( - homeAppItems, + homeAppState, hasThirdPartySource, prefs.useExpertMode.flow - ) { items, thirdParty, expertMode -> - if (items.isEmpty()) false + ) { state, thirdParty, expertMode -> + if (state?.visible.isNullOrEmpty()) false else expertMode || thirdParty }.stateIn(viewModelScope, SharingStarted.Eagerly, false) @@ -1135,10 +1215,10 @@ class HomeViewModel( */ val showSearchButton: StateFlow = combine( - homeAppItems, + homeAppState, hasThirdPartySource - ) { items, thirdParty -> - items.size > 4 || thirdParty + ) { state, thirdParty -> + (state?.visible?.size ?: 0) > 4 || thirdParty }.stateIn(viewModelScope, SharingStarted.Eagerly, false) /** @@ -1222,6 +1302,19 @@ class HomeViewModel( } pendingSavedApkInfo = savedInfo + // Check if every declared version is incompatible with the current device SDK + val versions = compatibleVersions[packageName] ?: emptyList() + val deviceSdk = Build.VERSION.SDK_INT + val allIncompatible = versions.isNotEmpty() && + versions.all { b -> + val minSdk = b.target.minSdk + minSdk != null && deviceSdk < minSdk + } + if (allIncompatible) { + showNoCompatibleVersionsDialog = packageName + return + } + // Check if we should auto-use saved APK in simple mode val isExpertMode = prefs.useExpertMode.getBlocking() val recommendedVersion = recommendedVersions[packageName] @@ -1334,7 +1427,9 @@ class HomeViewModel( } if (selectedApp != null) { - processSelectedApp(selectedApp) + // The saved file is a merged mono-APK signed with our keystore. + // Skip signature verification to avoid a false "invalid signature" dialog + processSelectedAppIgnoringSignature(selectedApp) } else { cleanupPendingData() } @@ -1600,7 +1695,7 @@ class HomeViewModel( // Merge newly added patches (present in bundle but absent from saved selection) // into the validated selection, respecting each patch's include=true default. // This runs after validation so removed patches never sneak back in. - val mergedPatches = buildMap> { + val mergedPatches = buildMap { // Start from the validated (post-removal) selection putAll(validatedPatches) allBundles.forEach { bundle -> @@ -1964,117 +2059,6 @@ class HomeViewModel( } } - /** - * Initialise ExpertModeDialog for a repatch from InstalledAppInfoDialog. - * - * Loads saved patch selections/options for [originalPackageName], merges in any newly - * added patches, and populates the shared [expertMode*] state so the dialog rendered - * by HomeDialogs can be reused without duplicating logic. - * - * [onProceed] receives the final (patches, options) after the user confirms and is - * responsible for persisting the selection and navigating to the patcher screen. - */ - fun initRepatchExpertMode( - originalPackageName: String, - version: String, - allowIncompatible: Boolean, - onProceed: (patches: PatchSelection, options: Options) -> Unit - ) { - viewModelScope.launch { - val allBundles = withContext(Dispatchers.IO) { - patchBundleRepository - .scopedBundleInfoFlow(originalPackageName, version) - .first() - } - - if (allBundles.isEmpty()) { - app.toast(app.getString(R.string.home_no_patches_available)) - return@launch - } - - val bundlesMap = allBundles.associate { it.uid to it.patches.associateBy { patch -> patch.name } } - val currentBundleUids = allBundles.map { it.uid }.toSet() - - val savedSelections = withContext(Dispatchers.IO) { - patchSelectionRepository.getAllSelectionsForPackage(originalPackageName) - .filterKeys { it in currentBundleUids } - } - - val savedOptions = withContext(Dispatchers.IO) { - optionsRepository.getAllOptionsForPackage(originalPackageName, bundlesMap) - .filterKeys { it in currentBundleUids } - } - - val patches = if (savedSelections.isNotEmpty()) { - val patchesBeforeValidation = savedSelections.values.sumOf { it.size } - val validatedPatches = validatePatchSelection(savedSelections, bundlesMap) - val patchesAfterValidation = validatedPatches.values.sumOf { it.size } - val removedCount = patchesBeforeValidation - patchesAfterValidation - if (removedCount > 0) { - app.toast(app.resources.getQuantityString( - R.plurals.home_app_info_repatch_cleaned_invalid_data, - removedCount, - removedCount - )) - } - buildMap> { - putAll(validatedPatches) - allBundles.forEach { bundle -> - val seenForBundle = withContext(Dispatchers.IO) { - patchSelectionRepository.getSeenPatches(originalPackageName, bundle.uid) - } - val knownNames = seenForBundle - ?: savedSelections[bundle.uid] - ?: return@forEach - val newPatchNames = bundle.patches.map { it.name }.toSet() - knownNames - if (newPatchNames.isEmpty()) return@forEach - val newDefaultEnabled = bundle.patches - .filter { it.name in newPatchNames && it.include } - .mapTo(mutableSetOf()) { it.name } - if (newDefaultEnabled.isNotEmpty()) { - val existing = getOrDefault(bundle.uid, emptySet()) - put(bundle.uid, existing + newDefaultEnabled) - } - } - } - } else { - allBundles.toPatchSelection(allowIncompatible) { _, patch -> patch.include } - } - - val newPatchesMap: Map> = if (savedSelections.isNotEmpty()) { - buildMap { - allBundles.forEach { bundle -> - val seenForBundle = withContext(Dispatchers.IO) { - patchSelectionRepository.getSeenPatches(originalPackageName, bundle.uid) - } - val seen = seenForBundle ?: return@forEach - val currentPatchNames = bundle.patches.map { it.name }.toSet() - val newForBundle = currentPatchNames - seen - if (newForBundle.isNotEmpty()) put(bundle.uid, newForBundle) - } - } - } else { - emptyMap() - } - - val validatedOptions = validatePatchOptions(savedOptions, bundlesMap) - if (validatedOptions != savedOptions) { - withContext(Dispatchers.IO) { - optionsRepository.saveOptions(originalPackageName, validatedOptions) - } - } - - expertModeBundles = allBundles - patches.toMutableMap().also { expertModePatches = it; expertModeInitialPatches = it } - expertModeOptions = validatedOptions.toMutableMap() - expertModeNewPatches = newPatchesMap - expertModeSelectedApp = null // repatch has no SelectedApp - onRepatchProceed = onProceed - repatchPackageName = originalPackageName - showExpertModeDialog = true - } - } - /** * Resolve download redirect. */ @@ -2169,6 +2153,7 @@ class HomeViewModel( } pendingSelectedApp = null } + showApkAvailabilityDialog = false showDownloadInstructionsDialog = false showFilePickerPromptDialog = false } @@ -2178,11 +2163,16 @@ class HomeViewModel( * Returns a map of package name to a list of [BundledAppTarget] - versions are grouped by * bundle (ordered by bundle display name) and sorted newest→oldest within each bundle. * Versions are NOT deduplicated across bundles so the UI can show per-bundle sections. + * + * All declared versions are included regardless of [AppTarget.minSdk]. The minSdk value is + * preserved in [AppTarget.minSdk] so that: + * - [recommendedVersionsFlow] skips versions incompatible with the current device SDK. + * - The UI can render incompatible versions as greyed-out / non-selectable with a badge. */ private fun extractCompatibleVersions( bundleInfo: Map, bundleNames: Map, - enabledBundleUids: Set = emptySet() + enabledBundleUids: Set = emptySet(), ): Map> { // packageName → bundleUid → version → AppTarget val targetsByPackage = mutableMapOf>>() @@ -2202,10 +2192,12 @@ class HomeViewModel( // If a version appears in multiple patches of the same bundle, prefer stable if (version !in bundleMap || !isExperimental) { val description = pkg.versionDescriptions?.get(version) + val minSdk = pkg.versionMinSdks?.get(version) bundleMap[version] = AppTarget( version = version, isExperimental = isExperimental, - description = description + description = description, + minSdk = minSdk, ) } } @@ -2217,7 +2209,7 @@ class HomeViewModel( return targetsByPackage .mapValues { (_, byBundle) -> byBundle.entries - .sortedBy { (uid, _) -> bundleNames[uid] ?: "" } + .sortedWith(compareBy({ it.key != DEFAULT_SOURCE_UID }, { bundleNames[it.key] ?: "" })) .flatMap { (uid, versionMap) -> versionMap.values .sortedDescending() diff --git a/app/src/main/java/app/morphe/manager/ui/viewmodel/ImportExportViewModel.kt b/app/src/main/java/app/morphe/manager/ui/viewmodel/ImportExportViewModel.kt index 05e4580df..645f3698d 100644 --- a/app/src/main/java/app/morphe/manager/ui/viewmodel/ImportExportViewModel.kt +++ b/app/src/main/java/app/morphe/manager/ui/viewmodel/ImportExportViewModel.kt @@ -2,7 +2,11 @@ package app.morphe.manager.ui.viewmodel import android.app.ActivityManager import android.app.Application +import android.content.ContentValues import android.net.Uri +import android.os.Build +import android.os.Environment +import android.provider.MediaStore import android.util.Log import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue @@ -16,9 +20,7 @@ import app.morphe.manager.domain.manager.PreferencesManager import app.morphe.manager.domain.repository.PatchBundleRepository import app.morphe.manager.domain.repository.PatchOptionsRepository import app.morphe.manager.domain.repository.PatchSelectionRepository -import app.morphe.manager.util.tag -import app.morphe.manager.util.toast -import app.morphe.manager.util.uiSafe +import app.morphe.manager.util.* import com.github.pgreze.process.Redirect import com.github.pgreze.process.process import kotlinx.coroutines.CancellationException @@ -27,17 +29,22 @@ import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch import kotlinx.coroutines.withContext +import kotlinx.io.IOException import kotlinx.serialization.ExperimentalSerializationApi import kotlinx.serialization.Serializable import kotlinx.serialization.json.Json import kotlinx.serialization.json.decodeFromStream import kotlinx.serialization.json.encodeToStream +import java.io.BufferedWriter import java.io.File +import java.io.FileOutputStream +import java.io.OutputStream import java.nio.file.Files import java.nio.file.Path import java.nio.file.StandardCopyOption import java.time.LocalDateTime import java.time.format.DateTimeFormatter +import java.util.Locale import kotlin.io.path.deleteExisting import kotlin.io.path.inputStream @@ -102,7 +109,9 @@ class ImportExportViewModel( private val contentResolver = app.contentResolver private var keystoreImportPath by mutableStateOf(null) + private var keystoreImportFormat by mutableStateOf(KeystoreInputFormat.KEYSTORE) val showCredentialsDialog by derivedStateOf { keystoreImportPath != null } + val detectedKeystoreFormat: KeystoreInputFormat get() = keystoreImportFormat fun startKeystoreImport(content: Uri) = viewModelScope.launch { uiSafe(app, R.string.settings_system_import_keystore_failed, "Failed to import keystore") { @@ -116,16 +125,31 @@ class ImportExportViewModel( } } - // Try known aliases and passwords first - aliases.forEach { alias -> - knownPasswords.forEach { pass -> - if (tryKeystoreImport(alias, pass, path)) { - return@launch + // Detect format: magic bytes first (reliable), extension as fallback + val detectedFormat = withContext(Dispatchers.IO) { + val header = path.inputStream().use { stream -> + val buf = ByteArray(4) + stream.read(buf) + buf + } + KeystoreInputFormat.detectFromBytes(header) + } ?: run { + val ext = content.lastPathSegment?.substringAfterLast('.')?.lowercase() ?: "" + KeystoreInputFormat.fromExtension(ext) ?: KeystoreInputFormat.PKCS12 + } + keystoreImportFormat = detectedFormat + + // Try known aliases/passwords with all supported formats (detected format first) + val autoFormats = listOf(detectedFormat) + (KeystoreInputFormat.entries - detectedFormat) + for (format in autoFormats) { + for (alias in aliases) { + for (pass in knownPasswords) { + if (tryKeystoreImport(alias, pass, format, path)) return@launch } } } - // If automatic import fails, prompt user for credentials + // Auto-import failed - ask the user for credentials keystoreImportPath = path } } @@ -135,25 +159,45 @@ class ImportExportViewModel( keystoreImportPath = null } - suspend fun tryKeystoreImport(alias: String, pass: String): Boolean = - tryKeystoreImport(alias, pass, keystoreImportPath!!) + suspend fun tryKeystoreImport(alias: String, pass: String, format: KeystoreInputFormat): Boolean = + tryKeystoreImport(alias, pass, format, keystoreImportPath!!) + + private suspend fun tryKeystoreImport(alias: String, pass: String, format: KeystoreInputFormat, path: Path): Boolean { + // BKS is passed through as-is; converting it would re-encrypt the private key + // in a way that ApkSigner cannot read back + val bksBytes = if (format == KeystoreInputFormat.BKS || format == KeystoreInputFormat.KEYSTORE) { + withContext(Dispatchers.IO) { path.toFile().readBytes() } + } else { + val result = withContext(Dispatchers.IO) { + path.inputStream().use { KeystoreConversionUtils.convert(it, format, alias, pass) } + } + when (result) { + is KeystoreConversionResult.Error -> return false + is KeystoreConversionResult.Success -> result.data.toByteArray() + } + } - private suspend fun tryKeystoreImport(alias: String, pass: String, path: Path): Boolean { - path.inputStream().use { stream -> - if (keystoreManager.import(alias, pass, stream)) { + return try { + val imported = keystoreManager.import(alias, pass, bksBytes.inputStream()) + if (imported) { app.toast(app.getString(R.string.settings_system_import_keystore_success)) cancelKeystoreImport() - return true } + imported + } catch (_: IOException) { + false } - return false } fun canExport() = keystoreManager.hasKeystore() fun exportKeystore(target: Uri) = viewModelScope.launch { - keystoreManager.export(contentResolver.openOutputStream(target)!!) - app.toast(app.getString(R.string.settings_system_export_keystore_success)) + uiSafe(app, R.string.settings_system_export_keystore_failed, "Failed to export keystore") { + withContext(Dispatchers.IO) { + keystoreManager.export(contentResolver.openOutputStream(target)!!) + } + app.toast(app.getString(R.string.settings_system_export_keystore_success)) + } } fun importManagerSettings(source: Uri) = viewModelScope.launch { @@ -359,59 +403,67 @@ class ImportExportViewModel( return "morphe_logcat_$time.log" } - fun exportDebugLogs(target: Uri) = viewModelScope.launch { - val exitCode = try { - withContext(Dispatchers.IO) { - contentResolver.openOutputStream(target)!!.bufferedWriter().use { writer -> + /** + * Writes the debug log content to [writer]. Returns the logcat exit code. + * Must be called from within a [Dispatchers.IO] context. + * Shared by [exportDebugLogs]. + */ + private suspend fun writeDebugLogContent(writer: BufferedWriter): Int = withContext(Dispatchers.IO) { + val versionName = runCatching { + app.packageManager.getPackageInfo(app.packageName, 0).versionName + }.getOrDefault("unknown") + + writer.write("=== Morphe Manager Debug Log ===\n") + writer.write("Date : ${LocalDateTime.now()}\n") + writer.write("Version : $versionName\n") + + writer.write("\n--- Device ---\n") + writer.write("Model : ${Build.MANUFACTURER} ${Build.MODEL}\n") + writer.write("Android : ${Build.VERSION.RELEASE} (SDK ${Build.VERSION.SDK_INT})\n") + writer.write("ABI : ${Build.SUPPORTED_ABIS.joinToString()}\n") + writer.write("Locale : ${Locale.getDefault().toLanguageTag()}\n") + + writer.write("\n--- Memory ---\n") + val activityManager = app.getSystemService(ActivityManager::class.java) + val memInfo = ActivityManager.MemoryInfo().also { activityManager.getMemoryInfo(it) } + val toMb = { bytes: Long -> bytes / 1024 / 1024 } + writer.write("RAM avail : ${toMb(memInfo.availMem)} MB / ${toMb(memInfo.totalMem)} MB\n") + writer.write("Low memory : ${memInfo.lowMemory}\n") + writer.write("Low mem thr: ${toMb(memInfo.threshold)} MB\n") + + writer.write("\n--- Storage ---\n") + val internalDir = app.filesDir + val toMbL = { bytes: Long -> bytes / 1024 / 1024 } + writer.write("Internal : ${toMbL(internalDir.freeSpace)} MB free / ${toMbL(internalDir.totalSpace)} MB total\n") + val externalDir = app.getExternalFilesDir(null) + if (externalDir != null) { + writer.write("External : ${toMbL(externalDir.freeSpace)} MB free / ${toMbL(externalDir.totalSpace)} MB total\n") + } - val versionName = runCatching { - app.packageManager.getPackageInfo(app.packageName, 0).versionName - }.getOrDefault("unknown") - - writer.write("=== Morphe Manager Debug Log ===\n") - writer.write("Date : ${LocalDateTime.now()}\n") - writer.write("Version : $versionName\n") - - writer.write("\n--- Device ---\n") - writer.write("Model : ${android.os.Build.MANUFACTURER} ${android.os.Build.MODEL}\n") - writer.write("Android : ${android.os.Build.VERSION.RELEASE} (SDK ${android.os.Build.VERSION.SDK_INT})\n") - writer.write("ABI : ${android.os.Build.SUPPORTED_ABIS.joinToString()}\n") - writer.write("Locale : ${java.util.Locale.getDefault().toLanguageTag()}\n") - - writer.write("\n--- Memory ---\n") - val activityManager = app.getSystemService(ActivityManager::class.java) - val memInfo = ActivityManager.MemoryInfo().also { activityManager.getMemoryInfo(it) } - val toMb = { bytes: Long -> bytes / 1024 / 1024 } - writer.write("RAM avail : ${toMb(memInfo.availMem)} MB / ${toMb(memInfo.totalMem)} MB\n") - writer.write("Low memory : ${memInfo.lowMemory}\n") - writer.write("Low mem thr: ${toMb(memInfo.threshold)} MB\n") - - writer.write("\n--- Storage ---\n") - val internalDir = app.filesDir - val toMbL = { bytes: Long -> bytes / 1024 / 1024 } - writer.write("Internal : ${toMbL(internalDir.freeSpace)} MB free / ${toMbL(internalDir.totalSpace)} MB total\n") - val externalDir = app.getExternalFilesDir(null) - if (externalDir != null) { - writer.write("External : ${toMbL(externalDir.freeSpace)} MB free / ${toMbL(externalDir.totalSpace)} MB total\n") - } + writer.write("\n--- Environment ---\n") + val hasRoot = runCatching { + Runtime.getRuntime().exec(arrayOf("su", "-c", "id")).waitFor() == 0 + }.getOrDefault(false) + writer.write("Root access: $hasRoot\n") - writer.write("\n--- Environment ---\n") - val hasRoot = runCatching { - Runtime.getRuntime().exec(arrayOf("su", "-c", "id")).waitFor() == 0 - }.getOrDefault(false) - writer.write("Root access: $hasRoot\n") + writer.write("\n=== Logcat ===\n\n") - writer.write("\n=== Logcat ===\n\n") + val consumer = Redirect.Consume { flow -> + flow + .onEach { line -> writer.write("$line\n") } + .flowOn(Dispatchers.IO) + .collect { } + } - val consumer = Redirect.Consume { flow -> - flow - .onEach { line -> writer.write("$line\n") } - .flowOn(Dispatchers.IO) - .collect { } - } + // Filter logs by current process UID to include only Morphe Manager logs + process("logcat", "-d", "--uid=${app.applicationInfo.uid}", stdout = consumer).resultCode + } - // Filter logs by current process UID to include only Morphe Manager logs - process("logcat", "-d", "--uid=${app.applicationInfo.uid}", stdout = consumer).resultCode + fun exportDebugLogs(target: Uri) = viewModelScope.launch { + val exitCode = try { + withContext(Dispatchers.IO) { + contentResolver.openOutputStream(target)!!.bufferedWriter().use { writer -> + writeDebugLogContent(writer) } } } catch (e: CancellationException) { @@ -429,6 +481,79 @@ class ImportExportViewModel( } } + /** + * Opens a writable [OutputStream] to a new file in the public Downloads folder. + * Used as a fallback on Android TV where [android.content.Intent.ACTION_CREATE_DOCUMENT] is unavailable. + * On API 29+ uses [MediaStore] (no storage permission required). + * On older versions falls back to [Environment.getExternalStoragePublicDirectory]. + */ + private fun openDownloadsOutputStream(fileName: String, mimeType: String): OutputStream? { + return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + val values = ContentValues().apply { + put(MediaStore.Downloads.DISPLAY_NAME, fileName) + put(MediaStore.Downloads.MIME_TYPE, mimeType) + put(MediaStore.Downloads.RELATIVE_PATH, Environment.DIRECTORY_DOWNLOADS) + } + val uri = contentResolver.insert(MediaStore.Downloads.EXTERNAL_CONTENT_URI, values) + ?: return null + contentResolver.openOutputStream(uri) + } else { + @Suppress("DEPRECATION") + val dir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS) + dir.mkdirs() + FileOutputStream(File(dir, fileName)) + } + } + + /** + * Exports the keystore to the Downloads folder. + */ + fun exportKeystoreToDownloads() = viewModelScope.launch { + uiSafe(app, R.string.settings_system_export_keystore_failed, "Failed to export keystore to Downloads") { + withContext(Dispatchers.IO) { + val stream = openDownloadsOutputStream("Morphe.keystore", BIN_MIMETYPE) + ?: throw IllegalStateException("Cannot open Downloads output stream") + stream.use { keystoreManager.export(it) } + } + app.toast(app.getString(R.string.settings_system_export_keystore_success)) + } + } + + /** + * Exports manager settings to the Downloads folder. + */ + fun exportManagerSettingsToDownloads() = viewModelScope.launch { + uiSafe(app, R.string.settings_system_export_manager_settings_fail, "Failed to export settings to Downloads") { + val snapshot = preferencesManager.exportSettings() + val bundles = withContext(Dispatchers.IO) { patchBundleRepository.exportCustomBundles() } + withContext(Dispatchers.IO) { + val stream = openDownloadsOutputStream("morphe_manager_settings.json", JSON_MIMETYPE) + ?: throw IllegalStateException("Cannot open Downloads output stream") + stream.use { + json.encodeToStream( + ManagerSettingsExportFile( + settings = snapshot.copy(customBundles = bundles.ifEmpty { null }) + ), + it + ) + } + } + app.toast(app.getString(R.string.settings_system_export_manager_settings_success)) + } + } + + /** + * Exports debug logs to the Downloads folder. + */ + fun exportDebugLogsToDownloads() = viewModelScope.launch { + uiSafe(app, R.string.settings_system_export_debug_logs_export_failed, "Failed to export debug logs to Downloads") { + val stream = openDownloadsOutputStream(debugLogFileName, TEXT_MIMETYPE) + ?: throw IllegalStateException("Cannot open Downloads output stream") + stream.bufferedWriter().use { writer -> writeDebugLogContent(writer) } + app.toast(app.getString(R.string.settings_system_export_debug_logs_export_success)) + } + } + override fun onCleared() { super.onCleared() cancelKeystoreImport() diff --git a/app/src/main/java/app/morphe/manager/ui/viewmodel/InstallViewModel.kt b/app/src/main/java/app/morphe/manager/ui/viewmodel/InstallViewModel.kt index f31071182..addb94467 100644 --- a/app/src/main/java/app/morphe/manager/ui/viewmodel/InstallViewModel.kt +++ b/app/src/main/java/app/morphe/manager/ui/viewmodel/InstallViewModel.kt @@ -1,34 +1,28 @@ package app.morphe.manager.ui.viewmodel -import android.annotation.SuppressLint import android.app.Application import android.content.* import android.content.pm.PackageInfo -import android.content.pm.PackageInstaller -import android.content.pm.PackageManager import android.net.Uri -import android.os.Build import android.util.Log import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue import androidx.core.content.ContextCompat -import androidx.core.net.toUri import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import app.morphe.manager.R import app.morphe.manager.data.room.apps.installed.InstallType -import app.morphe.manager.domain.installer.InstallerManager -import app.morphe.manager.domain.installer.RootInstaller -import app.morphe.manager.domain.installer.ShizukuInstaller +import app.morphe.manager.domain.installer.* import app.morphe.manager.domain.manager.PreferencesManager -import app.morphe.manager.service.InstallService +import app.morphe.manager.util.AppDataResolver import app.morphe.manager.util.PM import app.morphe.manager.util.simpleMessage import app.morphe.manager.util.toast import kotlinx.coroutines.* import org.koin.core.component.KoinComponent import org.koin.core.component.inject +import ru.solrudev.ackpine.installer.InstallFailure import java.io.File import java.io.IOException import java.nio.file.Files @@ -41,9 +35,10 @@ class InstallViewModel : ViewModel(), KoinComponent { private val app: Application by inject() private val pm: PM by inject() private val rootInstaller: RootInstaller by inject() - private val shizukuInstaller: ShizukuInstaller by inject() + private val ackpineInstaller: AckpineInstaller by inject() private val installerManager: InstallerManager by inject() private val prefs: PreferencesManager by inject() + private val appDataResolver: AppDataResolver by inject() /** * Current installation state. @@ -97,10 +92,6 @@ class InstallViewModel : ViewModel(), KoinComponent { var mountOperation: MountOperation? by mutableStateOf(null) private set - private var awaitingPackageName: String? = null - private var installTimeoutJob: Job? = null - private var isWaitingForUninstall = false - // For external installer monitoring private var pendingExternalInstall: InstallerManager.InstallPlan.External? = null private var externalInstallTimeoutJob: Job? = null @@ -117,65 +108,16 @@ class InstallViewModel : ViewModel(), KoinComponent { var currentInstallType: InstallType = InstallType.DEFAULT private set - // Broadcast receiver for install results - private val installReceiver = object : BroadcastReceiver() { + // Broadcast receiver - only needed for external (third-party installer app) install monitoring. + // Internal/Shizuku results come directly from Ackpine's session.await(). + // Uninstall is also handled via Ackpine. + private val packageReceiver = object : BroadcastReceiver() { override fun onReceive(context: Context?, intent: Intent?) { when (intent?.action) { Intent.ACTION_PACKAGE_ADDED, Intent.ACTION_PACKAGE_REPLACED -> { val pkg = intent.data?.schemeSpecificPart ?: return - - // Check external installation first - if (handleExternalInstallSuccess(pkg)) return - - if (pkg == awaitingPackageName) { - handleInstallSuccess(pkg) - } - } - - Intent.ACTION_PACKAGE_REMOVED -> { - val pkg = intent.data?.schemeSpecificPart ?: return - if (isWaitingForUninstall && pkg == awaitingPackageName) { - handleUninstallComplete() - } - } - - InstallService.APP_INSTALL_ACTION -> { - val pmStatus = intent.getIntExtra( - InstallService.EXTRA_INSTALL_STATUS, - PackageInstaller.STATUS_FAILURE - ) - - when (pmStatus) { - PackageInstaller.STATUS_PENDING_USER_ACTION -> { - // User needs to confirm - keep installing state - } - PackageInstaller.STATUS_SUCCESS -> { - val packageName = intent.getStringExtra(InstallService.EXTRA_PACKAGE_NAME) - if (packageName != null) { - handleInstallSuccess(packageName) - } else { - awaitingPackageName?.let { handleInstallSuccess(it) } - } - } - else -> { - val message = intent.getStringExtra(InstallService.EXTRA_INSTALL_STATUS_MESSAGE) - ?.takeIf { it.isNotBlank() } - - // Check for signature mismatch - if (installerManager.isSignatureMismatch(message)) { - awaitingPackageName?.let { pkg -> - installState = InstallState.Conflict(pkg) - } - return - } - - val formatted = installerManager.formatFailureHint(pmStatus, message) - handleInstallError( - formatted ?: message ?: app.getString(R.string.install_app_fail, pmStatus.toString()) - ) - } - } + handleExternalInstallSuccess(pkg) } } } @@ -184,12 +126,10 @@ class InstallViewModel : ViewModel(), KoinComponent { init { ContextCompat.registerReceiver( app, - installReceiver, + packageReceiver, IntentFilter().apply { - addAction(InstallService.APP_INSTALL_ACTION) addAction(Intent.ACTION_PACKAGE_ADDED) addAction(Intent.ACTION_PACKAGE_REPLACED) - addAction(Intent.ACTION_PACKAGE_REMOVED) addDataScheme("package") }, ContextCompat.RECEIVER_NOT_EXPORTED @@ -199,11 +139,10 @@ class InstallViewModel : ViewModel(), KoinComponent { override fun onCleared() { super.onCleared() try { - app.unregisterReceiver(installReceiver) + app.unregisterReceiver(packageReceiver) } catch (e: Exception) { Log.e(TAG, "Failed to unregister receiver", e) } - installTimeoutJob?.cancel() externalInstallTimeoutJob?.cancel() pendingExternalInstall?.let(installerManager::cleanup) } @@ -240,7 +179,6 @@ class InstallViewModel : ViewModel(), KoinComponent { ?: throw Exception("Failed to load application info") val targetPackageName = packageInfo.packageName - awaitingPackageName = targetPackageName // Check if app is already installed val existingInfo = pm.getPackageInfo(targetPackageName) @@ -252,11 +190,10 @@ class InstallViewModel : ViewModel(), KoinComponent { installState = InstallState.Conflict(targetPackageName) return@launch } - - // Check for signature conflict - val hasConflict = hasSignatureConflict(outputFile, targetPackageName) - if (hasConflict) { - Log.i(TAG, "Signature conflict detected for $targetPackageName") + // Check signature mismatch before launching the installer - avoids + // INSTALL_FAILED_UPDATE_INCOMPATIBLE from the system PackageInstaller + if (pm.hasSignatureMismatch(targetPackageName, outputFile)) { + Log.i(TAG, "Signature mismatch detected for $targetPackageName - showing conflict") installState = InstallState.Conflict(targetPackageName) return@launch } @@ -378,7 +315,7 @@ class InstallViewModel : ViewModel(), KoinComponent { is InstallerManager.InstallPlan.Internal -> { Log.d(TAG, "Using internal (standard) installer") currentInstallType = InstallType.DEFAULT - performStandardInstall(outputFile, originalPackageName) + performStandardInstall(outputFile, originalPackageName, onPersistApp) } is InstallerManager.InstallPlan.Shizuku -> { @@ -407,81 +344,85 @@ class InstallViewModel : ViewModel(), KoinComponent { } /** - * Standard PackageInstaller installation. + * Internal (standard PackageInstaller) installation via Ackpine. + * Suspends until the user confirms or cancels the system dialog. */ private suspend fun performStandardInstall( outputFile: File, - originalPackageName: String + originalPackageName: String, + onPersistApp: suspend (String, InstallType) -> Boolean ) { + val packageInfo = pm.getPackageInfo(outputFile) + ?: throw Exception("Failed to load application info") + val targetPackageName = packageInfo.packageName + // Unmount if mounted as root if (rootInstaller.hasRootAccess() && rootInstaller.isAppMounted(originalPackageName)) { rootInstaller.unmount(originalPackageName) } - // Start system installation - pm.installApp(listOf(outputFile)) + val failure = try { + ackpineInstaller.installInternal(outputFile) + } catch (_: InstallCancelledException) { + // User dismissed the dialog - go back to Ready immediately, no error shown + installState = InstallState.Ready + return + } - // Set timeout - installTimeoutJob?.cancel() - installTimeoutJob = viewModelScope.launch { - delay(INSTALL_TIMEOUT_MS) - if (installState is InstallState.Installing) { - handleInstallError(app.getString(R.string.install_timeout_message)) + when (failure) { + null -> { + onPersistApp(targetPackageName, InstallType.DEFAULT) + handleInstallSuccess(targetPackageName) } + is InstallFailure.Conflict -> { + Log.i(TAG, "Signature conflict detected for $targetPackageName by Ackpine") + installState = InstallState.Conflict(targetPackageName) + } + else -> handleInstallError( + app.getString(R.string.install_app_fail, failure.message ?: failure.javaClass.simpleName) + ) } } /** - * Shizuku installation. + * Shizuku installation via Ackpine's ShizukuPlugin. + * Suspends until the system confirms or cancels. */ private suspend fun performShizukuInstall( outputFile: File, onPersistApp: suspend (String, InstallType) -> Boolean ) { - try { - val packageInfo = pm.getPackageInfo(outputFile) - ?: throw Exception("Failed to load application info") + val packageInfo = pm.getPackageInfo(outputFile) + ?: throw Exception("Failed to load application info") + val targetPackageName = packageInfo.packageName - val targetPackageName = packageInfo.packageName + if (rootInstaller.hasRootAccess() && rootInstaller.isAppMounted(targetPackageName)) { + rootInstaller.unmount(targetPackageName) + } - // Unmount if mounted as root - if (rootInstaller.hasRootAccess() && rootInstaller.isAppMounted(targetPackageName)) { - rootInstaller.unmount(targetPackageName) - } + Log.d(TAG, "Starting Shizuku install for $targetPackageName") - Log.d(TAG, "Starting Shizuku install for $targetPackageName") - val result = shizukuInstaller.install(outputFile, targetPackageName) + val failure = try { + ackpineInstaller.installShizuku(outputFile) + } catch (_: InstallCancelledException) { + installState = InstallState.Ready + return + } - if (result.status == PackageInstaller.STATUS_SUCCESS) { + when (failure) { + null -> { Log.d(TAG, "Shizuku install successful") - - // Persist app data with SHIZUKU type - try { - onPersistApp(targetPackageName, InstallType.SHIZUKU) - Log.d(TAG, "Persisted app with InstallType: SHIZUKU") - } catch (e: Exception) { - Log.e(TAG, "Failed to persist app data", e) - } - + onPersistApp(targetPackageName, InstallType.SHIZUKU) installedPackageName = targetPackageName installState = InstallState.Installed(targetPackageName) app.toast(app.getString(R.string.install_app_success)) - } else { - val message = result.message ?: app.getString(R.string.installer_hint_generic) - Log.e(TAG, "Shizuku install failed: $message") - handleInstallError(app.getString(R.string.install_app_fail, message)) } - } catch (e: ShizukuInstaller.InstallerOperationException) { - val message = e.message ?: app.getString(R.string.installer_hint_generic) - Log.e(TAG, "Shizuku install exception", e) - handleInstallError(app.getString(R.string.install_app_fail, message)) - } catch (e: Exception) { - Log.e(TAG, "Shizuku install failed", e) - handleInstallError( - app.getString( - R.string.install_app_fail, - e.simpleMessage() ?: e.javaClass.simpleName - ) + is InstallFailure.Conflict -> { + Log.i(TAG, "Signature conflict detected for $targetPackageName by Ackpine") + installState = InstallState.Conflict(targetPackageName) + } + else -> handleInstallError( + app.getString(R.string.install_app_fail, failure.message ?: failure.javaClass.simpleName) ) } } @@ -814,16 +755,13 @@ class InstallViewModel : ViewModel(), KoinComponent { val file = pendingInstallFile ?: return val originalPkg = pendingOriginalPackageName ?: return + val callback = pendingPersistCallback ?: return viewModelScope.launch { installState = InstallState.Installing - currentInstallType = InstallType.DEFAULT // Standard installer + currentInstallType = InstallType.DEFAULT try { - val packageInfo = pm.getPackageInfo(file) - ?: throw Exception("Failed to load application info") - - awaitingPackageName = packageInfo.packageName - performStandardInstall(file, originalPkg) + performStandardInstall(file, originalPkg, callback) } catch (e: Exception) { Log.e(TAG, "Fallback install failed", e) handleInstallError( @@ -859,150 +797,62 @@ class InstallViewModel : ViewModel(), KoinComponent { install(file, originalPkg, callback) } - private suspend fun hasSignatureConflict(apkFile: File, packageName: String): Boolean = - withContext(Dispatchers.IO) { + /** + * Uninstalls a package via Ackpine. Shows the system confirmation dialog and + * suspends until the user confirms or cancels. + */ + fun requestUninstall(packageName: String) { + viewModelScope.launch { try { - if (pm.getPackageInfo(packageName) == null) { - return@withContext false - } - - val installedSignatures = getInstalledPackageSignatures(packageName) - val apkSignatures = getApkFileSignatures(apkFile) - - if (installedSignatures.isEmpty() || apkSignatures.isEmpty()) { - return@withContext false - } - - val signaturesMatch = installedSignatures.any { installed -> - apkSignatures.any { apk -> installed.contentEquals(apk) } - } - - !signaturesMatch + ackpineInstaller.uninstall(packageName) + // After successful uninstall, reset install state + delay(300) + installState = InstallState.Ready + installedPackageName = null + } catch (_: UninstallCancelledException) { + // User dismissed - stay in current state silently } catch (e: Exception) { - Log.e(TAG, "Error checking signature conflict", e) - false + Log.e(TAG, "Uninstall failed", e) + app.toast(app.getString(R.string.install_app_fail, e.simpleMessage())) } } - - @Suppress("DEPRECATION") - private fun getInstalledPackageSignatures(packageName: String): List { - return try { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { - val packageInfo = app.packageManager.getPackageInfo( - packageName, - PackageManager.GET_SIGNING_CERTIFICATES - ) - val signingInfo = packageInfo.signingInfo - if (signingInfo != null) { - if (signingInfo.hasMultipleSigners()) { - signingInfo.apkContentsSigners.map { it.toByteArray() } - } else { - signingInfo.signingCertificateHistory?.map { it.toByteArray() } ?: emptyList() - } - } else emptyList() - } else { - val packageInfo = app.packageManager.getPackageInfo( - packageName, - PackageManager.GET_SIGNATURES - ) - packageInfo.signatures?.map { it.toByteArray() } ?: emptyList() - } - } catch (_: PackageManager.NameNotFoundException) { - emptyList() - } catch (e: Exception) { - Log.e(TAG, "Error getting installed package signatures", e) - emptyList() - } } - @Suppress("DEPRECATION") - private fun getApkFileSignatures(apkFile: File): List { - return try { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { - val packageInfo = app.packageManager.getPackageArchiveInfo( - apkFile.absolutePath, - PackageManager.GET_SIGNING_CERTIFICATES - ) - if (packageInfo != null) { - val signingInfo = packageInfo.signingInfo - if (signingInfo != null) { - if (signingInfo.hasMultipleSigners()) { - signingInfo.apkContentsSigners.map { it.toByteArray() } - } else { - signingInfo.signingCertificateHistory?.map { it.toByteArray() } ?: emptyList() - } - } else emptyList() - } else emptyList() - } else { - val packageInfo = app.packageManager.getPackageArchiveInfo( - apkFile.absolutePath, - PackageManager.GET_SIGNATURES - ) - packageInfo?.signatures?.map { it.toByteArray() } ?: emptyList() - } - } catch (e: Exception) { - Log.e(TAG, "Error getting APK signatures", e) - emptyList() - } + fun openApp() { + installedPackageName?.let { pm.launch(it) } } - @SuppressLint("UseKtx") - fun requestUninstall(packageName: String) { - isWaitingForUninstall = true - awaitingPackageName = packageName - - val intent = Intent(Intent.ACTION_DELETE).apply { - data = "package:$packageName".toUri() - flags = Intent.FLAG_ACTIVITY_NEW_TASK - } - app.startActivity(intent) + /** + * Returns installer entries for the one-time selection dialog shown during install. + * Mirrors the logic in SettingsViewModel but scoped to PATCHER target only. + */ + fun getInstallerOptions(): List { + val token = installerManager.getPrimaryToken() + val raw = installerManager.listEntries(InstallerManager.InstallTarget.PATCHER, includeNone = false) + return installerManager.ensureValidEntries(raw, token, InstallerManager.InstallTarget.PATCHER) } - fun openApp() { - installedPackageName?.let { pm.launch(it) } - } + fun getPrimaryInstallerToken(): InstallerManager.Token = + installerManager.getPrimaryToken() + + fun openShizukuApp(): Boolean = installerManager.openShizukuApp() private fun handleInstallSuccess(packageName: String) { - installTimeoutJob?.cancel() externalInstallTimeoutJob?.cancel() - awaitingPackageName = null - isWaitingForUninstall = false selectedInstallerToken = null installedPackageName = packageName + appDataResolver.invalidate(packageName) installState = InstallState.Installed(packageName) - - pendingPersistCallback?.let { callback -> - val installType = currentInstallType - viewModelScope.launch { - try { - callback(packageName, installType) - Log.d(TAG, "Persisted app with InstallType: $installType") - } catch (e: Exception) { - Log.e(TAG, "Failed to persist app data", e) - } - } - } } private fun handleInstallError(message: String) { - installTimeoutJob?.cancel() externalInstallTimeoutJob?.cancel() - awaitingPackageName = null selectedInstallerToken = null installState = InstallState.Error(message) } - private fun handleUninstallComplete() { - viewModelScope.launch { - delay(500) - isWaitingForUninstall = false - installState = InstallState.Ready - } - } - companion object { private const val TAG = "Morphe Install" - private const val INSTALL_TIMEOUT_MS = 240_000L private const val EXTERNAL_INSTALL_TIMEOUT_MS = 60_000L private const val INSTALL_MONITOR_POLL_MS = 1000L } diff --git a/app/src/main/java/app/morphe/manager/ui/viewmodel/InstalledAppInfoViewModel.kt b/app/src/main/java/app/morphe/manager/ui/viewmodel/InstalledAppInfoViewModel.kt index 4af41934f..d905135bb 100644 --- a/app/src/main/java/app/morphe/manager/ui/viewmodel/InstalledAppInfoViewModel.kt +++ b/app/src/main/java/app/morphe/manager/ui/viewmodel/InstalledAppInfoViewModel.kt @@ -11,8 +11,10 @@ import app.morphe.manager.R import app.morphe.manager.data.platform.Filesystem import app.morphe.manager.data.room.apps.installed.InstallType import app.morphe.manager.data.room.apps.installed.InstalledApp +import app.morphe.manager.domain.installer.AckpineInstaller import app.morphe.manager.domain.installer.InstallerManager import app.morphe.manager.domain.installer.RootInstaller +import app.morphe.manager.domain.installer.UninstallCancelledException import app.morphe.manager.domain.repository.* import app.morphe.manager.ui.screen.home.AppliedPatchBundleUi import app.morphe.manager.util.* @@ -34,11 +36,13 @@ class InstalledAppInfoViewModel( private val installedAppRepository: InstalledAppRepository by inject() private val patchBundleRepository: PatchBundleRepository by inject() private val rootInstaller: RootInstaller by inject() + private val ackpineInstaller: AckpineInstaller by inject() private val installerManager: InstallerManager by inject() private val originalApkRepository: OriginalApkRepository by inject() private val filesystem: Filesystem by inject() lateinit var onBackClick: () -> Unit + var onAppStateChanged: ((packageName: String) -> Unit)? = null var installedApp: InstalledApp? by mutableStateOf(null) private set @@ -73,7 +77,7 @@ class InstalledAppInfoViewModel( if (app != null) { // Run all checks in parallel - val deferredMounted = async { rootInstaller.isAppMounted(app.currentPackageName) } + val deferredMounted = async { rootInstaller.isDeviceRooted() && rootInstaller.isAppMounted(app.currentPackageName) } val deferredOriginalApk = async { originalApkRepository.get(app.originalPackageName) != null } val deferredAppState = async { refreshAppState(app) } val deferredPatches = async { resolveAppliedSelection(app) } @@ -128,8 +132,20 @@ class InstalledAppInfoViewModel( fun uninstall() { val app = installedApp ?: return when (app.installType) { - InstallType.DEFAULT, InstallType.CUSTOM -> pm.uninstallPackage(app.currentPackageName) - InstallType.SHIZUKU -> pm.uninstallPackage(app.currentPackageName) + InstallType.DEFAULT, InstallType.CUSTOM, InstallType.SHIZUKU, InstallType.SAVED -> { + viewModelScope.launch { + try { + ackpineInstaller.uninstall(app.currentPackageName) + // Ackpine suspends until confirmed - refresh state after success + refreshCurrentAppState() + onAppStateChanged?.invoke(app.currentPackageName) + } catch (_: UninstallCancelledException) { + // User dismissed dialog - do nothing + } catch (e: Exception) { + context.toast(context.getString(R.string.install_app_fail, e.simpleMessage())) + } + } + } InstallType.MOUNT -> viewModelScope.launch { rootInstaller.uninstall(app.currentPackageName) @@ -137,14 +153,12 @@ class InstalledAppInfoViewModel( deleteRecordAndApk(app) onBackClick() } - - InstallType.SAVED -> pm.uninstallPackage(app.currentPackageName) } } /** - * Remove app completely: database record, patched APK and original APK - * Patch selection and options are preserved for future patching + * Remove app completely: database record, patched APK and original APK. + * Patch selection and options are preserved for future patching. */ fun removeAppCompletely() = viewModelScope.launch { val app = installedApp ?: return@launch @@ -166,8 +180,8 @@ class InstalledAppInfoViewModel( } /** - * Delete database record and patched APK file - * Note: Patch selection and options are NOT deleted - they remain for future patching + * Delete database record and patched APK file. + * Note: Patch selection and options are NOT deleted - they remain for future patching. */ private suspend fun deleteRecordAndApk(app: InstalledApp) { // Delete database record @@ -180,8 +194,8 @@ class InstalledAppInfoViewModel( hasSavedCopy = false } - fun updateInstallType(packageName: String, newInstallType: InstallType) = viewModelScope.launch { - val app = installedApp ?: return@launch + suspend fun updateInstallType(packageName: String, newInstallType: InstallType) { + val app = installedApp ?: return // Update in database withContext(Dispatchers.IO) { installedAppRepository.addOrUpdate( @@ -231,7 +245,7 @@ class InstalledAppInfoViewModel( } // Update mounted state - isMounted = rootInstaller.isAppMounted(app.currentPackageName) + isMounted = rootInstaller.isDeviceRooted() && rootInstaller.isAppMounted(app.currentPackageName) } /** Manually refresh app state (e.g., after app installation/uninstallation) */ diff --git a/app/src/main/java/app/morphe/manager/ui/viewmodel/MainViewModel.kt b/app/src/main/java/app/morphe/manager/ui/viewmodel/MainViewModel.kt index dedcb6d4a..d8019c4a6 100644 --- a/app/src/main/java/app/morphe/manager/ui/viewmodel/MainViewModel.kt +++ b/app/src/main/java/app/morphe/manager/ui/viewmodel/MainViewModel.kt @@ -1,5 +1,6 @@ package app.morphe.manager.ui.viewmodel +import android.net.Uri import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue @@ -24,5 +25,12 @@ class MainViewModel( */ var pendingDeepLinkSource: DeepLinkSource? by mutableStateOf(null) + /** + * Set by [app.morphe.manager.MainActivity.handleDeepLinkIntent] when the app is opened + * by tapping a .mpp file in a file manager. HomeScreen observes this via LaunchedEffect, + * shows a confirmation dialog, then resets the flag to null. + */ + var pendingMppUri: Uri? by mutableStateOf(null) + data class DeepLinkSource(val url: String, val name: String?) } diff --git a/app/src/main/java/app/morphe/manager/ui/viewmodel/PatcherViewModel.kt b/app/src/main/java/app/morphe/manager/ui/viewmodel/PatcherViewModel.kt index 907d7c0e4..b568a6146 100644 --- a/app/src/main/java/app/morphe/manager/ui/viewmodel/PatcherViewModel.kt +++ b/app/src/main/java/app/morphe/manager/ui/viewmodel/PatcherViewModel.kt @@ -47,7 +47,12 @@ import kotlinx.coroutines.sync.withLock import org.koin.core.component.KoinComponent import org.koin.core.component.get import org.koin.core.component.inject +import android.content.ContentValues +import android.os.Build +import android.os.Environment +import android.provider.MediaStore import java.io.File +import java.io.FileOutputStream import java.io.IOException import java.nio.file.Files import java.util.UUID @@ -425,31 +430,22 @@ class PatcherViewModel( /** * Save original APK file for future repatching. * Called after successful patching, independent of installation method. - * For split APK archives (apkm, apks, xapk), saves the original archive. + * For split APK archives: inputFile points to the merged mono-APK already saved to + * originalApksDir by the worker via onMergedApkReady - this call will detect the + * existing record and skip re-saving. * For regular APK files, saves the APK itself. * * Thread-safe: uses mutex to prevent concurrent saves from observeWorker and persistPatchedApp. */ private suspend fun saveOriginalApkIfNeeded() = saveOriginalApkMutex.withLock { try { - // Determine which file to save: - // - For SelectedApp.Local with split archives: save the original user file - // - For other cases: save the inputFile (which might be a downloaded/extracted APK) + // Determine which file to save. + // For SelectedApp.Local with a split archive: inputFile is updated to the merged + // mono-APK via setInputFile(merged=true) after prepareIfNeeded() completes, so + // we always use inputFile here - it already points to the correct file. val fileToSave = when (val selected = input.selectedApp) { - is SelectedApp.Local -> { - // Check if original file is a split archive - if (SplitApkPreparer.isSplitArchive(selected.file)) { - // Save the original split archive, not the merged APK - selected.file - } else { - // For regular APK, use inputFile (might be same as selected.file) - inputFile ?: selected.file - } - } - else -> { - // For non-local apps (Download, Search, Installed), use inputFile - inputFile - } + is SelectedApp.Local -> inputFile ?: selected.file + else -> inputFile } if (fileToSave == null || !fileToSave.exists()) { @@ -598,28 +594,74 @@ class PatcherViewModel( ?: throw IOException("Could not open output stream for export") } }.isSuccess + finishExport(exportSucceeded) + } finally { + _isSaving.value = false + } + } + } - if (!exportSucceeded) { - app.toast(app.getString(R.string.saved_app_export_failed)) - return@let + /** + * Exports the patched APK to the public Downloads folder. + * Used as a fallback on devices without DocumentsUI. + */ + fun exportToDownloads() = viewModelScope.launch { + if (_isSaving.value) return@launch + _isSaving.value = true + try { + ensureExportMetadata() + val fileName = exportFileName + val exportSucceeded = runCatching { + withContext(Dispatchers.IO) { + val stream = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + val values = ContentValues().apply { + put(MediaStore.Downloads.DISPLAY_NAME, fileName) + put(MediaStore.Downloads.MIME_TYPE, APK_MIMETYPE) + put(MediaStore.Downloads.RELATIVE_PATH, Environment.DIRECTORY_DOWNLOADS) + } + val uri = app.contentResolver.insert( + MediaStore.Downloads.EXTERNAL_CONTENT_URI, values + ) ?: throw IOException("Could not create Downloads entry") + app.contentResolver.openOutputStream(uri) + ?: throw IOException("Could not open Downloads output stream") + } else { + @Suppress("DEPRECATION") + val dir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS) + dir.mkdirs() + FileOutputStream(File(dir, fileName)) + } + stream.use { Files.copy(outputFile.toPath(), it) } } + }.isSuccess + finishExport(exportSucceeded) + } finally { + _isSaving.value = false + } + } - val saved = persistPatchedApp(null, InstallType.SAVED) + /** + * Shared post-export logic: persists the patched app record, shows a toast, + * and triggers the notification prompt on success. + */ + private suspend fun finishExport(exportSucceeded: Boolean) { + if (!exportSucceeded) { + app.toast(app.getString(R.string.saved_app_export_failed)) + return + } - if (!saved) { - app.toast(app.getString(R.string.patched_app_save_failed_toast)) - } else { - app.toast(app.getString(R.string.save_apk_success)) - delay(2000) // Delay before resetting save state - } + val saved = persistPatchedApp(null, InstallType.SAVED) - if (saved) triggerNotificationPromptIfNeeded() - } finally { - _isSaving.value = false - } + if (!saved) { + app.toast(app.getString(R.string.patched_app_save_failed_toast)) + } else { + app.toast(app.getString(R.string.save_apk_success)) + delay(2000) } + + if (saved) triggerNotificationPromptIfNeeded() } + /** * Checks prefs and triggers the notification prompt if conditions are met. * Called after a successful install or export so UI doesn't read prefs directly. @@ -806,7 +848,7 @@ class PatcherViewModel( saveOriginalApkIfNeeded() } finally { withContext(Dispatchers.Main) { - // Delete temporary file after saving + // Delete temporary input file after saving if (input.selectedApp is SelectedApp.Local && input.selectedApp.temporary) { inputFile?.takeIf { it.exists() }?.delete() inputFile = null diff --git a/app/src/main/java/app/morphe/manager/ui/viewmodel/SettingsViewModel.kt b/app/src/main/java/app/morphe/manager/ui/viewmodel/SettingsViewModel.kt index e182dca9c..9b4e04e50 100644 --- a/app/src/main/java/app/morphe/manager/ui/viewmodel/SettingsViewModel.kt +++ b/app/src/main/java/app/morphe/manager/ui/viewmodel/SettingsViewModel.kt @@ -20,6 +20,7 @@ import app.morphe.manager.util.AppDataSource import app.morphe.manager.util.syncFcmTopics import app.morphe.manager.worker.UpdateCheckInterval import app.morphe.manager.worker.UpdateCheckWorker +import app.morphe.patcher.dex.BytecodeMode import com.google.android.gms.common.ConnectionResult import com.google.android.gms.common.GoogleApiAvailability import kotlinx.coroutines.Dispatchers @@ -189,10 +190,15 @@ class SettingsViewModel( prefs.patcherProcessMemoryLimit.update(limit) } + /** Enables/disables native library stripping for plain APKs, and simultaneously split APK filtering for split bundles. */ fun setStripUnusedNativeLibs(enabled: Boolean) = viewModelScope.launch { prefs.stripUnusedNativeLibs.update(enabled) } + fun setBytecodeMode(mode: BytecodeMode) = viewModelScope.launch { + prefs.bytecodeModePreference.update(mode) + } + fun setGitHubPat(pat: String, includeInExport: Boolean) = viewModelScope.launch { prefs.gitHubPat.update(pat) prefs.includeGitHubPatInExports.update(includeInExport) @@ -223,7 +229,7 @@ class SettingsViewModel( token: InstallerManager.Token, ): List { val raw = installerManager.listEntries(installTarget, includeNone = false) - return ensureValidEntries(raw, token, installerManager, installTarget) + return installerManager.ensureValidEntries(raw, token, installTarget) } fun parseInstallerToken(preference: String): InstallerManager.Token = @@ -310,46 +316,6 @@ class SettingsViewModel( } companion object { - /** - * Builds a deduplicated list of [InstallerManager.Entry] objects, guaranteeing - * that the currently-preferred [token] is always included even if the live - * [installerManager] does not enumerate it. - */ - fun ensureValidEntries( - entries: List, - token: InstallerManager.Token, - installerManager: InstallerManager, - installTarget: InstallerManager.InstallTarget, - ): List { - // Remove duplicates based on component name for Component tokens - val normalized = buildList { - val seen = mutableSetOf() - entries.forEach { entry -> - val key = when (val entryToken = entry.token) { - is InstallerManager.Token.Component -> entryToken.componentName - else -> entryToken - } - if (seen.add(key)) add(entry) - } - } - - val tokenExists = token == InstallerManager.Token.Internal || - token == InstallerManager.Token.AutoSaved || - normalized.any { tokensEqual(it.token, token) } - - return if (tokenExists) normalized - else installerManager.describeEntry(token, installTarget) - ?.let { normalized + it } ?: normalized - } - - fun tokensEqual(a: InstallerManager.Token?, b: InstallerManager.Token?): Boolean = when { - a === b -> true - a == null || b == null -> false - a is InstallerManager.Token.Component && b is InstallerManager.Token.Component -> - a.componentName == b.componentName - else -> false - } - fun parseJsonValue(jsonString: String): Any? = try { val json = Json { ignoreUnknownKeys = true } when (val element = json.parseToJsonElement(jsonString)) { diff --git a/app/src/main/java/app/morphe/manager/ui/viewmodel/ThemeSettingsViewModel.kt b/app/src/main/java/app/morphe/manager/ui/viewmodel/ThemeSettingsViewModel.kt index ea8ac328b..0e5d82707 100644 --- a/app/src/main/java/app/morphe/manager/ui/viewmodel/ThemeSettingsViewModel.kt +++ b/app/src/main/java/app/morphe/manager/ui/viewmodel/ThemeSettingsViewModel.kt @@ -3,13 +3,18 @@ package app.morphe.manager.ui.viewmodel import androidx.compose.ui.graphics.Color import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import app.morphe.manager.R import app.morphe.manager.domain.manager.PreferencesManager import app.morphe.manager.ui.screen.shared.BackgroundType import app.morphe.manager.ui.theme.Theme import app.morphe.manager.util.applyAppLanguage import app.morphe.manager.util.resetListItemColorsCached import app.morphe.manager.util.toHexString +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.launch +import java.util.concurrent.TimeUnit enum class ThemePreset { DEFAULT, @@ -18,6 +23,18 @@ enum class ThemePreset { DYNAMIC } +/** + * How often the random background rotates. + * [ON_LAUNCH] picks a new background every time the app is opened. + * [DAILY] keeps the same background for the calendar day. + * [EVERY_3_DAYS] rotates every 3 days based on epoch day. + */ +enum class RandomInterval(val labelResId: Int) { + ON_LAUNCH(R.string.settings_appearance_background_random_interval_launch), + DAILY(R.string.settings_appearance_background_random_interval_daily), + EVERY_3_DAYS(R.string.settings_appearance_background_random_interval_3days) +} + private data class ThemePresetConfig( val theme: Theme, val dynamicColor: Boolean = false, @@ -35,6 +52,41 @@ class ThemeSettingsViewModel( ThemePreset.DYNAMIC to ThemePresetConfig(theme = Theme.SYSTEM, dynamicColor = true) ) + /** + * The currently resolved background for this session when RANDOM mode is active. + * Populated by [resolveRandomBackground]; null until first resolution. + */ + private val _resolvedRandomBackground = MutableStateFlow(null) + val resolvedRandomBackground: StateFlow = _resolvedRandomBackground.asStateFlow() + + /** + * Resolves the effective background type when [BackgroundType.RANDOM] is selected. + * Called once on app start and again whenever the interval preference changes. + * + * - [RandomInterval.ON_LAUNCH] — picks a new random type each time. + * - [RandomInterval.DAILY] — uses today's epoch day as a stable index. + * - [RandomInterval.EVERY_3_DAYS] — uses epoch day ÷ 3 as a stable index. + */ + fun resolveRandomBackground(interval: RandomInterval) { + val pool = BackgroundType.RANDOMIZABLE + _resolvedRandomBackground.value = when (interval) { + RandomInterval.ON_LAUNCH -> pool.random() + RandomInterval.DAILY -> { + val dayIndex = TimeUnit.MILLISECONDS.toDays(System.currentTimeMillis()) + pool[(dayIndex % pool.size).toInt()] + } + RandomInterval.EVERY_3_DAYS -> { + val periodIndex = TimeUnit.MILLISECONDS.toDays(System.currentTimeMillis()) / 3 + pool[(periodIndex % pool.size).toInt()] + } + } + } + + fun setRandomInterval(interval: RandomInterval) = viewModelScope.launch { + prefs.randomBackgroundInterval.update(interval) + resolveRandomBackground(interval) + } + fun setCustomAccentColor(color: Color?) = viewModelScope.launch { val value = color?.toHexString().orEmpty() prefs.customAccentColor.update(value) @@ -51,6 +103,10 @@ class ThemeSettingsViewModel( applyAppLanguage(languageCode) } + fun toggleShowGreetingPhrases(current: Boolean) = viewModelScope.launch { + prefs.showGreetingPhrases.update(!current) + } + fun togglePureBlackTheme(current: Boolean) = viewModelScope.launch { prefs.pureBlackTheme.update(!current) } diff --git a/app/src/main/java/app/morphe/manager/ui/viewmodel/UpdateViewModel.kt b/app/src/main/java/app/morphe/manager/ui/viewmodel/UpdateViewModel.kt index 6a13d0497..33b99617a 100644 --- a/app/src/main/java/app/morphe/manager/ui/viewmodel/UpdateViewModel.kt +++ b/app/src/main/java/app/morphe/manager/ui/viewmodel/UpdateViewModel.kt @@ -2,7 +2,6 @@ package app.morphe.manager.ui.viewmodel import android.app.Application import android.content.* -import android.content.pm.PackageInstaller import androidx.annotation.StringRes import androidx.compose.runtime.* import androidx.core.content.ContextCompat @@ -12,13 +11,14 @@ import app.morphe.manager.BuildConfig import app.morphe.manager.R import app.morphe.manager.data.platform.Filesystem import app.morphe.manager.data.platform.NetworkInfo +import app.morphe.manager.domain.installer.AckpineInstaller +import app.morphe.manager.domain.installer.InstallCancelledException import app.morphe.manager.domain.installer.InstallerManager -import app.morphe.manager.domain.installer.ShizukuInstaller +import ru.solrudev.ackpine.installer.InstallFailure import app.morphe.manager.domain.manager.PreferencesManager import app.morphe.manager.network.api.MorpheAPI import app.morphe.manager.network.dto.MorpheAsset import app.morphe.manager.network.service.HttpService -import app.morphe.manager.service.InstallService import app.morphe.manager.util.* import io.ktor.client.plugins.onDownload import io.ktor.client.request.url @@ -33,8 +33,7 @@ class UpdateViewModel( private val app: Application by inject() private val morpheAPI: MorpheAPI by inject() private val http: HttpService by inject() - private val pm: PM by inject() - private val shizukuInstaller: ShizukuInstaller by inject() + private val ackpineInstaller: AckpineInstaller by inject() private val networkInfo: NetworkInfo by inject() private val fs: Filesystem by inject() private val prefs: PreferencesManager by inject() @@ -178,7 +177,21 @@ class UpdateViewModel( when (plan) { is InstallerManager.InstallPlan.Internal -> { state = State.INSTALLING - pm.installApp(listOf(location)) + try { + handleInstallFailure( + failure = ackpineInstaller.installInternal(location), + successToast = R.string.install_app_success + ) + } catch (_: InstallCancelledException) { + // User dismissed dialog — go back to CAN_INSTALL so they can retry + state = State.CAN_INSTALL + } catch (e: Exception) { + val message = e.simpleMessage().orEmpty() + installError = message + canResumeDownload = false + app.toast(app.getString(R.string.install_app_fail, message)) + state = State.FAILED + } } is InstallerManager.InstallPlan.Mount -> { @@ -192,18 +205,14 @@ class UpdateViewModel( is InstallerManager.InstallPlan.Shizuku -> { state = State.INSTALLING try { - shizukuInstaller.install(location, app.packageName) - installError = "" - state = State.SUCCESS - app.toast(app.getString(R.string.update_completed)) - } catch (error: ShizukuInstaller.InstallerOperationException) { - val message = error.message ?: app.getString(R.string.installer_hint_generic) - installError = message - canResumeDownload = false - app.toast(app.getString(R.string.install_app_fail, message)) - state = State.FAILED - } catch (error: Exception) { - val message = error.simpleMessage().orEmpty() + handleInstallFailure( + failure = ackpineInstaller.installShizuku(location), + successToast = R.string.update_completed + ) + } catch (_: InstallCancelledException) { + state = State.CAN_INSTALL + } catch (e: Exception) { + val message = e.simpleMessage().orEmpty() installError = message canResumeDownload = false app.toast(app.getString(R.string.install_app_fail, message)) @@ -215,6 +224,29 @@ class UpdateViewModel( } } + private fun handleInstallFailure(failure: InstallFailure?, @StringRes successToast: Int) { + when (failure) { + null -> { + installError = "" + state = State.SUCCESS + app.toast(app.getString(successToast)) + } + is InstallFailure.Conflict -> { + installError = app.getString(R.string.installer_hint_conflict) + canResumeDownload = false + app.toast(installError) + state = State.FAILED + } + else -> { + val message = failure.message ?: failure.javaClass.simpleName + installError = message + canResumeDownload = false + app.toast(app.getString(R.string.install_app_fail, message)) + state = State.FAILED + } + } + } + private fun launchExternalInstaller(plan: InstallerManager.InstallPlan.External) { pendingExternalInstall?.let(installerManager::cleanup) externalInstallTimeoutJob?.cancel() @@ -264,6 +296,8 @@ class UpdateViewModel( state = State.SUCCESS } + // Broadcast receiver — only needed for external (third-party installer) monitoring. + // Internal/Shizuku results come directly from Ackpine's session.await(). private val installBroadcastReceiver = object : BroadcastReceiver() { override fun onReceive(context: Context?, intent: Intent?) { when (intent?.action) { @@ -272,53 +306,12 @@ class UpdateViewModel( val pkg = intent.data?.schemeSpecificPart ?: return handleExternalInstallSuccess(pkg) } - - InstallService.APP_INSTALL_ACTION -> { - val pmStatus = intent.getIntExtra(InstallService.EXTRA_INSTALL_STATUS, -999) - val extra = - intent.getStringExtra(InstallService.EXTRA_INSTALL_STATUS_MESSAGE) ?: "" - - when (pmStatus) { - PackageInstaller.STATUS_SUCCESS -> { - pendingExternalInstall?.let(installerManager::cleanup) - pendingExternalInstall = null - externalInstallTimeoutJob?.cancel() - externalInstallTimeoutJob = null - installError = "" - app.toast(app.getString(R.string.install_app_success)) - state = State.SUCCESS - } - PackageInstaller.STATUS_FAILURE_ABORTED -> { - pendingExternalInstall?.let(installerManager::cleanup) - pendingExternalInstall = null - externalInstallTimeoutJob?.cancel() - externalInstallTimeoutJob = null - state = State.CAN_INSTALL - } - else -> { - val hint = installerManager.formatFailureHint(pmStatus, extra) - val message = app.getString( - R.string.install_app_fail, - hint ?: extra.ifBlank { pmStatus.toString() } - ) - pendingExternalInstall?.let(installerManager::cleanup) - pendingExternalInstall = null - externalInstallTimeoutJob?.cancel() - externalInstallTimeoutJob = null - app.toast(message) - installError = hint ?: extra - canResumeDownload = false - state = State.FAILED - } - } - } } } } init { ContextCompat.registerReceiver(app, installBroadcastReceiver, IntentFilter().apply { - addAction(InstallService.APP_INSTALL_ACTION) addAction(Intent.ACTION_PACKAGE_ADDED) addAction(Intent.ACTION_PACKAGE_REPLACED) addDataScheme("package") @@ -339,20 +332,18 @@ class UpdateViewModel( } /** - * Reset state if installation was canceled by user (dismissed system dialog) - * Called when dialog reopens to check if we need to reset + * Reset state if an external installation timed out or was abandoned. + * Internal/Shizuku cancellations are handled automatically via Ackpine's await(). */ fun resetIfInstallCancelled() { // If we're in INSTALLING state but the pending installation was canceled, // reset to CAN_INSTALL so user can try again if (state == State.INSTALLING && pendingExternalInstall == null) { - // Check if the APK file still exists - if (location.exists() && location.length() > 0) { - state = State.CAN_INSTALL + state = if (location.exists() && location.length() > 0) { + State.CAN_INSTALL } else { - // File was deleted somehow, need to download again - state = State.CAN_DOWNLOAD canResumeDownload = false + State.CAN_DOWNLOAD } } } diff --git a/app/src/main/java/app/morphe/manager/util/AppDataResolver.kt b/app/src/main/java/app/morphe/manager/util/AppDataResolver.kt index 441ce7ace..a8f6f8658 100644 --- a/app/src/main/java/app/morphe/manager/util/AppDataResolver.kt +++ b/app/src/main/java/app/morphe/manager/util/AppDataResolver.kt @@ -12,19 +12,22 @@ import android.graphics.drawable.Drawable import app.morphe.manager.data.platform.Filesystem import app.morphe.manager.domain.repository.InstalledAppRepository import app.morphe.manager.domain.repository.OriginalApkRepository +import app.morphe.manager.domain.repository.PatchBundleRepository import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.first import kotlinx.coroutines.withContext import java.io.File +import java.util.concurrent.ConcurrentHashMap /** * Data source priority for app information. */ enum class AppDataSource { - INSTALLED, // Installed app via PackageManager - ORIGINAL_APK, // Saved original APK file - PATCHED_APK, // Saved patched APK file - CONSTANTS // Fallback to hardcoded constants + INSTALLED, // Installed app via PackageManager + ORIGINAL_APK, // Saved original APK file + PATCHED_APK, // Saved patched APK file + BUNDLE_METADATA, // Display name declared in the patch bundle (BundleAppMetadata) + CONSTANTS // Fallback to hardcoded constants } /** @@ -51,63 +54,93 @@ class AppDataResolver( private val pm: PM, private val originalApkRepository: OriginalApkRepository, private val installedAppRepository: InstalledAppRepository, - private val filesystem: Filesystem + private val filesystem: Filesystem, + private val patchBundleRepository: PatchBundleRepository ) { private val packageManager: PackageManager = context.packageManager + // In-memory cache - keyed by packageName + preferredSource. + // Avoids redundant IO when multiple composables resolve the same package simultaneously. + // Entries are never evicted: the resolver is a singleton and package data rarely changes + // during a single session. + private val cache = ConcurrentHashMap, ResolvedAppData>() + + /** + * Invalidate cached data for a specific package. + * Call this after installation, uninstallation, or any state change + * that affects what source the package data comes from. + */ + fun invalidate(packageName: String) { + cache.keys.removeAll { it.first == packageName } + } + /** * Resolve app data from any available source. + * + * Display name and icon are resolved **independently**: + * - Icon/packageInfo: best available APK source ordered by [preferredSource] + * - Name: [AppDataSource.BUNDLE_METADATA] always wins when available, because patched APK + * labels may contain internal class names instead of the real product name. + * Falls back to APK label → constants. + * * @param packageName Package name to resolve - * @param preferredSource Preferred data source (will still fallback if unavailable) - * @return ResolvedAppData with information from the best available source + * @param preferredSource Preferred data source for icon/packageInfo (will still fallback) + * @return [ResolvedAppData] with the best available name and icon, potentially from + * different sources */ suspend fun resolveAppData( packageName: String, preferredSource: AppDataSource = AppDataSource.INSTALLED ): ResolvedAppData = withContext(Dispatchers.IO) { - // Define source check order based on preference - val sourceOrder = when (preferredSource) { - AppDataSource.INSTALLED -> listOf( - AppDataSource.INSTALLED, - AppDataSource.ORIGINAL_APK, - AppDataSource.PATCHED_APK, - AppDataSource.CONSTANTS - ) + cache[packageName to preferredSource]?.let { return@withContext it } + + // APK sources ordered by preference - provide icon, packageInfo and raw label + val apkSources = when (preferredSource) { AppDataSource.ORIGINAL_APK -> listOf( AppDataSource.ORIGINAL_APK, AppDataSource.INSTALLED, AppDataSource.PATCHED_APK, - AppDataSource.CONSTANTS ) AppDataSource.PATCHED_APK -> listOf( AppDataSource.PATCHED_APK, AppDataSource.ORIGINAL_APK, AppDataSource.INSTALLED, - AppDataSource.CONSTANTS ) - AppDataSource.CONSTANTS -> listOf(AppDataSource.CONSTANTS) + else -> listOf( + AppDataSource.INSTALLED, + AppDataSource.ORIGINAL_APK, + AppDataSource.PATCHED_APK, + ) } - // Try each source in order until we get data - for (source in sourceOrder) { - val result = when (source) { + // Phase 1: find the best available icon + packageInfo from APK sources + val apkResult = apkSources.firstNotNullOfOrNull { source -> + when (source) { AppDataSource.INSTALLED -> tryGetFromInstalled(packageName) AppDataSource.ORIGINAL_APK -> tryGetFromOriginalApk(packageName) AppDataSource.PATCHED_APK -> tryGetFromPatchedApk(packageName) - AppDataSource.CONSTANTS -> getFromConstants(packageName) + else -> null } - if (result != null) return@withContext result } - // Ultimate fallback - return package name as display name + // Phase 2: display name - bundle metadata always wins when available + val bundleName = tryGetFromBundleMetadata(packageName)?.displayName + val displayName = bundleName + ?: apkResult?.displayName + ?: getFromConstants(packageName).displayName + ResolvedAppData( packageName = packageName, - displayName = packageName, - version = null, - icon = null, - packageInfo = null, - source = AppDataSource.CONSTANTS - ) + displayName = displayName, + version = apkResult?.version, + icon = apkResult?.icon, + packageInfo = apkResult?.packageInfo, + source = when { + bundleName != null -> AppDataSource.BUNDLE_METADATA + apkResult != null -> apkResult.source + else -> AppDataSource.CONSTANTS + } + ).also { cache[packageName to preferredSource] = it } } /** @@ -223,6 +256,24 @@ class AppDataResolver( } } + /** + * Try to get app display name from patch bundle metadata. + * Uses [PatchBundleRepository.appMetadata] snapshot, no allocations. + * Returns null if bundles are not yet loaded or package isn't in any bundle. + */ + private fun tryGetFromBundleMetadata(packageName: String): ResolvedAppData? { + val displayName = patchBundleRepository.appMetadata.value[packageName]?.displayName + ?: return null + return ResolvedAppData( + packageName = packageName, + displayName = displayName, + version = null, + icon = null, + packageInfo = null, + source = AppDataSource.BUNDLE_METADATA + ) + } + /** * Get app data from hardcoded constants. */ diff --git a/app/src/main/java/app/morphe/manager/util/AvatarUtils.kt b/app/src/main/java/app/morphe/manager/util/AvatarUtils.kt index c3e74567b..de1c5bc15 100644 --- a/app/src/main/java/app/morphe/manager/util/AvatarUtils.kt +++ b/app/src/main/java/app/morphe/manager/util/AvatarUtils.kt @@ -5,7 +5,6 @@ package app.morphe.manager.util -import android.annotation.SuppressLint import android.graphics.Bitmap import android.graphics.BitmapFactory import androidx.compose.foundation.Image @@ -53,10 +52,9 @@ suspend fun loadRemoteAvatar(url: String): Bitmap? = withContext(Dispatchers.IO) */ @Composable fun RemoteAvatar( + modifier: Modifier = Modifier, url: String, - fallbackUrl: String? = null, - @SuppressLint("ModifierParameter") - modifier: Modifier = Modifier + fallbackUrl: String? = null ) { var bitmap by remember(url) { mutableStateOf(AvatarCache[url] ?: fallbackUrl?.let { AvatarCache[it] }) diff --git a/app/src/main/java/app/morphe/manager/util/ColorUtils.kt b/app/src/main/java/app/morphe/manager/util/ColorUtils.kt index a956aa26a..f365feffa 100644 --- a/app/src/main/java/app/morphe/manager/util/ColorUtils.kt +++ b/app/src/main/java/app/morphe/manager/util/ColorUtils.kt @@ -49,6 +49,23 @@ fun Color.toHexString(includeAlpha: Boolean = false): String { } } +/** + * Adjusts the color so it has sufficient contrast against [background]. + * If the accent is too close to the background (same lightness zone), + * it is lightened or darkened until it passes the [minLuminanceDiff] threshold. + */ +fun Color.ensureContrast( + background: Color, + minLuminanceDiff: Float = 0.05f +): Color { + val bgLum = background.luminance() + val fgLum = this.luminance() + val diff = kotlin.math.abs(bgLum - fgLum) + if (diff >= minLuminanceDiff) return this + return if (bgLum > 0.5f) darken((minLuminanceDiff - diff + 0.05f).coerceIn(0f, 0.8f)) + else lighten((minLuminanceDiff - diff + 0.05f).coerceIn(0f, 0.8f)) +} + fun String?.toColorOrNull(): Color? { val value = this?.trim().orEmpty() if (value.isEmpty()) return null diff --git a/app/src/main/java/app/morphe/manager/util/Constants.kt b/app/src/main/java/app/morphe/manager/util/Constants.kt index 6833fbbbd..9a32bc384 100644 --- a/app/src/main/java/app/morphe/manager/util/Constants.kt +++ b/app/src/main/java/app/morphe/manager/util/Constants.kt @@ -41,7 +41,7 @@ object KnownApps { const val YOUTUBE = "com.google.android.youtube" const val YOUTUBE_MUSIC = "com.google.android.apps.youtube.music" const val REDDIT = "com.reddit.frontpage" - const val X_TWITTER = "com.twitter.android" + // const val X_TWITTER = "com.twitter.android" // Shared Morphe brand gradient tail val GRADIENT_MID = Color(0xFF1E5AA8) @@ -124,6 +124,8 @@ object KnownApps { const val APK_MIMETYPE = "application/vnd.android.package-archive" const val JSON_MIMETYPE = "application/json" const val BIN_MIMETYPE = "application/octet-stream" +const val TEXT_MIMETYPE = "text/plain" +const val MPP_MIMETYPE = "application/vnd.ms-project" val APK_FILE_MIME_TYPES = arrayOf( BIN_MIMETYPE, @@ -146,16 +148,10 @@ val APK_FILE_MIME_TYPES = arrayOf( // "application/vnd.android.apks", // "application/apks", ) -val APK_FILE_EXTENSIONS = setOf( - "apk", - "apkm", - "apks", - "xapk", - "zip" -) val MPP_FILE_MIME_TYPES = arrayOf( BIN_MIMETYPE, + MPP_MIMETYPE, // "application/x-zip-compressed" "*/*" ) diff --git a/app/src/main/java/app/morphe/manager/util/FcmTopicManager.kt b/app/src/main/java/app/morphe/manager/util/FcmTopicManager.kt index b626830ea..1f07968f3 100644 --- a/app/src/main/java/app/morphe/manager/util/FcmTopicManager.kt +++ b/app/src/main/java/app/morphe/manager/util/FcmTopicManager.kt @@ -77,7 +77,7 @@ fun syncFcmTopics( * Subscribes to or unsubscribes from a single FCM topic and logs the result. */ private fun FirebaseMessaging.syncTopic(topic: String, subscribe: Boolean) { - val tag = "FcmTopicSync" + val tag = "Morphe FcmTopicSync" if (subscribe) { subscribeToTopic(topic).addOnCompleteListener { task -> Log.d(tag, if (task.isSuccessful) "Subscribed to $topic" else "Failed to subscribe to $topic") diff --git a/app/src/main/java/app/morphe/manager/util/FileFilters.kt b/app/src/main/java/app/morphe/manager/util/FileFilters.kt deleted file mode 100644 index 01b8d9e5f..000000000 --- a/app/src/main/java/app/morphe/manager/util/FileFilters.kt +++ /dev/null @@ -1,15 +0,0 @@ -package app.morphe.manager.util - -import java.nio.file.Path -import java.util.Locale -import kotlin.io.path.name - -fun isAllowedApkFile(path: Path): Boolean { - val extension = path.name.substringAfterLast('.', "").lowercase(Locale.ROOT) - return extension in APK_FILE_EXTENSIONS -} - -fun isAllowedMppFile(path: Path): Boolean { - val extension = path.name.substringAfterLast('.', "").lowercase(Locale.ROOT) - return extension == "mpp" -} diff --git a/app/src/main/java/app/morphe/manager/util/FilePickerUtils.kt b/app/src/main/java/app/morphe/manager/util/FilePickerUtils.kt index 796191128..ae7c100b1 100644 --- a/app/src/main/java/app/morphe/manager/util/FilePickerUtils.kt +++ b/app/src/main/java/app/morphe/manager/util/FilePickerUtils.kt @@ -1,16 +1,37 @@ +/* + * Copyright 2026 Morphe. + * https://github.com/MorpheApp/morphe-manager + */ + package app.morphe.manager.util -import android.content.Context +import android.content.ContentResolver import android.net.Uri +import android.provider.OpenableColumns +import android.app.UiModeManager +import android.content.Context +import android.content.Intent +import android.content.res.Configuration import androidx.activity.compose.rememberLauncherForActivityResult +import androidx.activity.result.contract.ActivityResultContract import androidx.activity.result.contract.ActivityResultContracts import androidx.compose.runtime.Composable import androidx.compose.runtime.remember +import androidx.compose.ui.platform.LocalContext import app.morphe.manager.data.platform.Filesystem -import app.morphe.manager.data.room.apps.installed.InstalledApp import org.koin.compose.koinInject import java.io.File +/** Parsed metadata from a .mpp patch bundle's META-INF/MANIFEST.MF entry. */ +data class MppManifest( + val name: String?, + val version: String?, + val author: String?, + val description: String?, + val source: String?, + val timestamp: Long?, +) + /** * Convert content:// URI to file path * This only works for some URIs and should be avoided when possible. @@ -37,6 +58,67 @@ fun Uri.toFilePath(): String { } } +/** + * Resolves the display name of a URI using [ContentResolver]. + * For content:// URIs queries the provider via [OpenableColumns.DISPLAY_NAME]. + * Falls back to the last path segment for file:// URIs or if the provider does not expose a name. + */ +fun Uri.displayName(contentResolver: ContentResolver): String? = + runCatching { + contentResolver.query(this, arrayOf(OpenableColumns.DISPLAY_NAME), null, null, null) + ?.use { cursor -> + val col = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME) + if (col != -1 && cursor.moveToFirst()) cursor.getString(col) else null + } + }.getOrNull() ?: lastPathSegment + +/** + * Returns true if the URI refers to a .mpp patch bundle file. + * Delegates entirely to [displayName] which already handles both file:// and content:// + * with a lastPathSegment fallback. + */ +fun Uri.hasMppExtension(contentResolver: ContentResolver): Boolean = + displayName(contentResolver)?.endsWith(".mpp", ignoreCase = true) == true + + +/** + * Reads and parses the META-INF/MANIFEST.MF entry from a .mpp patch bundle URI. + * Returns null if the entry is missing, the URI is unreadable, or any IO error occurs. + * Values equal to "na" (case-insensitive) are treated as absent. + */ +fun Uri.readMppManifest(contentResolver: ContentResolver): MppManifest? = + runCatching { + contentResolver.openInputStream(this)?.use { stream -> + java.util.zip.ZipInputStream(stream).use { zip -> + var manifest: MppManifest? = null + var entry = zip.nextEntry + while (entry != null && manifest == null) { + if (entry.name == "META-INF/MANIFEST.MF") { + val attrs = zip.bufferedReader().readText() + .lineSequence() + .filter { ":" in it } + .associate { line -> + val idx = line.indexOf(':') + line.substring(0, idx).trim() to line.substring(idx + 1).trim() + } + fun attr(key: String) = + attrs[key]?.takeUnless { it.isBlank() || it.equals("na", ignoreCase = true) } + manifest = MppManifest( + name = attr("Name"), + version = attr("Version"), + author = attr("Author"), + description = attr("Description"), + source = attr("Source") ?: attr("Website"), + timestamp = attr("Timestamp")?.toLongOrNull(), + ) + } + entry = zip.nextEntry + } + manifest + } + } + }.getOrNull() + /** * Folder picker launcher with automatic permission handling * Only use this for operations that create multiple files/folders @@ -76,16 +158,6 @@ fun rememberFolderPickerWithPermission( } } -/** - * Helper function to get APK path for installed app - */ -fun getApkPath(context: Context, app: InstalledApp): String? { - return runCatching { - context.packageManager.getPackageInfo(app.currentPackageName, 0) - .applicationInfo?.sourceDir - }.getOrNull() -} - /** * Represents the result of validating a single path-valued patch option. */ @@ -130,3 +202,63 @@ fun validateOptionPaths(options: Map>>): List } return failures } + +/** + * [ActivityResultContract] that wraps ACTION_GET_CONTENT in a chooser so that + * all installed file managers appear as options - bypassing DocumentsUI which + * is the default (and often absent) handler on Android TV. + */ +class GetContentWithChooser(private val chooserTitle: String) : ActivityResultContract, Uri?>() { + override fun createIntent(context: Context, input: Array): Intent { + val getContent = Intent(Intent.ACTION_GET_CONTENT).apply { + type = if (input.size == 1) input[0] else "*/*" + if (input.size > 1) putExtra(Intent.EXTRA_MIME_TYPES, input) + addCategory(Intent.CATEGORY_OPENABLE) + } + return Intent.createChooser(getContent, chooserTitle) + } + + override fun parseResult(resultCode: Int, intent: Intent?): Uri? = + if (resultCode == android.app.Activity.RESULT_OK) intent?.data else null +} + +/** + * Returns true if the device is an Android TV or Google TV. + */ +fun Context.isAndroidTv(): Boolean { + val uiModeManager = getSystemService(UiModeManager::class.java) + return uiModeManager?.currentModeType == Configuration.UI_MODE_TYPE_TELEVISION +} + +/** + * On Android TV uses [ActivityResultContracts.OpenDocument] which routes through + * DocumentsUI and shows registered storage providers (file managers). + * On phones/tablets uses [GetContentWithChooser] to show all compatible apps. + * + * @param mimeTypes MIME types passed to the picker. OpenDocument supports multiple types + * natively; GetContentWithChooser uses the first entry as the ACTION_GET_CONTENT type. + */ +@Composable +fun rememberAdaptiveFilePicker( + mimeTypes: Array, + chooserTitle: String, + onResult: (Uri?) -> Unit, +): () -> Unit { + val context = LocalContext.current + val isTV = remember { context.isAndroidTv() } + + val tvLauncher = rememberLauncherForActivityResult( + contract = ActivityResultContracts.OpenDocument() + ) { uri -> onResult(uri) } + + val phoneLauncher = rememberLauncherForActivityResult( + contract = GetContentWithChooser(chooserTitle) + ) { uri -> onResult(uri) } + + return remember(isTV) { + { + if (isTV) tvLauncher.launch(mimeTypes) + else phoneLauncher.launch(mimeTypes) + } + } +} diff --git a/app/src/main/java/app/morphe/manager/util/JksKeyStoreParser.kt b/app/src/main/java/app/morphe/manager/util/JksKeyStoreParser.kt new file mode 100644 index 000000000..ba96b93e8 --- /dev/null +++ b/app/src/main/java/app/morphe/manager/util/JksKeyStoreParser.kt @@ -0,0 +1,151 @@ +/* + * Copyright 2026 Morphe. + * https://github.com/MorpheApp/morphe-manager + */ + +package app.morphe.manager.util + +import java.io.DataInputStream +import java.io.InputStream +import java.security.MessageDigest + +/** + * Minimal JKS keystore parser that extracts private key + certificate chain entries. + * + * Android's security provider does not include a JKS KeyStoreSpi, so we parse + * the format manually. The key protection algorithm is Sun's proprietary XOR scheme: + * keystream = SHA1(pwd || salt) || SHA1(pwd || SHA1(pwd || salt)) || ... + * plaintext = ciphertext XOR keystream + * integrity = SHA1(pwd || plaintext) (last 20 bytes of the protected-key blob) + * + * Reference: sun.security.provider.KeyProtector (OpenJDK source) + */ +object JksKeyStoreParser { + + private const val JKS_MAGIC = 0xFEEDFEED.toInt() + private const val TAG_PRIVATE_KEY = 1 + private const val TAG_TRUSTED_CERT = 2 + + data class JksEntry( + val alias: String, + val privateKeyDer: List, + val certificatesDer: List> + ) + + /** + * Parses a JKS [inputStream] and returns all private key entries. + * [password] is used both to decrypt each key entry and to verify keystore integrity. + * + * @throws IllegalArgumentException if the stream is not a valid JKS keystore. + * @throws SecurityException if key decryption fails (wrong password). + */ + fun parse(inputStream: InputStream, password: String): List { + val dis = DataInputStream(inputStream.buffered()) + + val magic = dis.readInt() + require(magic == JKS_MAGIC) { "Not a JKS keystore (magic=0x${magic.toString(16)})" } + + val version = dis.readInt() + require(version == 1 || version == 2) { "Unsupported JKS version: $version" } + + val count = dis.readInt() + val entries = mutableListOf() + + repeat(count) { + when (dis.readInt()) { + TAG_PRIVATE_KEY -> entries.add(readPrivateKeyEntry(dis, password)) + TAG_TRUSTED_CERT -> skipTrustedCertEntry(dis) + else -> error("Unknown JKS entry tag") + } + } + + return entries + } + + private fun readPrivateKeyEntry(dis: DataInputStream, password: String): JksEntry { + val alias = dis.readUTF() + dis.readLong() // timestamp - unused + + val protectedKeyLen = dis.readInt() + val protectedKey = ByteArray(protectedKeyLen).also { dis.readFully(it) } + val privateKeyDer = decryptKey(protectedKey, password).toList() + + val chainCount = dis.readInt() + val certs = (0 until chainCount).map { + dis.readUTF() + val certLen = dis.readInt() + ByteArray(certLen).also { dis.readFully(it) }.toList() + } + + return JksEntry(alias, privateKeyDer, certs) + } + + private fun skipTrustedCertEntry(dis: DataInputStream) { + dis.readUTF() // alias + dis.readLong() // timestamp + dis.readUTF() // cert type + val len = dis.readInt() + dis.skipBytes(len) + } + + /** + * Decrypts a Sun JKS protected key blob. + * + * Blob layout: [20 salt][ciphertext][20 sha1_check] + * Keystream: SHA1(pwd_utf16be || salt) || SHA1(pwd_utf16be || prev_hash) || ... + * Verify: SHA1(pwd_utf16be || plaintext) == sha1_check + */ + private fun decryptKey(protectedKey: ByteArray, password: String): ByteArray { + // The EncryptedPrivateKeyInfo DER wraps the actual protected blob. + // Structure: SEQUENCE { AlgorithmIdentifier(16 bytes from offset 4), OCTET STRING { blob } } + // AlgorithmIdentifier is always 16 bytes for OID 1.3.6.1.4.1.42.2.17.1.1 with NULL params. + val algoIdLen = 16 + val outerHeaderLen = 4 // 30 82 xx xx + var pos = outerHeaderLen + algoIdLen + + // OCTET STRING tag + length (may be 2 or 4 bytes depending on size) + pos++ // skip 0x04 tag + val lenByte = protectedKey[pos++].toInt() and 0xff + if (lenByte and 0x80 != 0) { + val n = lenByte and 0x7f + pos += n // skip multi-byte length + } + + val blob = protectedKey.copyOfRange(pos, protectedKey.size) + val salt = blob.copyOfRange(0, 20) + val ciphertext = blob.copyOfRange(20, blob.size - 20) + val check = blob.copyOfRange(blob.size - 20, blob.size) + + val pwdUtf16 = password.toByteArray(Charsets.UTF_16BE) + + val keystream = generateKeystream(pwdUtf16, salt, ciphertext.size) + val plaintext = ByteArray(ciphertext.size) { i -> (ciphertext[i].toInt() xor keystream[i].toInt()).toByte() } + + val sha1 = MessageDigest.getInstance("SHA-1") + sha1.update(pwdUtf16) + sha1.update(plaintext) + val verify = sha1.digest() + + if (!verify.contentEquals(check)) { + throw SecurityException("JKS key decryption failed - wrong password for key entry") + } + + return plaintext + } + + private fun generateKeystream(pwdUtf16: ByteArray, salt: ByteArray, length: Int): ByteArray { + val sha1 = MessageDigest.getInstance("SHA-1") + val result = ByteArray(length) + var pos = 0 + var digestInput = pwdUtf16 + salt + + while (pos < length) { + val hash = sha1.digest(digestInput) + val copy = minOf(hash.size, length - pos) + hash.copyInto(result, pos, 0, copy) + pos += copy + digestInput = pwdUtf16 + hash + } + return result + } +} diff --git a/app/src/main/java/app/morphe/manager/util/KeystoreConversionUtils.kt b/app/src/main/java/app/morphe/manager/util/KeystoreConversionUtils.kt new file mode 100644 index 000000000..17728ccaa --- /dev/null +++ b/app/src/main/java/app/morphe/manager/util/KeystoreConversionUtils.kt @@ -0,0 +1,141 @@ +/* + * Copyright 2026 Morphe. + * https://github.com/MorpheApp/morphe-manager + */ + +package app.morphe.manager.util + +import java.io.ByteArrayOutputStream +import java.io.InputStream +import java.security.KeyFactory +import java.security.KeyStore +import java.security.cert.CertificateFactory +import java.security.spec.PKCS8EncodedKeySpec + +enum class KeystoreInputFormat( + val displayName: String, + val extensions: List, + val jcaType: String +) { + KEYSTORE(".keystore (BKS)", listOf("keystore"), "BKS"), + BKS(".bks (BKS)", listOf("bks"), "BKS"), + PKCS12(".p12 / .pfx (PKCS12)", listOf("p12", "pfx"), "PKCS12"), + JKS(".jks (JKS)", listOf("jks"), "JKS"); + + companion object { + fun fromExtension(ext: String): KeystoreInputFormat? = + entries.firstOrNull { ext.lowercase() in it.extensions } + + /** + * Sniff the keystore format from the first 4 bytes of the file. + * Returns null if the header is not recognized. + * + * PKCS12 - ASN.1 SEQUENCE: 0x30 + any BER length byte (0x80–0x84) + * JKS - magic: 0xFEEDFEED + * BKS - 4-byte big-endian version int, value 1 or 2 + */ + fun detectFromBytes(header: ByteArray): KeystoreInputFormat? { + if (header.size < 4) return null + return when { + header[0] == 0x30.toByte() && header[1] in byteArrayOf( + 0x80.toByte(), 0x81.toByte(), 0x82.toByte(), 0x83.toByte(), 0x84.toByte() + ) -> PKCS12 + header[0] == 0xFE.toByte() && header[1] == 0xED.toByte() && + header[2] == 0xFE.toByte() && header[3] == 0xED.toByte() -> JKS + header[0] == 0x00.toByte() && header[1] == 0x00.toByte() && + header[2] == 0x00.toByte() && + (header[3] == 0x01.toByte() || header[3] == 0x02.toByte()) -> BKS + else -> null + } + } + } +} + +sealed interface KeystoreConversionResult { + /** Conversion succeeded - [data] is a BKS keystore ready for [app.morphe.manager.domain.manager.KeystoreManager.import]. */ + data class Success(val data: List) : KeystoreConversionResult + /** Wrong password/alias, corrupt file, or unsupported format variant. */ + data class Error(val cause: Exception) : KeystoreConversionResult +} + +object KeystoreConversionUtils { + + /** + * Loads a keystore of [format] from [inputStream] and re-encodes all entries into a + * new BKS keystore, returning the raw bytes. The stream is read but not closed. + * + * When [alias] is blank all entries are transferred. Otherwise, the matching entry is + * looked up case-insensitively, falling back to transferring everything if not found. + * + * [password] is used for both the keystore and the individual key entries. + */ + fun convert( + inputStream: InputStream, + format: KeystoreInputFormat, + alias: String, + password: String + ): KeystoreConversionResult = runCatching { + val pass = password.toCharArray() + + // JKS requires manual parsing - Android's BC does not include JKSKeyStoreSpi. + // JksKeyStoreParser decrypts the key using Sun's proprietary XOR/SHA1 scheme + if (format == KeystoreInputFormat.JKS) { + val entries = JksKeyStoreParser.parse(inputStream, password) + check(entries.isNotEmpty()) { "No entries found in JKS keystore" } + + val jksTarget = KeyStore.getInstance("BKS").apply { load(null, pass) } + val cf = CertificateFactory.getInstance("X.509") + val kf = KeyFactory.getInstance("RSA") + + val filtered = if (alias.isBlank()) entries + else entries.filter { it.alias.equals(alias, ignoreCase = true) } + .ifEmpty { entries } + + filtered.forEach { entry -> + val privateKey = kf.generatePrivate(PKCS8EncodedKeySpec(entry.privateKeyDer.toByteArray())) + val certs = entry.certificatesDer.map { + cf.generateCertificate(it.toByteArray().inputStream()) + }.toTypedArray() + jksTarget.setKeyEntry(entry.alias, privateKey, pass, certs) + } + + val out = ByteArrayOutputStream() + jksTarget.store(out, pass) + return@runCatching KeystoreConversionResult.Success(out.toByteArray().toList()) + } + + // PKCS12 uses Android's default provider which handles it natively + val source = KeyStore.getInstance(format.jcaType).apply { load(inputStream, pass) } + + val entriesToMigrate = if (alias.isBlank()) { + source.aliases().toList() + } else { + source.aliases().toList() + .filter { it.equals(alias, ignoreCase = true) } + .ifEmpty { source.aliases().toList() } + } + + check(entriesToMigrate.isNotEmpty()) { "No entries found in keystore" } + + // Target BKS keystore using Android's default provider + val target = KeyStore.getInstance("BKS").apply { load(null, pass) } + + for (entryAlias in entriesToMigrate) { + val protection = KeyStore.PasswordProtection(pass) + when { + source.isKeyEntry(entryAlias) -> { + val entry = source.getEntry(entryAlias, protection) ?: continue + target.setEntry(entryAlias, entry, protection) + } + source.isCertificateEntry(entryAlias) -> + target.setCertificateEntry(entryAlias, source.getCertificate(entryAlias)) + } + } + + val out = ByteArrayOutputStream() + target.store(out, pass) + KeystoreConversionResult.Success(out.toByteArray().toList()) + }.getOrElse { e -> + KeystoreConversionResult.Error(e as? Exception ?: RuntimeException(e)) + } +} diff --git a/app/src/main/java/app/morphe/manager/util/LocaleUtils.kt b/app/src/main/java/app/morphe/manager/util/LocaleUtils.kt index 83c26364e..529e4a801 100644 --- a/app/src/main/java/app/morphe/manager/util/LocaleUtils.kt +++ b/app/src/main/java/app/morphe/manager/util/LocaleUtils.kt @@ -24,36 +24,6 @@ fun parseLocaleCode(code: String): Locale? { } } -/** - * Convert a legacy Android resource locale code to BCP 47 format. - * - * Examples: - * - `"uk-rUA"` → `"uk-UA"` - * - `"in-rID"` → `"id-ID"` - * - `"iw-rIL"` → `"he-IL"` - * - `"uk-UA"` → `"uk-UA"` (already BCP 47 — no change) - * - `"system"` → `"system"` - */ -fun migrateLegacyLocaleCode(code: String): String { - if (code.isBlank() || code == "system") return code - - // Legacy Android resource format: "uk-rUA" → "uk-UA" - val normalized = if (code.contains("-r")) { - code.replace("-r", "-") - } else { - code - } - - // Legacy language codes: in → id, iw → he - return when { - normalized.startsWith("in-") -> normalized.replaceFirst("in-", "id-") - normalized.startsWith("iw-") -> normalized.replaceFirst("iw-", "he-") - normalized == "in" -> "id" - normalized == "iw" -> "he" - else -> normalized - } -} - /** * Returns the list of supported locale codes, excluding "en" which is * handled separately as the default language. diff --git a/app/src/main/java/app/morphe/manager/util/PM.kt b/app/src/main/java/app/morphe/manager/util/PM.kt index 73e2a0e57..e85f14d48 100644 --- a/app/src/main/java/app/morphe/manager/util/PM.kt +++ b/app/src/main/java/app/morphe/manager/util/PM.kt @@ -2,11 +2,8 @@ package app.morphe.manager.util import android.annotation.SuppressLint import android.app.Application -import android.app.PendingIntent -import android.content.Context import android.content.Intent import android.content.pm.PackageInfo -import android.content.pm.PackageInstaller import android.content.pm.PackageManager import android.content.pm.PackageManager.NameNotFoundException import android.content.pm.PackageManager.PackageInfoFlags @@ -15,20 +12,9 @@ import android.os.Build import android.os.Parcelable import androidx.compose.runtime.Immutable import androidx.core.content.pm.PackageInfoCompat -import app.morphe.manager.domain.repository.PatchBundleRepository -import app.morphe.manager.receiver.InstallReceiver -import app.morphe.manager.receiver.UninstallReceiver -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.async -import kotlinx.coroutines.flow.flowOn -import kotlinx.coroutines.flow.map -import kotlinx.coroutines.withContext import kotlinx.parcelize.Parcelize import java.io.File -private const val byteArraySize = 1024 * 1024 // Because 1,048,576 is not readable - @Immutable @Parcelize data class AppInfo( @@ -39,70 +25,10 @@ data class AppInfo( @SuppressLint("QueryPermissionsNeeded") class PM( - private val app: Application, - patchBundleRepository: PatchBundleRepository + private val app: Application ) { - private val scope = CoroutineScope(Dispatchers.IO) val application: Application get() = app - val appList = patchBundleRepository.enabledBundlesInfoFlow.map { bundles -> - val compatibleApps = scope.async { - val compatiblePackages = bundles - .flatMap { (_, bundle) -> bundle.patches } - .flatMap { it.compatiblePackages.orEmpty() } - .mapNotNull { pkg -> pkg.packageName } - .groupingBy { it } - .eachCount() - - compatiblePackages.keys.map { pkg -> - AppInfo( - pkg, - compatiblePackages[pkg], - getPackageInfo(pkg) - ) - } - } - - val installedApps = scope.async { - getInstalledPackages().map { packageInfo -> - AppInfo( - packageInfo.packageName, - 0, - packageInfo - ) - } - } - - val compatibleList = compatibleApps.await() - if (compatibleList.isNotEmpty()) { - (compatibleList + installedApps.await()) - .distinctBy { it.packageName } - .sortedWith( - compareByDescending { - it.packageInfo != null && (it.patches ?: 0) > 0 - }.thenByDescending { - it.patches - }.thenBy { - it.packageInfo?.label() - }.thenBy { it.packageName } - ) - } else { - emptyList() - } - }.flowOn(Dispatchers.IO) - - private fun getInstalledPackages(flags: Int = 0): List = - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) - app.packageManager.getInstalledPackages(PackageInfoFlags.of(flags.toLong())) - else - app.packageManager.getInstalledPackages(flags) - - fun getPackagesWithFeature(feature: String) = - getInstalledPackages(PackageManager.GET_CONFIGURATIONS) - .filter { pkg -> - pkg.reqFeatures?.any { it.name == feature } == true - } - fun getPackageInfo(packageName: String, flags: Int = 0): PackageInfo? = try { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) @@ -134,31 +60,6 @@ class PM( fun getVersionCode(packageInfo: PackageInfo) = PackageInfoCompat.getLongVersionCode(packageInfo) - fun getSignature(packageName: String): Signature = - // Get the last signature from the list because we want the newest one if SigningInfo.getSigningCertificateHistory() was used. - PackageInfoCompat.getSignatures(app.packageManager, packageName).last() - - @SuppressLint("InlinedApi") - fun hasSignature(packageName: String, signature: ByteArray) = PackageInfoCompat.hasSignatures( - app.packageManager, - packageName, - mapOf(signature to PackageManager.CERT_INPUT_RAW_X509), - false - ) - - suspend fun installApp(apks: List) = withContext(Dispatchers.IO) { - val packageInstaller = app.packageManager.packageInstaller - packageInstaller.openSession(packageInstaller.createSession(sessionParams)).use { session -> - apks.forEach { apk -> session.writeApk(apk) } - session.commit(app.installIntentSender) - } - } - - fun uninstallPackage(pkg: String) { - val packageInstaller = app.packageManager.packageInstaller - packageInstaller.uninstall(pkg, app.uninstallIntentSender) - } - fun launch(pkg: String) = app.packageManager.getLaunchIntentForPackage(pkg)?.let { it.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) app.startActivity(it) @@ -166,35 +67,58 @@ class PM( fun canInstallPackages() = app.packageManager.canRequestPackageInstalls() - fun isAppDeleted(packageName: String, hasSavedCopy: Boolean, wasInstalledOnDevice: Boolean): Boolean { - val currentlyInstalled = getPackageInfo(packageName) != null - return !currentlyInstalled && wasInstalledOnDevice && hasSavedCopy + /** + * Returns the first signing certificate of an installed package, or null if not found. + */ + @Suppress("DEPRECATION") + fun getSignature(packageName: String): Signature? { + val flags = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { + PackageManager.GET_SIGNING_CERTIFICATES + } else { + PackageManager.GET_SIGNATURES + } + val info = getPackageInfo(packageName, flags) ?: return null + return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { + info.signingInfo?.apkContentsSigners?.firstOrNull() + ?: info.signatures?.firstOrNull() + } else { + info.signatures?.firstOrNull() + } } - private fun PackageInstaller.Session.writeApk(apk: File) { - apk.inputStream().use { inputStream -> - openWrite(apk.name, 0, apk.length()).use { outputStream -> - inputStream.copyTo(outputStream, byteArraySize) - fsync(outputStream) - } + /** + * Returns the first signing certificate of an APK file, or null if not found. + */ + @Suppress("DEPRECATION") + fun getArchiveSignature(file: File): Signature? { + val flags = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { + PackageManager.GET_SIGNING_CERTIFICATES or PackageManager.GET_SIGNATURES + } else { + PackageManager.GET_SIGNATURES + } + val info = app.packageManager.getPackageArchiveInfo(file.absolutePath, flags) ?: return null + return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { + info.signingInfo?.apkContentsSigners?.firstOrNull() + ?: info.signatures?.firstOrNull() + } else { + info.signatures?.firstOrNull() } } - private val intentFlags - get() = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) - PendingIntent.FLAG_MUTABLE - else - 0 - - private val sessionParams - get() = PackageInstaller.SessionParams( - PackageInstaller.SessionParams.MODE_FULL_INSTALL - ).apply { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) - setRequestUpdateOwnership(true) - @SuppressLint("WrongConstant") - setInstallReason(PackageManager.INSTALL_REASON_USER) - } + /** + * Returns true if the signing certificate of [file] differs from the installed package. + * Returns false if either signature cannot be read (install proceeds normally). + */ + fun hasSignatureMismatch(packageName: String, file: File): Boolean { + val installed = getSignature(packageName)?.toByteArray() ?: return false + val archive = getArchiveSignature(file)?.toByteArray() ?: return false + return !installed.contentEquals(archive) + } + + fun isAppDeleted(packageName: String, hasSavedCopy: Boolean, wasInstalledOnDevice: Boolean): Boolean { + val currentlyInstalled = getPackageInfo(packageName) != null + return !currentlyInstalled && wasInstalledOnDevice && hasSavedCopy + } private fun cleanLabel(raw: String, packageName: String): String { val trimmed = raw.trim() @@ -207,20 +131,4 @@ class PM( val candidate = withoutSuffix.ifBlank { base } return candidate.ifBlank { trimmed } } - - private val Context.installIntentSender - get() = PendingIntent.getBroadcast( - this, - 0, - Intent(this, InstallReceiver::class.java), - intentFlags or PendingIntent.FLAG_UPDATE_CURRENT - ).intentSender - - private val Context.uninstallIntentSender - get() = PendingIntent.getBroadcast( - this, - 0, - Intent(this, UninstallReceiver::class.java), - intentFlags or PendingIntent.FLAG_UPDATE_CURRENT - ).intentSender } diff --git a/app/src/main/java/app/morphe/manager/util/Util.kt b/app/src/main/java/app/morphe/manager/util/Util.kt index 62d018e69..9d01ef1a6 100644 --- a/app/src/main/java/app/morphe/manager/util/Util.kt +++ b/app/src/main/java/app/morphe/manager/util/Util.kt @@ -39,6 +39,7 @@ import kotlinx.datetime.format.Padding import kotlinx.datetime.format.char import kotlinx.datetime.toInstant import kotlinx.datetime.toLocalDateTime +import kotlin.coroutines.cancellation.CancellationException import kotlin.properties.PropertyDelegateProvider import kotlin.properties.ReadWriteProperty import kotlin.reflect.KProperty @@ -48,8 +49,8 @@ typealias PatchSelection = Map> typealias Options = Map>> fun isArmV7(): Boolean { - val abis = Build.SUPPORTED_ABIS.map { it.lowercase() } - return abis.any { it.contains("armeabi-v7a") } + // Only check the primary ABI - ArmV8 devices also list armeabi-v7a as a secondary ABI + return Build.SUPPORTED_ABIS.firstOrNull()?.lowercase()?.contains("armeabi-v7a") == true } fun Context.toastHandle(string: String, duration: Int = Toast.LENGTH_SHORT): Toast = @@ -186,6 +187,25 @@ fun htmlAnnotatedString(html: String): AnnotatedString { fun Modifier.enabled(condition: Boolean) = if (condition) this else alpha(0.5f) +/** + * Returns a human-readable Android version name for this SDK integer. + */ +fun Int.androidVersionName(): String = when (this) { + 37 -> "17" + 36 -> "16" + 35 -> "15" + 34 -> "14" + 33 -> "13" + 32 -> "12L" + 31 -> "12" + 30 -> "11" + 29 -> "10" + 28 -> "9" + 27 -> "8.1" + 26 -> "8.0" + else -> "$this" // future or very old SDK - just use the number +} + @MainThread fun SavedStateHandle.saveableVar(init: () -> T): PropertyDelegateProvider> = PropertyDelegateProvider { _: Any?, property -> diff --git a/app/src/main/java/app/morphe/manager/worker/UpdateCheckWorker.kt b/app/src/main/java/app/morphe/manager/worker/UpdateCheckWorker.kt index e361c3408..9e66692df 100644 --- a/app/src/main/java/app/morphe/manager/worker/UpdateCheckWorker.kt +++ b/app/src/main/java/app/morphe/manager/worker/UpdateCheckWorker.kt @@ -15,7 +15,6 @@ import app.morphe.manager.domain.repository.PatchBundleRepository import app.morphe.manager.network.api.MorpheAPI import app.morphe.manager.util.UpdateNotificationManager import app.morphe.manager.util.tag -import app.morphe.manager.worker.UpdateCheckInterval.HOURLY import kotlinx.coroutines.flow.first import org.koin.core.component.KoinComponent import org.koin.core.component.inject diff --git a/app/src/main/jniLibs/arm64-v8a/libaapt2.so b/app/src/main/jniLibs/arm64-v8a/libaapt2.so deleted file mode 100644 index 704314abd..000000000 Binary files a/app/src/main/jniLibs/arm64-v8a/libaapt2.so and /dev/null differ diff --git a/app/src/main/jniLibs/armeabi-v7a/libaapt2.so b/app/src/main/jniLibs/armeabi-v7a/libaapt2.so deleted file mode 100644 index 67778042a..000000000 Binary files a/app/src/main/jniLibs/armeabi-v7a/libaapt2.so and /dev/null differ diff --git a/app/src/main/jniLibs/x86/libaapt2.so b/app/src/main/jniLibs/x86/libaapt2.so deleted file mode 100644 index 15ec894f1..000000000 Binary files a/app/src/main/jniLibs/x86/libaapt2.so and /dev/null differ diff --git a/app/src/main/jniLibs/x86_64/libaapt2.so b/app/src/main/jniLibs/x86_64/libaapt2.so deleted file mode 100644 index dd3710556..000000000 Binary files a/app/src/main/jniLibs/x86_64/libaapt2.so and /dev/null differ diff --git a/app/src/main/res/values-af-rZA/strings.xml b/app/src/main/res/values-af-rZA/strings.xml index 180ea1689..48f97ef60 100644 --- a/app/src/main/res/values-af-rZA/strings.xml +++ b/app/src/main/res/values-af-rZA/strings.xml @@ -39,10 +39,12 @@ + + diff --git a/app/src/main/res/values-am-rET/strings.xml b/app/src/main/res/values-am-rET/strings.xml index 180ea1689..48f97ef60 100644 --- a/app/src/main/res/values-am-rET/strings.xml +++ b/app/src/main/res/values-am-rET/strings.xml @@ -39,10 +39,12 @@ + + diff --git a/app/src/main/res/values-ar-rSA/plurals.xml b/app/src/main/res/values-ar-rSA/plurals.xml index f5d183326..64652bc44 100644 --- a/app/src/main/res/values-ar-rSA/plurals.xml +++ b/app/src/main/res/values-ar-rSA/plurals.xml @@ -33,12 +33,12 @@ %s حزمة - تنفيذ %s تعديل - تنفيذ %s تعديل - تنفيذ %s تعديلان - تنفيذ %s تعديلات - تنفيذ %s تعديلًا - تنفيذ %s تعديلًا + تم تنفيذ %s تعديل + تم تنفيذ %s تعديل + تم تنفيذ %s تعديلين + تم تنفيذ %s تعديلات + تم تنفيذ %s تعديل + تم تنفيذ %s تعديل %s ملف APK @@ -72,4 +72,20 @@ %s تعديل محدد %s تعديل محدد + + إظهار %s تطبيق + إظهار %s تطبيق + إظهار %s تطبيق + إظهار %s تطبيق + إظهار %s تطبيق + إظهار %s تطبيق + + + %s تطبيق مخفي + %s تطبيق مخفي + %s تطبيق مخفي + %s تطبيق مخفي + %s تطبيق مخفي + %s تطبيق مخفي + diff --git a/app/src/main/res/values-ar-rSA/strings.xml b/app/src/main/res/values-ar-rSA/strings.xml index ed34e4948..3c77fcfbe 100644 --- a/app/src/main/res/values-ar-rSA/strings.xml +++ b/app/src/main/res/values-ar-rSA/strings.xml @@ -32,6 +32,8 @@ لا توجد تطبيقات متاحة أضِف مصدرًا للتعديل أو قم بتمكين مصدر موجود في %s لا يوجد تطبيقات تطابق \"%1$s\" + تم إخفاء جميع التطبيقات + لقد أخفيت جميع التطبيقات من الشاشة الرئيسية ما التطبيق الذي تريد تعديله؟ @@ -91,10 +93,14 @@ لتعديل <b>%s</b>، تحتاج إلى ملف APK غير معدل من الإصدارات: موصى به غير معدل + غير متوافق نعم، ساعدني في العثور على ملف APK لا، لدي ملف APK بالفعل استخدام ملف APK المحفوظ (v%s) لا يوجد ملف APK محفوظ. ستتطلب عملية التعديل إعادة اختيار ملف APK + يتطلب Android %1$s+ + غير مدعوم على هذا الجهاز + <b>%1$s</b> لا يحتوي على إصدارات مدعومة لنظام Android %2$d (واجهة برمجة التطبيقات %3$d). جميع الإصدارات المعلنة تتطلب إصدار Android أعلى تنزيل ملف APK الأصلي التعليمات: @@ -221,6 +227,7 @@ إصدار أحدث من التعديلات متاح. اعد تعديل التطبيق للحصول على آخر التحسينات و الإصلاحات التطبيق غير مثبت تم إلغاء تثبيت هذا التطبيق من خارج Morphe. اعد تعديله لاستعادة الوظائف + حجم ملف APK يتطلب وضع Root تثبيت ملف APK الأصلي على جهازك قبل التعديل تم استبعاد تعديل دعم GmsCore في وضع Root @@ -308,16 +315,13 @@ فشل تطبيق %s تم حفظ التطبيق المعدل لوقت لاحق فشل حفظ التطبيق المعدل - تنزيل ملف APK يلزم تفاعل المستخدم للمتابعة باستخدام هذا المكون الإضافي انتهت عملية أداة التعديل بالرمز %1$s تم التثبيت بنجاح فشل تثبيت التطبيق: %s - لم يكتمل التثبيت. تحقق من مربع حوار أداة التثبيت في النظام وحاول مرة أخرى تم حفظ APK فشل تصدير التطبيق المعدل تمت إزالة التطبيق المعدل المحفوظ - تمت إزالة النسخة المحفوظة قم بتثبيت التطبيق قبل فتحه اسم الحزمة فشل تركيب: %s @@ -337,6 +341,10 @@ شبكة جسيمات بدون + عشوائي + عند التشغيل + يوميًا + كل 3 أيام تأثير المنظر البانورامي تغيير سلس للخلفية عند إمالة الجهاز @@ -353,6 +361,10 @@ اللغة الحالية قد تكون ترجمات بعض اللغات مفقودة أو غير مكتملة لترجمة لغات جديدة أو تحسين الترجمات الحالية، تفضل بزيارة %s + + الشاشة الرئيسية + عبارات الترحيب + عرض رسالة ترحيب على الشاشة الرئيسية أيقونة التطبيق الافتراضي @@ -399,8 +411,17 @@ تمكين إعدادات تعديل Morphe المتقدمة وخيارات التخصيص تمكين وضع الخبير؟ وضع الخبير يوفر مزيدًا من التحكم في كيفية تطبيق التعديلات، ولكن الإعداد الخاطئ لوضع الخبير يمكن أن يوقف التطبيق عن العمل - إزالة المكتبات الأصلية غير المستخدمة - حذف المكتبات الأصلية لبنى المعالجات غير المدعومة من التطبيقات المعدلة + تحسين وفق معمارية الجهاز + "تخطي وحدات APK المقسمة الخاصة بمعماريات المعالج، اللغات وكثافات الشاشة غير المدعومة أثناء الدمج. +بالنسبة لملفات APK العادية، يتم إزالة المكتبات الأصلية الخاصة بالمعماريات غير المدعومة بعد التعديل" + + وضع المعالجة Bytecode + يتحكم في كيفية معالجة Bytecode أثناء التعديل. يؤثر على سرعة التعديل، استخدام الذاكرة، وحجم APK الناتج + السريع (موصى به) + السريع + تعديل أسرع واستخدام أقل للذاكرة، على حساب حجم APK أكبر. موصى به للأجهزة ذات ذاكرة الوصول العشوائي المنخفضة أو الأجهزة القديمة + الكامل + تعديل أبطأ، يكرر السلوك القديم. استخدمه فقط إذا تسبب الوضع السريع في حدوث مشكلات رمز GitHub PAT مطلوب لمصادر طلب السحب @@ -510,13 +531,7 @@ morphe_adaptive_monochrome_custom.xml إصدار Shizuku المثبت غير مدعوم فتح Shizuku فشل التثبيت. حاول مرة أخرى أو قم بالتبديل إلى أداة تثبيت أخرى - تم حظر التثبيت بواسطة Android. تحقق من إعدادات Play Protect أو الأمان يوجد تطبيق آخر بنفس اسم الحزمة مثبت بالفعل. قم بإلغاء تثبيته قبل المتابعة - ملف APK غير متوافق مع هذا الجهاز أو إصدار Android - ملف APK غير صالح أو تالف - لا توجد مساحة تخزين كافية للتثبيت - انتهت مهلة التثبيت. حاول مرة أخرى - تم إلغاء التثبيت جارٍ فتح %1$s… لم يؤكد %1$s التثبيت. تحقق من التطبيق الآخر وحاول مرة أخرى أفاد %1$s عن نجاح التثبيت @@ -547,6 +562,7 @@ morphe_adaptive_monochrome_custom.xml اسم المستخدم (الاسم المستعار) كلمة المرور استيراد + تنسيق مخزن المفاتيح بيانات مخزن المفاتيح غير صحيحة تم استيراد مخزن المفاتيح فشل استيراد مخزن المفاتيح @@ -554,6 +570,7 @@ morphe_adaptive_monochrome_custom.xml تصدير مخزن المفاتيح الحالي لا يوجد مخزن مفاتيح للتصدير تم تصدير مخزن المفاتيح + فشل تصدير مخزن المفاتيح عرض كلمة المرور إخفاء كلمة المرور @@ -682,7 +699,8 @@ morphe_adaptive_monochrome_custom.xml السماح التفاصيل إخفاء - الافتراضي + مخفي + إظهار السجلات المصادر جارٍ التثبيت… diff --git a/app/src/main/res/values-as-rIN/plurals.xml b/app/src/main/res/values-as-rIN/plurals.xml index a25712873..6fdf646d3 100644 --- a/app/src/main/res/values-as-rIN/plurals.xml +++ b/app/src/main/res/values-as-rIN/plurals.xml @@ -16,10 +16,6 @@ %s প্যাকেজ %s টা প্যাকেজ - - %s টা প্যাচ কাৰ্যান্বিত কৰক - %s টা প্যাচ কাৰ্যান্বিত কৰক - %s টা APK ফাইল %s টা APK ফাইল diff --git a/app/src/main/res/values-as-rIN/strings.xml b/app/src/main/res/values-as-rIN/strings.xml index d9f62f727..795c7c2ee 100644 --- a/app/src/main/res/values-as-rIN/strings.xml +++ b/app/src/main/res/values-as-rIN/strings.xml @@ -39,10 +39,12 @@ + + টা প্যাচ বিকল্প diff --git a/app/src/main/res/values-az-rAZ/plurals.xml b/app/src/main/res/values-az-rAZ/plurals.xml index 4eabd4615..10f1d6358 100644 --- a/app/src/main/res/values-az-rAZ/plurals.xml +++ b/app/src/main/res/values-az-rAZ/plurals.xml @@ -17,8 +17,8 @@ %s paket - %s yamaq icra edildi - %s yamaq icra edildi + %s yamaq icra olundu + %s yamaq icra olundu %s APK faylı @@ -36,4 +36,12 @@ %s yamaq seçildi %s yamaq seçildi + + %s tətbiqi göstər + %s tətbiqi göstər + + + %s gizli tətbiq + %s gizli tətbiq + diff --git a/app/src/main/res/values-az-rAZ/strings.xml b/app/src/main/res/values-az-rAZ/strings.xml index 6988767d9..fa2c9df63 100644 --- a/app/src/main/res/values-az-rAZ/strings.xml +++ b/app/src/main/res/values-az-rAZ/strings.xml @@ -32,6 +32,8 @@ Mövcud tətbiq yoxdur %s bölməsində yamaq mənbəyi əlavə edin və ya mövcud olanı aktivləşdir \"%1$s\" ilə uyuşan tətbiq yoxdur + Bütün tətbiqlər gizlidir + Bütün tətbiqləri əsas ekrandan gizlətmisiniz Hansı tətbiqi yamaqlamaq istəyirsiniz? @@ -81,6 +83,7 @@ Uğurlu yeniləmə Yenilənmə əlçatan deyil Mənbələri yükləmək olmadı: %s + \'%1$s\' yükləmək olmadı: %2$s \'%1$s\' yenilənə bilmədi: yamaq JSON faylı qurulan URL-də tapılmadı Mütəxəssis Bu yamağı əl ilə yerləşdirmək üçün Tənzimləmələrdə Mütəxəssis rejimin aktivləşdir @@ -220,6 +223,7 @@ Davam etmək istədiyinizə əminsiniz?" Yamaqların daha yeni versiyası var. Ən son təkmilləşdirmələr və düzəlişlər üçün tətbiqinizi təkrar yamaqlayın Tətbiq quraşdırması silindi Bu tətbiq Morphe xarici silindi. Funksionallığı qaytarmaq üçün onu təkrar quraşdır + APK həcmi Root rejimi yamaqdan əvvəl orijinal APK-nın cihazınızda quraşdırılmasını tələb edir GmsCore dəstək yamağı root rejimində çıxarılır @@ -307,16 +311,13 @@ Davam etmək istədiyinizə əminsiniz?" %s tətbiq etmək uğursuz oldu Yamaqlanan tətbiq sonra üçün saxlanıldı Yamaqlanan tətbiqi saxlamaq olmadı - APK faylını yüklə Bu qoşmayla davam etmək üçün istifadəçi reaksiyası tələb olunur Yamaqlayıcı emalı %1$s şifrəsi ilə tamamlandı Uğurla quraşdırıldı Tətbiqi quraşdırmaq olmadı: %s - Quraşdırma bitmədi. Sistem quraşdırıcısı dialoqunu yoxla və təkrar sına APK Saxlanıldı Yamaqlanan tətbiqi ixrac etmək olmadı Yamaqlanıb saxlanılan tətbiq silindi - Saxlanılan nüsxə silindi Açmazdan əvvəl tətbiqi quraşdır Paket adı Montajlama uğursuzdu: %s @@ -352,6 +353,10 @@ Davam etmək istədiyinizə əminsiniz?" Gündəlik dil Bəzi dillər üçün tərcümələr yarımçıq və ya qüsurlu ola bilər Yeni dilləri tərcümə etmək və ya mövcud tərcümələri təkmilləşdirmək üçün %s -i ziyarət et + + Əsas ekran + Salamlaşma ifadələri + Əsas ekranda salamlaşma mesajı göstər Tətbiq simvolu İlkin @@ -398,8 +403,17 @@ Davam etmək istədiyinizə əminsiniz?" Qəliz Morphe yamaqlanma tənzimləmələrin və fərdiləşdirmə seçimlərin aktivləşdir Mütəxəssis rejimi aktivləşdirilsin? Mütəxəssis rejimi yamaqların necə tətbiq olunduğuna daha çox nəzarət verir, lakin mütəxəssis rejimindəki tənzimləmələri səhv qurma tətbiqi funksional pozuntuya sala bilər - İşlədilməyən yerli kitabxanaları təmizlə - Yamalanan tətbiqlərdən dəstəklənməyən CPU quruluşları üçün yerli kitabxanaları silin + Cihaz quruluşu üçün optimallaşdır + "Birləşdirmə əsnasında dəstəklənməyən CPU quruluşları, yerləşmələr və ekran sıxlıqları üçün bölünən APK modullarını ötür. +Adi APK-lar üçün, yamaqlanmadan sonra dəstəklənməyən quruluşlar üçün yerli kitabxanaları sil" + + Baytkod emalı rejimi + Yamaqlanma əsnasında baytkodun necə irəlilədiyinə nəzarət edir. Yamaqlanma sürətinə, yaddaş istifadəsinə və hasilat APK həcminə təsir edir + Sürətli (Tövsiyə olunur) + Sürətli + Daha həcmli APK dəyərində daha sürətli yamaqlanma və daha az yaddaş istifadəsi. Aşağı RAM və ya köhnə cihazlar üçün tövsiyə olunur. + Tam + Daha yavaş yamaqlanma, köhnə davranışı təkrarlayır. Yalnız sürətli rejim problemlərə səbəb olursa istifadə edin. GitHub PAT Pull request mənbələri üçün tələb olunur @@ -508,13 +522,7 @@ morphe_header_custom_light.png morphe_header_custom_dark.png Quraşdırılan Shizuku versiyası dəstəklənmir Shizuku-nu Aç Quraşdırma uğursuz oldu. Təkrar sına yaxud fərqli quraşdırıcıya keç - Quraşdırma Android tərəfindən əngəlləndi. Play Protect yaxud təhlükəsizlik tənzimləmələrin yoxla Başqa tətbiq bu paket adıyla artıq quraşdırılıb. Davam etməzdən əvvəl onu sil - APK bu cihaz yaxud Android versiyası ilə uyuşan deyil - APK etibarsız və ya qüsurludur - Quraşdırma üçün yaddaş yeri kifayət deyildir - Quraşdırma vaxtı bitdi. Təkrar sına - Quraşdırma ləğv edildi %1$s açılır… %1$s quraşdırmanı təsdiqləmədi. Digər tətbiqi yoxla və təkrar sına %1$s uğurlu quraşdırmanı bildirdi @@ -552,6 +560,7 @@ morphe_header_custom_light.png morphe_header_custom_dark.png Hazırkı keystore-u ixrac et İxrac etmək üçün keystore mövcud deyil Keystore ixrac edildi + Keystore-u ixrac etmək olmadı Parolu göstər Parolu gizlət @@ -680,8 +689,10 @@ morphe_header_custom_light.png morphe_header_custom_dark.png İcazə verin Təfərrüatlar Gizlət - İlkin + Gizlədilən + Göstər Jurnallar + Mənbələr Quraşdırılır… Montajlanır... Montaj silinir… diff --git a/app/src/main/res/values-be-rBY/strings.xml b/app/src/main/res/values-be-rBY/strings.xml index 871d226c1..08d35fe70 100644 --- a/app/src/main/res/values-be-rBY/strings.xml +++ b/app/src/main/res/values-be-rBY/strings.xml @@ -50,10 +50,12 @@ + + diff --git a/app/src/main/res/values-bg-rBG/plurals.xml b/app/src/main/res/values-bg-rBG/plurals.xml index d7afd78c5..0754f1ddf 100644 --- a/app/src/main/res/values-bg-rBG/plurals.xml +++ b/app/src/main/res/values-bg-rBG/plurals.xml @@ -17,8 +17,8 @@ %s пакети - Приложи %s пач - Приложи %s пачове + Изпълнен %s пач + Изпълнени %s пачове %s APK файл @@ -36,4 +36,12 @@ %s избран пач %s избрани пачове + + Покажи %s приложение + Покажи %s приложения + + + %s скрито приложение + %s скрити приложения + diff --git a/app/src/main/res/values-bg-rBG/strings.xml b/app/src/main/res/values-bg-rBG/strings.xml index b053430fc..7dccb31aa 100644 --- a/app/src/main/res/values-bg-rBG/strings.xml +++ b/app/src/main/res/values-bg-rBG/strings.xml @@ -32,6 +32,8 @@ Няма налични приложения Добавете източник на корекция или активирайте съществуващ в %s Няма съвпадения за \"%1$s\" + Всички приложения са скрити + Скрили сте всички приложения от началния екран Кое приложение искате да пачнете? @@ -91,10 +93,14 @@ За да прилагате <b>%s</b>, нужна е непренагласена APK от версиите: Рекомендовано Необновен + Несъвместим Да, помогнете ми да намира APK Не, вече имам APK Използвайте запазеното APK (в%с) Няма запазено APK. Пatching ще изисква отново да изберете APK файл. + Изисква Android %1$s+ + Не се поддържа на това устройство + <b>%1$s</b> няма поддържани версии за Android %2$d (API %3$d). Всички декларирани версии изискват по-нова версия на Android Изтеглете оригинален APK Инструкции: @@ -221,6 +227,7 @@ Достъпна е по-нова версия на поправките. Поправете отново приложението си, за да получите последното подобрение и поправки Приложението беше деинсталирано Това приложение беше деинсталирано извън Morphe. Поправете го отново, за да възстановите функционалността + Размер на APK Режимът \"Root\" изисква оригиналния APK да бъде инсталиран на вашето устройство преди да се прилагат патчове Поддръжката на GmsCore е изключена в режим на корен @@ -308,16 +315,13 @@ Неуспешно прилагане на %s Patching on this device architecture is unsupported and will most likely fail. Import - Изтегли APK файл Потребителски интеракция е нужна, за да продължите с този плагин. Процесът на патчването излязъл с код %1$s Инсталирано успешно Неуспешно инсталиране на приложението: %s - Инсталирането не е завършило. Проверете диалога на системния инсталатор и опитайте отново. APK запазен Неуспешно изнасяне на модифицирано приложение Премахнато запазено модифицирано приложение - Премахната запазената копия Инсталирайте приложението преди да го отворите. Име на пакета Неуспешно монтиране: %s @@ -337,6 +341,10 @@ Решетка Частици Без + Случайно + При стартиране + Всеки ден + На всеки 3 дни Ефект на паралакса Сгладко преместване на фона при накланяне на устройството @@ -353,6 +361,10 @@ Текущият език Недостъпни или неполни преводи за някои езици За да преведете нови езици или подобрите съществуващите преводи, посетете %s + + Начален екран + Поздравителни фрази + Показване на поздравително съобщение на началния екран Икона на приложението По подразбиране @@ -399,8 +411,17 @@ Включете сложни настройки за патчове и опции за personnalization на Morphe Включете експерт режим? Експертният режим дава повече контрол над прилагането на патчове, но грешно конфигуриране на настройките в експертния режим може да доведе до неработеща приложение - Изтриване на непотребимите библиотеки - Изтриване на библиотеки за неподдържани CPU архитектури от замести + Оптимизиране за архитектурата на устройството + "Пропускане на разделени APK модули за неподдържани CPU архитектури, езици и резолюции на екрана по време на сливане. +За обикновени APK, премахва native библиотеки за неподдържани архитектури след поправяне" + + Режим на обработка на bytecode + Контролира как bytecode се обработва по време на поправяне. Влияе върху скоростта на поправяне, използването на памет и размера на изходния APK + Бърз (Препоръчително) + Бързо + По-бързо поправяне и по-ниска консумация на памет, но с по-голям APK файл. Препоръчително за устройства с малко RAM или по-стари устройства + Пълен + По-бавно поправяне, възпроизвежда legacy поведение. Използвайте само ако режимът бързо причинява проблеми Токен за достъп до GitHub Треба за източници на заявка за изтегляне @@ -510,13 +531,7 @@ morphe_header_custom_dark.png Версията на инсталирания Shizuku не е поддържана. Not selected Инсталирането провалило. Опитайте отново или превключете на различен инсталатор. - Инсталирането бе блокирано от Android. Проверете Play Protect или настройките за сигурност. Друго приложение с това име вече е инсталирано. Деинсталирайте го преди да продължите. - APK не е съвместим с това устройство или версия на Android. - APK е невалиден или повреден. - Няма достатъчно свободно пространство за инсталиране. - Инсталирането изтекла. Опитайте отново. - Инсталирането бе отменено. Отваряне на %1$s… %1$s не потвърди инсталацията. Проверете другата приложение и опитайте отново %1$s докладва за успешно инсталиране @@ -547,6 +562,7 @@ morphe_header_custom_dark.png Име на потребител (Алиас) Парола Импортиране + Формат на хранилището Невалидни данни за ключовата запазна книга Ключовата запазна книга е импортирана Неуспешно импортиране на ключовата запазна книга @@ -554,6 +570,7 @@ morphe_header_custom_dark.png Износ на текущата ключова запазна книга Няма налична ключова запазна книга за износ Ключовата запазна книга е изнесена + Неуспешен експорт на хранилище на ключове Показване на парола Скриване на парола @@ -682,7 +699,8 @@ morphe_header_custom_dark.png Разреши Детайли Скриване - По подразбиране + Скрито + Покажи Дневници Източници Инсталиране… diff --git a/app/src/main/res/values-bn-rBD/plurals.xml b/app/src/main/res/values-bn-rBD/plurals.xml index 538dfea1f..1377700cd 100644 --- a/app/src/main/res/values-bn-rBD/plurals.xml +++ b/app/src/main/res/values-bn-rBD/plurals.xml @@ -36,4 +36,12 @@ %s টি প্যাচ নির্বাচন করা হয়েছে %s টি প্যাচ নির্বাচন করা হয়েছে + + %s অ্যাপ্লিকেশন দেখান + %s অ্যাপ্লিকেশনগুলি দেখান + + + %s লুকানো অ্যাপ্লিকেশন + %s লুকানো অ্যাপ্লিকেশনগুলি + diff --git a/app/src/main/res/values-bn-rBD/strings.xml b/app/src/main/res/values-bn-rBD/strings.xml index 5931a71f5..63cbb21c9 100644 --- a/app/src/main/res/values-bn-rBD/strings.xml +++ b/app/src/main/res/values-bn-rBD/strings.xml @@ -32,6 +32,8 @@ কোনো অ্যাপস পাওয়া যায়নি একটি প্যাচ উৎস যোগ করুন অথবা %s-এ বিদ্যমান একটি সক্ষম করুন কোনো অ্যাপস \"%1$s\"-এর সাথে মেলে না + সমস্ত অ্যাপ্লিকেশন লুকানো হয়েছে + আপনি হোম স্ক্রীন থেকে সমস্ত অ্যাপ্লিকেশন লুকিয়ে ফেলেছেন আপনি কোন অ্যাপ প্যাচ করতে চান? @@ -91,10 +93,14 @@ প্যাচ করতে <b>%s</b> এর জন্য, অ-প্যাচড এপিকি (APK) সংস্করণ দরকার: প্রশংসিত অনুশিষ্টিত + অসঙ্গতিপূর্ণ হ্যাঁ, আমাকে একটি APK খুঁজতে সাহায্য করুন না, আমি ইতিমধ্যে একটি APK পেয়েছি বর্তমান সংরক্ষিত APK (v%s) ব্যবহার করুন কোন সংরক্ষিত APK নেই। প্যাচিংয়ের জন্য আবার APK ফাইল নির্বাচন করতে হবে + Android %1$s+ প্রয়োজন + এই ডিভাইসে সমর্থিত নয় + <b>%1$s</b>-এর Android %2$d (API %3$d)-এর জন্য কোনো সমর্থিত সংস্করণ নেই। ঘোষিত সমস্ত সংস্করণের জন্য উচ্চতর Android সংস্করণ প্রয়োজন মূল APK ডাউনলোড করুন নির্দেশাবলী: @@ -221,6 +227,7 @@ প্যাচের নতুন ভার্সন উপলব্ধ। আপনার অ্যাপকে পুনরায় প্যাচ করুন সর্বশেষ উন্নতির জন্য অ্যাপটি বের করা হয়েছে এই অ্যাপটি মরফে বাইরে বের করা হয়েছে। ফাংশনালিটি পুনরুদ্ধার করতে পুনরায় প্যাচ করুন + APK এর আকার রুট মোডে আরিজিনাল এপিকে ডিভাইসে ইনস্টল করা থাকতে হবে প্যাচিংয়ের আগে রুট মোডে GmsCore সমর্থন প্যাচ বাদ দেওয়া হয়েছে @@ -308,16 +315,13 @@ %s প্রয়োগ করতে ব্যর্থ প্যাচ করা অ্যাপ পরে জন্য সংরক্ষণ করা হয়েছে প্যাচ করা অ্যাপ সংরক্ষণ করতে ব্যর্থ - APK ফাইল ডাউনলোড করুন এই প্লাগইন চালিয়ে যেতে জন্য ব্যবহারকারীর ইন্টারেকশন প্রয়োজন প্যাচার প্রক্রিয়া কোড %1$s এর সাথে বের হয়েছে সাফল্য়ের সাথে ইনস্টল করা হয়েছে অ্যাপ ইনস্টল করতে ব্যর্থ: %s - ইনস্টলেশন শেষ হয়নি. সিস্টেম ইনস্টলার ডায়ালগকে চেক করুন এবং আবার চেষ্টা করুন APK সেভ করা হয়েছে প্যাচ করা অ্যাপ এক্সপোর্ট করতে ব্যর্থ সেভ করা প্যাচ করা অ্যাপ সরানো হয়েছে - সংরক্ষিত কপি সরানো হয়েছে এই অ্যাপটি খুলতে আগে ইনস্টল করুন প্যাকেজের নাম মাউন্ট করতে ব্যর্থ: %s @@ -337,6 +341,10 @@ গ্রিড কণা থিম + র্যান্ডম + চালু করার সময় + দৈনিক + প্রতি ৩ দিনে প্যারাল্যাক্স ইফেক্ট ডিভাইসকে কাঁটিলে পটভূমি পরিবর্তনের সাবলীলতা @@ -353,6 +361,10 @@ বর্তমান ভাষা কিছু ভাষার জন্য অনুবাদ বা সম্পূর্ণ অনুবাদ উপলব্ধ নেই নতুন ভাষা অনুবাদ বা বর্তমান অনুবাদ উন্নগ্রদন করতে, %s-এ যান + + হোম স্ক্রিন + শুভেচ্ছা বার্তা + হোম স্ক্রিনে একটি শুভেচ্ছা বার্তা দেখান অ্যাপ আইকন ডিফল্ট @@ -399,8 +411,16 @@ কমপ্লেক্স মর্ফে প্যাচিং সেটিংস ও কাস্টমাইজেশন অপশনসকে সক্ষম করুন এবং নিয়ন্ত্রণ করুন এক্সপার্ট মোড সক্ষম করবেন? এক্সপার্ট মোড অ্যাপ্লিকেশনগুলিতে প্যাচগুলি কীভাবে প্রয়োগ করা হবে তার উপর বেশি নিয়ন্ত্রণ প্রদান করে, কিন্তু এক্সপার্ট মোডে সেটিংস মিসকনফিগার করলে অ্যাপ্লিকেশনটি কাজ না করবে এমন অবস্থা হতে পারে - ব্যন্ত-নেটিভ লাইব্রেরিস মুছে ফেলুন - অনুসুশিত নয় এমন সিপ-আর্কিত লাইব্রেরিসগুলি প্যাচড অ্যাপ থেকে মুছে ফেলুন + যন্ত্রের আর্কিটেকচারের জন্য অপ্টিমাইজ করুন + "সাধারণ APK-এর জন্য প্যাচিং করার পরে, সমর্থিত নয় এমন আর্কিটেকচারের জন্য নেটিভ লাইব্রেরিগুলি স্ট্রিপ করা হয়।" + + বাইটকোর্ট প্রক্রিয়াকরণ মোড + প্যাচিং করার সময় বাইটকোর্ট কীভাবে প্রক্রিয়া করা হবে তা নিয়ন্ত্রণ করে। এটি প্যাচিং গতি, মেমরি ব্যবহার এবং আউটপুট APK আকারের উপর প্রভাব ফেলে + দ্রুত (সুপারিশ করা) + দ্রুত + ধীর প্যাচিং এবং কম মেমরি ব্যবহার, তবে APK-এর আকার বড় হবে। কম RAM বা পুরোনো ডিভাইসের জন্য এটি উপযুক্ত + সম্পূর্ণ + ধীর প্যাচিং, আগের সংস্করণের আচরণ প্রতিলিপি করে। দ্রুত মোড সমস্যা সৃষ্টি করলে এটি ব্যবহার করুন গিটহাব PAT পুল অনুরোধ উৎসের জন্য প্রয়োজনীয় @@ -500,13 +520,7 @@ morphe_adaptive_monochrome_custom.xml ইনস্টল করা শিজুকু ভার্সন সমর্থিত নয় Shizuku খুলুন ইনস্টলেশন ব্যর্থ হয়েছে. আবার চেষ্টা করুন বা ভিন্ন ইনস্টলার ব্যবহার করুন - এন্ড্রয়েড দ্বারা ইনস্টলেশন ব্লক করা হয়েছে. প্লে প্রোটেক্ট বা সিকিউরিটি সেটিংস চেক করুন এই প্যাকেজ নামের সাথে আরেকটি অ্যাপ ইতিমধ্যে ইনস্টল করা আছে. এটিকে বাতিল করুন এবং চলে যান - এই ডিভাইস বা অ্যান্ড্রয়েড ভার্সনের সাথে এপিকে সমর্থিত নয় - এপিকটি অকার্যকর বা ক্ষতিগ্রস্ত হয়েছে - ইনস্টলেশন জন্য স্টোরেজ স্পেস पर्यাপ্ত নেই - ইনস্টলেশন টাইমআউট হয়েছে. আবার চেষ্টা করুন - ইনস্টলেশন বাতিল করা হয়েছে %1$s খোলা হচ্ছে… %1$s ইনস্টলেশন কনফার্ম করেননি. অন্যান্য অ্যাপ চেক করুন এবং আবার চেষ্টা করুন %1$s একটি সফল ইনস্টলেশন রিপোর্ট করেছে @@ -539,6 +553,7 @@ morphe_adaptive_monochrome_custom.xml কাস্টম কী-স্টোর আমদানি করুন কী-স্টোর নাম (এলিয়াস) আমদানি করুন + কী-স্টোর ফর্ম্যাট কী-স্টোর নাম (এলিয়াস) ভুলে কী-স্টোর নাম (এলিয়াস) ভুলে কী-স্টোর আমদানি ব্যর্থ হয়েছে @@ -546,6 +561,7 @@ morphe_adaptive_monochrome_custom.xml বর্তমান কী-স্টোর এক্সপোর্ট করুন এক্সপোর্ট করার জন্য কী-স্টোর পাওয়া যায়নি কী-স্টোর এক্সপোর্ট হয়েছে + কীস্টোর এক্সপোর্ট করা যায়নি পাসওয়ার্ড দেখান পাসওয়ার্ড লুকান @@ -674,7 +690,8 @@ morphe_adaptive_monochrome_custom.xml অনুমতি দিন বিস্তারিত গোপন রাখুন - ডিফল্ট + লুকানো + আনলুকানো লগসমূহ \"সোর্স\" ইনস্টল হচ্ছে… diff --git a/app/src/main/res/values-bs-rBA/strings.xml b/app/src/main/res/values-bs-rBA/strings.xml index 180ea1689..48f97ef60 100644 --- a/app/src/main/res/values-bs-rBA/strings.xml +++ b/app/src/main/res/values-bs-rBA/strings.xml @@ -39,10 +39,12 @@ + + diff --git a/app/src/main/res/values-ca-rES/strings.xml b/app/src/main/res/values-ca-rES/strings.xml index 25cce10f2..63e551c68 100644 --- a/app/src/main/res/values-ca-rES/strings.xml +++ b/app/src/main/res/values-ca-rES/strings.xml @@ -86,10 +86,12 @@ + + diff --git a/app/src/main/res/values-ckb-rIR/strings.xml b/app/src/main/res/values-ckb-rIR/strings.xml index 847042548..bdca52a11 100644 --- a/app/src/main/res/values-ckb-rIR/strings.xml +++ b/app/src/main/res/values-ckb-rIR/strings.xml @@ -49,10 +49,12 @@ + + diff --git a/app/src/main/res/values-cs-rCZ/plurals.xml b/app/src/main/res/values-cs-rCZ/plurals.xml index 173dc40bc..360197acd 100644 --- a/app/src/main/res/values-cs-rCZ/plurals.xml +++ b/app/src/main/res/values-cs-rCZ/plurals.xml @@ -1,22 +1,22 @@ - %s záplat - %s záplat + %s záplata + %s záplaty %s záplat %s záplat - %s nastavení záplaty - %s nastavení záplaty - %s nastavení záplaty - %s nastavení záplaty + %s možnost záplaty + %s možnosti záplaty + %s možností záplaty + %s možností záplaty - %s zdroj záplaty - %s zdroje záplaty - %s zdroje záplaty - %s zdrojů záplaty + %s zdroj + %s zdroje + %s zdrojů + %s zdrojů %s balíček @@ -25,10 +25,10 @@ %s balíčků - Vykonat %s záplatu - Vykonat %s záplaty - Vykonat %s záplat - Vykonat %s opatření + Aplikována %s záplata + Aplikovány %s záplaty + Aplikováno %s záplat + Aplikováno %s záplat %s APK soubor @@ -49,9 +49,21 @@ %s zdrojů nainstalováno - %s záplat vybráno - %s záplat vybráno + %s záplata vybrána + %s záplaty vybrány %s záplat vybráno %s záplat vybráno + + Zobrazit %s aplikaci + Zobrazit %s aplikace + Zobrazit %s aplikací + Zobrazit %s aplikací + + + %s skrytá aplikace + %s skryté aplikace + %s skrytých aplikací + %s skrytých aplikací + diff --git a/app/src/main/res/values-cs-rCZ/strings.xml b/app/src/main/res/values-cs-rCZ/strings.xml index aa6e237c6..1a9d87cbe 100644 --- a/app/src/main/res/values-cs-rCZ/strings.xml +++ b/app/src/main/res/values-cs-rCZ/strings.xml @@ -5,12 +5,12 @@ Ještě nezáplatováno Žádné záplaty k dispozici Skrýt tuto aplikaci? - Tato aplikace bude skryta z obrazovky. Můžete ji později obnovit pomocí tlačítka \'%1$s\' na dně seznamu. - Tato aplikace bude skryta. + Tato aplikace bude skryta z obrazovky. Můžete ji později obnovit pomocí tlačítka \'%1$s\' na konci seznamu + Tato aplikace bude skryta Zobrazit skryté aplikace Žádné skryté aplikace Skryté aplikace - Ťukněte pro zobrazení + Klepněte pro zobrazení Chyba Androidu 11 Oprávnění k instalaci aplikací musí být uděleno předem, abyste se vyhnuli chybě v systému Android 11, která negativně ovlivňuje uživatelský zážitek Přidat zdroj @@ -29,15 +29,17 @@ Vaše zdroje jsou aktuální Zdroje se načítají, počkejte prosím… Hledat aplikace - Neznalezly se žádné aplikace + Nenalezly se žádné aplikace Přidejte zdroj záplaty nebo povolte stávající v %s Žádné aplikace neodpovídají \"%1$s\" + Všechny aplikace jsou skryté + Všechny aplikace jste schovali z domovské obrazovky Kterou aplikaci chcete záplatovat? Připraveni učinit aplikace znovu legendárními? - Připoutejte se, čas záplat začíná + Připoutejte se, je čas na záplatování Další den, další mistrovské dílo bez reklam @@ -45,76 +47,80 @@ Pojďme zkrotit vaše aplikace - Dnešní předpověď: 100% pravděpodobnost záplatování + Dnešní předpověď: 100% šance na záplatování - Připraveni změnit podprůměrné na skvělé? + Připraveni změnit nudu v pecku? - Počínaje dneškem zachraňujeme aplikace před sebou samými + Už od dneška zachraňujeme aplikace před nimi samotnými - Poháněno kávou a dobrými záplatami + Běží na kofeinu a dobrých záplatách Zdroje záplat Otevřít v prohlížeči Nepodařilo se otevřít URL - Použít předběžné patche - Získejte předběžný přístup k novým experimentálním verzím patche. Pro získání předběžných patche zapněte předběžný přepínač u každé zdrojové lokace odděleně. + Použít předběžné záplaty + Získejte předběžný přístup k novým experimentálním verzím záplat Použít experimentální verze aplikace - Použít předběžné verze cílových experimentálních aplikací, pokud jsou dostupné + Záplatovat experimentální verze aplikací, jsou-li k dispozici Tento zdroj byl již přidán Zobrazit název Přejmenovat zdroj záplat Zdroj záplat s tímto názvem již existuje - Nebylo možné aktualizovat tento zdroj ovladače - Koukni na jakoukoli verzi - Koukni na jakýkoliv balíček + Nepodařilo se aktualizovat tento zdroj záplat + Jakákoliv verze + Jakýkoliv balíček Přidat zdroj záplat Smazat zdroj \"%s\"? Tato akce je nevratná Vzdálený - URL zdroje + URL zdroj Příklady: Lokální - Vybrat soubor zdroje záplat - Vyberte .mpp soubor zdroje záplat z úložiště + Vybrat soubor se záplaty + Vyberte .mpp soubor se záplaty z uložiště Změnit soubor Předinstalované Aktualizace úspěšná Nejsou dostupné žádné aktualizace Selhalo stahování zdrojů: %s Nepodařilo se stáhnout \'%1$s\': %2$s - Nebylo možné aktualizovat \'%1$s\': soubor JSON s patchem nebyl nalezen na nakonfigurované URL - Odborník - Povolte Expert režim v Nastavení, abyste mohli tento záplát ručně zahrnout + Nebylo možné aktualizovat \'%1$s\': JSON soubor záplaty nebyl nalezen na nakonfigurované URL + Expert + Povolte Expert režim v Nastavení, abyste mohli tuto záplatu ručně přidat - Potřebujete pomoc při hledání původního neopraveného APK? - Pro opravu <b>%s</b> je potrben neopravený APK verzí: - Pro opravu <b>%s</b> je potřebná neopravená APK verze: + Potřebujete pomoc při hledání původního nezáplatovaného APK? + Pro záplatu <b>%s</b> je potřeba nezáplatovaná APK verze: + Pro záplatu <b>%s</b> je potřeba některá nezáplatovaná APK z verzí: Doporučené - Neupravený + Nezáplatované + Nekompatibilní Ano, pomozte mi najít APK - Ne, už mám APK. + Ne, už mám APK Použít uložené APK (v%s) - Není uloženo žádné APK. Pro patchování bude třeba vybrat soubor APK znovu. + Není uložené žádné APK. Pro záplatování bude potřeba vybrat soubor APK znovu + Vyžaduje Android %1$s+ + Není podporováno na tomto zařízení + <b>%1$s</b> nemá podporované verze pro Android %2$d (API %3$d). Všechny deklarované verze vyžadují vyšší verzi Androidu Stáhnout původní APK Postup: - Stiskněte tlačítko \'%1$s\' níže. - Sjeďte na webové stránce dolů a stiskněte - Stiskněte tlačítko stažení na webových stránkách, ne zde 😊 + Stiskněte tlačítko \'%1$s\' níže + Na webové stránce sjeďte dolů a stiskněte + Na webové stránce stiskněte tlačítko stažení, ne zde 😊 Počkejte, než se stahování dokončí, stažené APK <b>NEinstalujte</b> Počkejte, než se stahování dokončí, stažené APK <b>poté nainstalujte</b> - Po dokončení stažení se vracíte do Morphe - Po instalaci původního APK se vracíte do Morphe + Po dokončení stažení se vraťte do Morphe + Po dokončení instalace původního APK se vraťte do Morphe Pokračujte na APKMirror.com - Vyberte stažený soubor APK - Vyberte soubor <b>%1$s</b> APK, který právě stáhnul + Vyberte stažený APK soubor + Vyberte APK soubor <b>%1$s</b> , který jste právě stáhnul Otevřít APK soubor Doporučená verze Vybraná verze Vybrat APK - Klikněte na tlačítko níže pro výběr APK souboru libovolné aplikace na záplatování - Nepodařilo se přečíst soubor. Zkontrolujte, zda \'Externí úložiště\' není omezeno kvůli baterii + Klikněte na tlačítko níže pro výběr APK souboru libovolné aplikace k záplatování + Nepodařilo se přečíst soubor. Zkontrolujte, zda \'Externí úložiště\' není omezeno z důvodu úspory baterie Vybraný soubor není platný APK Nepodařilo se otevřít soubor. Zkuste to znovu @@ -123,31 +129,31 @@ Pokračovat v záplatování Vypnout vše Zapnout vše - Povolit doporučené opravy + Povolit doporučené záplaty Obnovit uloženou hodnotu Nebyly nalezeny žádné záplaty Univerzální záplaty Nový - Vybráno více zdrojů pro aktualizace - "Vybrali jste aktualizace z více zdrojů. To může způsobit problémy s kompatibilitou nebo neočekávané chování. + Vybráno více zdrojů záplat + "Vybrali jste záplaty z více zdrojů. To může způsobit problémy s kompatibilitou nebo neočekávané chování. Opravdu chcete pokračovat?" - Neověřená APK - APK, kterou jste vybrali pro <b>%s</b>, neodpovídá očekávanému certifikátu podpisu. Může být modifikována nebo pocházet z nedůvěryhodného zdroje - Tato APK nemusí být originální - Detekován rozdělený APK - "Tento soubor je balíček APK + Neověřené APK + APK, který jste vybrali pro <b>%s</b>, neodpovídá očekávanému podpisovému certifikátu. Může být upravený nebo z nedůvěryhodného zdroje + Toto APK nemusí být originální + Rozdělené APK detekováno + "Tento soubor je APK balíček (<b>APKM / APKS / XAPK</b>). -Pro nejlepší výsledky doporučuje tato aplikace opravit <b>úplný APK</b>" +Pro nejlepší výsledky doporučujeme záplatovat <b>úplný APK</b>" Vyberte jinou APK Experimentální Tato verze má experimentální podporu a může být nestabilní nebo neúplná Chcete experimentovat? 🧪 - Tato verze <b>%s</b> má brzkou experimentální podporu<br/><br/>🔧 Očekávejte zvláštní chování aplikace nebo neidentifikované chyby, jak jsou opravy vylepšovány pro tuto verzi aplikace + Tato verze <b>%s</b> má předběžnou experimentální podporu<br/><br/>🔧 Očekávejte neobvyklé chování aplikace nebo neidentifikované chyby, záplaty pro tuto verzi aplikace jsou ještě ve vývoji Nepodporovaná verze - Tato APK není doporučenou verzí aplikace. Pokračování může vést k nefunkčnosti aplikace + Tato APK není doporučená verze aplikace. Pokračování může vést k nefunkčnosti aplikace Pokračovat i tak Kompatibilní verze: Nekompatibilní @@ -155,13 +161,13 @@ Pro nejlepší výsledky doporučuje tato aplikace opravit <b>úplný APK& Vybrané APK není pro aplikaci, kterou jste chtěli záplatovat Očekávaný balíček: Vybraný balíček: - Patče mohou být zastaralé - Mobilní data jsou aktivní a aktualizace patčů jsou zakázány - Používání zastaralých patčů může způsobit poškození aplikace - Aktualizovat & patčit + Záplaty mohou být zastaralé + Mobilní data jsou zapnuta a aktualizace záplat je zakázáno + Záplatování pomocí zastaralých záplat může vést k nefunkčnosti aplikace + Aktualizovat &; záplatovat Nedostatek místa na disku - Dostupných je pouze %1$.2f GB volného místa. Oprava vyžaduje alespoň %2$.2f GB volného místa, aby fungovala správně - Pokračování může vést k chybě \"soubor nenalezen\" nebo poškozené výstupní APK + Dostupných je pouze %1$.2f GB volného místa. Záplatování vyžaduje alespoň %2$.2f GB volného místa, aby fungovalo správně + Pokračování může vést k chybě \"soubor nenalezen\" nebo poškodit výslednou APK Vlastní barva Barva ve formátu Hex @@ -173,20 +179,20 @@ Pro nejlepší výsledky doporučuje tato aplikace opravit <b>úplný APK& Tato hodnota již existuje Hodnota nesmí být prázdná Ještě jste nepřidali žádné hodnoty - %s: vyplňte všechny povinné možnosti + %s: vyplňte všechna povinná pole Vytvořit adaptivní ikonu Vyberte obrázek popředí, vyberte barvu pozadí, poté použijte posuvník nebo gesta přiblížení a oddálení pro úpravu náhledu - Náhled adaptivního ikonu - Pro nejlepší výsledek udržujte ikonu uvnitř pevného vnitřního kruhu + Náhled adaptivní ikony + Pro nejlepší výsledek nechte ikonu uvnitř vnitřního kruhu Vybrat obrázek Změnit obrázek Barva pozadí Resetovat pozici a měřítko - Zóna bezpečí (vždy viditelná) + Bezpečná zóna (vždy viditelná) Maskovací zóna (může být skryta) - Adaptivní ikona vytvořena úspěšně - Nepodařilo se vytvořit adaptivní ikonu + Adaptivní ikona byla úspěšně vytvořena + Adaptivní ikona se nepodařila vytvořit Vyberte, kde vytvořit složku \'%1$s/%2$s\' s generovanými ikonami Tlačítko oznámení je skryté Použijte posuvník pro změnu velikosti. Táhněte pro přesunutí @@ -221,6 +227,7 @@ Pro nejlepší výsledky doporučuje tato aplikace opravit <b>úplný APK& Je k dispozici novější verze záplaty. Repatchujte svou aplikaci, abyste získali nejnovější vylepšení a opravy. Aplikace byla odinstalována Tuto aplikaci bylo odinstalováno mimo Morphe. Zápalte ji znovu, abyste obnovili funkčnost + Velikost APK Režim root vyžaduje, aby před záplatováním byl na vašem zařízení nainstalován původní APK Záplata s podporou GmsCore je v root režimu vyloučena @@ -308,16 +315,13 @@ Pro nejlepší výsledky doporučuje tato aplikace opravit <b>úplný APK& Selhalo aplikování %s Zastaralá aplikace uložena pro později Chyba při ukládání zastaralé aplikace - Stáhnout soubor APK Provedení je vyžadováno pro pokračování v tomto rozšíření Proces patcher skončil s kódem %1$s Úspěšně nainstalováno Selhání instalace aplikace: %s - Instalace nebyla dokončena. Zkontrolujte dialog systémového instalátoru a zkuste to znovu APK uložen Selhání při exportu upravené aplikace Odstraněna uložená upravená aplikace - Odstraněna uložená kopie Instalujte aplikaci před otevřením Název balíčku Selhalo připojení: %s @@ -337,6 +341,10 @@ Pro nejlepší výsledky doporučuje tato aplikace opravit <b>úplný APK& Mřížka Částice Žádné + Náhodné + Při spuštění + Denně + Ob tři dny Paralaxový efekt Plynulé posunování pozadí při naklánění zařízení @@ -353,6 +361,10 @@ Pro nejlepší výsledky doporučuje tato aplikace opravit <b>úplný APK& Aktuální jazyk Pro některé jazyky mohou chybět nebo být neúplné. Chcete přidat nový jazyk nebo vylepšit existující překlady? Navštivte %s. + + Domovská obrazovka + Pozdravné fráze + Zobrazit pozdravný vzkaz na domovské obrazovce Ikona aplikace Výchozí @@ -399,8 +411,17 @@ Pro nejlepší výsledky doporučuje tato aplikace opravit <b>úplný APK& Povolit pokročilé nastavení a možnosti personalizace pro Morphe Povolit Expert mód? Expert mód poskytuje více kontroly nad aplikací patchů, ale špatná konfigurace nastavení v expert módu může vést k nefunkční aplikaci - Odstranit nepoužívané nativní knihovny - Smazat nativní knihovny pro nepodporované architektury CPU ze záplatovaných aplikací + Optimalizovat pro architekturu zařízení + "Přeskočit split APK moduly pro nepodporované CPU architektury, jazykové oblasti a hustoty obrazovky během slučování. +Pro plain APK, odstraní nativní knihovny pro nepodporované architektury po opravování" + + Režim zpracování bajtkódu + Určuje, jak je bajtkód zpracováván během záplatování. Ovlivňuje rychlost záplatování, využití paměti a velikost výstupní APK + Rychlé (Doporučeno) + Rychle + Rychlejší opravy a nižší využití paměti, na úkor větší velikosti APK. Doporučeno pro zařízení s nízkou RAM nebo starší zařízení + Kompletní + Pomalé opravy, replikuje chování starších verzí. Používejte pouze v případě, že rychlý režim způsobuje problémy GitHub PAT Požadováno pro zdroje pull requestů @@ -510,13 +531,7 @@ Rozměry obrázků musí být následující: Použitá verze Shizuku není podporována Otevřít Shizuku Instalace selhala. Zkuste to znovu nebo přepněte na jiný instalátor - Instalace byla zablokována Androidem. Zkontrolujte Play Protect nebo nastavení bezpečnosti Jiná aplikace s tímto jménem balíku je již nainstalována. Odinstalujte ji před pokračováním - APK není kompatibilní s tímto zařízením nebo verzí Android - APK je neplatný nebo poškozený - Je nedostatek místa na disku pro instalaci - Instalace vypršela. Zkuste to znovu - Instalace byla zrušena Otevírání %1$s… %1$s nezahradil instalaci. Zkontrolujte jinou aplikaci a zkuste to znovu %1$s zaregistroval úspěšnou instalaci @@ -547,6 +562,7 @@ Rozměry obrázků musí být následující: Uživatelské jméno (alias) Heslo Importovat + Formát úložiště klíčů Neplatné přihlašovací údaje klíčového dílu Úložiště klíčů importováno Selhalo importování klíčového dílu @@ -554,6 +570,7 @@ Rozměry obrázků musí být následující: Exportovat současné úložiště klíčů Není k dispozici žádné úložiště klíčů k exportu Úložiště klíčů exportováno + Nepodařilo se exportovat keystore Zobrazit heslo Skrýt heslo @@ -682,7 +699,8 @@ Rozměry obrázků musí být následující: Povolit Detaily Skrýt - Výchozí + Skrytý + Zobrazit Protokoly Zdroje Instaluje se… diff --git a/app/src/main/res/values-da-rDK/plurals.xml b/app/src/main/res/values-da-rDK/plurals.xml index e4553dc5e..ce9c7897c 100644 --- a/app/src/main/res/values-da-rDK/plurals.xml +++ b/app/src/main/res/values-da-rDK/plurals.xml @@ -1,12 +1,12 @@ - %s stik - %s stikkeplads + %s patch + %s patches - %s valgmulighed - %s valgmuligheder + %s patch valgmulighed + %s patch valgmuligheder %s kilde @@ -17,8 +17,8 @@ %s pakker - Udfør %s patch - Udfør %s patches + Udført %s patch + Udført %s patches %s APK-fil @@ -36,4 +36,12 @@ %s patch valgt %s patches valgt + + Vis %s app + Vis %s apps + + + %s skjult app + %s skjulte apps + diff --git a/app/src/main/res/values-da-rDK/strings.xml b/app/src/main/res/values-da-rDK/strings.xml index 63286b470..b397bb111 100644 --- a/app/src/main/res/values-da-rDK/strings.xml +++ b/app/src/main/res/values-da-rDK/strings.xml @@ -1,7 +1,7 @@ - Andet + Andre apps Ikke patchet endnu Ingen patches tilgængelige Skjul denne app? @@ -14,7 +14,7 @@ Android 11-fejl App-installationsrettighed skal gives i forvejen for at undgå en fejl i Android 11-systemet, der vil have en negativ indflydelse på brugeroplevelsen Tilføj kilde - Vil du tilføje denne patch-kilde til Morphe? + Vil du tilføje denne patchkilde til Morphe? Tilføj kun kilder fra kilder, du stoler på Morphe-opdatering tilgængelig @@ -30,8 +30,10 @@ Kilder hentes, vent venligst… Søg apps Ingen apps tilgængelige - Tilføj en patch-kilde eller aktiver en eksisterende i %s + Tilføj en patchkilde eller aktiver en eksisterende i %s Ingen apps matcher \"%1$s\" + Alle apps er skjult + Du har skjult alle apps fra startskærmen Hvilken app vil du have til at blive opgraderet? @@ -53,24 +55,24 @@ Drevet af koffein og gode patches - Patch kilder + Patchkilder Åbn links i browser Kunne ikke åbne URL Brug pre-release patches - Få tidligt adgang til nye eksperimentelle patches versioner + Få tidligt adgang til nye eksperimentelle patch versioner Brug eksperimentelle app-versioner - Patche eksperimentelle app-targets, hvis de er tilgængelige + Patch eksperimentelle app-targets, hvis de er tilgængelige Denne kilde er allerede tilføjet Visningsnavn - Ændre navnet på patch kilde - En patch kilde med dette navn findes allerede + Omdøb navnet på patchkilde + En patchkilde med dette navn findes allerede Kunne ikke opdatere denne patchkilde Alle versioner Alle pakker - Tilføj patch kilde + Tilføj patchkilde Slet kilde \"%s\"? Dette kan ikke fortrydes - Remote + Fjernt Kilde-URL Eksempler: Lokalt @@ -80,21 +82,25 @@ Forhåndsinstalleret Opdatering vellykket Ingen opdatering tilgængelig - Kunne ikke hente patch kilder: %s - Kunne ikke hente patch kilder: %1$s: %2$s + Kunne ikke hente patchkilder: %s + Kunne ikke hente patchkilder: %1$s: %2$s Kunne ikke opdatere \'%1$s\': patchens JSON-fil blev ikke fundet på den konfigurerede URL Ekspert - Aktivér Ekspert-tilstand i Indstillinger for at inkludere denne patch manuelt + Aktivér Eksperttilstand i Indstillinger for at inkludere denne patch manuelt Har du brug for hjælp til at finde en original upatchet APK? For at patche <b>%s</b>, skal du have den upatchede APK-version: - For at patchere <b>%s</b>, skal du have et upatchtet APK af versioner: + For at patche <b>%s</b>, skal du have en upatchet APK af versioner: Anbefalet - Ubehandlet - Ja, hjælp mig finde et APK - Nej, jeg har allerede et APK + Upatchet + Inkompatibel + Ja, hjælp med at finde en APK + Nej, jeg har allerede en APK Brug gemt APK (v%s) - Ingen gemt APK. Patchning vil kræve at vælge APK-filen igen + Ingen gemt APK. Patchning kræver at du vælger APK-filen igen + Kræver Android %1$s+ + Ikke understøttet på denne enhed + <b>%1$s</b> har ingen understøttede versioner til Android %2$d (API %3$d). Alle erklærede versioner kræver en højere Android-version Download den originale APK Instruktioner: @@ -103,11 +109,11 @@ Tryk på download på hjemmesiden, og ikke her 😊 Vent på at downloaden er færdig, og <b>installer ikke</b> APK\'en Vent på at downloaden er færdig, og <b>installer APK\'en</b> - Når downloadet er færdigt, vend tilbage til Morphe + Når downloaden er færdigt, vend tilbage til Morphe Når det originale APK er installeret, vend tilbage til Morphe Fortsæt til APKMirror.com - Vælg det downloadede APK + Vælg den downloadede APK Vælg det <b>%1$s</b> APK-fil du lige har downloadet Åbn APK-fil Anbefalet version @@ -116,24 +122,24 @@ Tryk på knappen nedenfor for at vælge en APK-fil for en hvilket som helst app for at patche Kunne ikke læse filen. Tjek at \'Ekstern lagerplads\' ikke er batteribegrænset Den valgte fil er ikke en gyldig APK - Mislykkedes at åbne filen. Prøv igen + Kunne ikke åbne filen. Prøv igen - Expert-modus + Experttilstand Søg patches Gå videre til patching Deaktiver alle Aktiver alle - Aktivér anbefalede rettelser + Aktivér anbefalede patches Gendan gemt valg Ingen patches fundet Universelle patches Ny - Flere patch-kilder valgt - "Du har valgt patches fra flere patch-kilder. Dette kan forårsage kompatibilitetsproblemer eller uventet adfærd. + Flere patchkilder valgt + "Du har valgt patches fra flere patchkilder. Dette kan forårsage kompatibilitetsproblemer eller uventet adfærd. Er du sikker på, at du vil fortsætte?" - Ubekræftet APK + Uverificeret APK Den APK du valgte for <b>%s</b>, stemmer ikke overens med det forventede signeringscertifikat. Den kan være ændret eller fra en utroværdig kilde Denne APK er muligvis ikke den originale Opdelt APK opdaget @@ -143,7 +149,7 @@ Er du sikker på, at du vil fortsætte?" For de bedste resultater anbefaler denne app at patche en <b>fuld APK</b>" Vælg en anden APK Eksperimentel - Denne version har eksperimentel support og kan være ustabil eller ufærdig + Denne version har eksperimentel understøttelse og kan være ustabil eller ufærdig Vil du eksperimentere? 🧪 Denne version af <b>%s</b> har tidlig eksperimentel understøttelse<br/><br/>🔧 Forvent mærkelig app-adfærd eller uidentificerede fejl, mens patchesene bliver finpudset til denne appversion Ugyldig version @@ -152,11 +158,11 @@ For de bedste resultater anbefaler denne app at patche en <b>fuld APK</ Kompatible versioner: Ikke understøttet Forkert pakke valgt - Den valgte APK er ikke for den app, du tilsigtede at patche + Den valgte APK matcher ikke den app, du tilsigtede at patche Forventet pakke: Valgt pakke: Patches kan være forældede - Mobildata er aktiv og patch-opdateringer er deaktiveret + Mobildata er aktiv og patchopdateringer er deaktiveret At patche med forældede patches kan resultere i en defekt app Opdater & patch Lav diskplads @@ -188,52 +194,53 @@ For de bedste resultater anbefaler denne app at patche en <b>fuld APK</ Adaptiv ikon oprettet med succes Kunne ikke oprette adaptiv ikon Vælg hvor du vil oprette \'%1$s/%2$s\' mappe med genererede ikonfiler - Notifikations ikonforhåndsvisning + Notifikationsikon forhåndsvisning Brug skyderen til at ændre størrelsen. Træk for at flytte - Opret tilpasset header + Opret tilpasset sidehoved Vælg billeder for lys og mørketemaer, derefter juster med pinch-to-zoom bevægelser - Lys tema overskrift - Mørke tema overskrift + Lys tema sidehoved + Mørk tema sidehoved Intet billede valgt - Overskrift oprettet med succes - Kunne ikke oprette overskrift - Vælg hvor du vil oprette \'%1$s/%2$s\' mappe med genererede headerfiler + Sidehoved oprettet med succes + Kunne ikke oprette sidehoved + Vælg hvor du vil oprette \'%1$s/%2$s\' mappe med genererede sidehovedfiler Er du sikker på at du vil fjerne denne app? Dette vil permanent slette: Databaseindtastning Patchet APK-fil Original APK-fil - Din patch-valg og indstillinger vil blive bevaret til fremtidig patching + Dine patchvalg og indstillinger vil blive bevaret til fremtidig patching Patches Unavngivet Kunne ikke importere patches: %s Original APK ikke fundet. Kan ikke genpatche denne app Originalt pakkenavn Anvendte patches - Patch kilde brugt + Patchkilde brugt Installationstype - Brukerdefinert installer + Brugerdefineret installer Systeminstaller - Reparert - Patchoppdatering tilgjengelig - En nyere version af patches er tilgængelig. Repatch din app for at få de seneste forbedringer og fikser + Patchet + Patchopdatering tilgængelig + En nyere version af patches er tilgængelig. Repatch din app for at få de seneste forbedringer og rettelser Appen blev fjernet - Denne app blev fjernet udenfor Morphe. Repatch den for at genoprette funktionalitet + Denne app blev fjernet udenfor Morphe. Repatch den for at gendanne funktionalitet + APK-størrelse - Root-modus kræver, at original APK er installeret på dit enhed før patching + Root-tilstand kræver at original APK er installeret på din enhed før patching GmsCore support patch er udelukket i root-tilstand Vælg installationsmetode - Din enhed har root-adgang. Vælg, hvordan du vil installere den patcherede app + Din enhed har root-adgang. Vælg hvordan du vil installere den patchede app Root Mount - Mount patcheret APK over den originale app. Ingen GmsCore nødvendig + Mount patchet APK over den originale app. Ingen GmsCore nødvendig Standard Install Installer som en separat app med GmsCore support. Vælg installer (System installer, Shizuku, etc.) efter patching Installationsmetoden bestemmer, hvilke patches der er inkluderet og kan ikke ændres efter patching Fejl kopieret til udklipsholder - Fejlprotokol + Fejllog Tekniske oplysninger App-oplysninger Dette trin kan tage et stykke tid. Vent venligst... @@ -252,9 +259,9 @@ For de bedste resultater anbefaler denne app at patche en <b>fuld APK</ Tryk på knappen nedenfor for at installere Tryk på knappen nedenfor for at montere Kan ikke tilgå lagringssti - En eller flere patch-muligheder peger på stier, som Morphe ikke kan læse. Giv storage-adgang og tryk på knappen, så vil patching starte automatisk - En eller flere patch-valg peger på stier, som ikke kan tilgås. Opdater stierne i patch-valg og prøv igen - Tillad lagringstilgang + En eller flere patchtilvalg peger på stier, som Morphe ikke kan læse. Giv adgang til lager og tryk på knappen, så vil patching starte automatisk + En eller flere patchtilvalg peger på stier, som ikke kan tilgås. Opdater stierne i patchtilvalg og prøv igen + Tillad lagringstilladelse Lagringstilladelse blev afvist. Giv tilladelse til at bruge denne sti, eller flyt filerne til appens private mappe Åbn lagerindstillinger Tilladelse afvist @@ -288,7 +295,7 @@ For de bedste resultater anbefaler denne app at patche en <b>fuld APK</ Store patches er ventetiden værd… ⏳ Pudser pixels, indtil de skinner… 💎 Bliv venligst siddende, indtil kaptajnen slukker for \"Udbedring i gang\" skiltet... 💺✈️ - Laver din patchede app, varm og frisk… 🍳 + Tilbereder din patchede app, varm og frisk… 🍳 Forhandler høfligt med bytekode… 🤝 @@ -308,16 +315,13 @@ For de bedste resultater anbefaler denne app at patche en <b>fuld APK</ Kunne ikke anvende %s Patchet app gemt til senere Kunne ikke gemme patchet app - Download APK-fil Brugerinteraktion er nødvendig for at fortsætte med dette plugin Patcher-processen lukkede med koden %1$s Installeret succesfuldt Kunne ikke installere app: %s - Installationen blev ikke fuldført. Tjek systeminstalleringsdialogen og prøv igen APK gemt Kunne ikke eksportere patchet app Gemt patchet app fjernet - Gemt kopi fjernet Installer appen før du åbner den Pakkenavn Kunne ikke montere: %s @@ -337,6 +341,10 @@ For de bedste resultater anbefaler denne app at patche en <b>fuld APK</ Linjer Partikler Ingen + Tilfældig + Ved opstart + Dagligt + Hver 3. dag Parallakse-effekt Flydende baggrundsbevægelse ved tiltning af enheden @@ -347,12 +355,16 @@ For de bedste resultater anbefaler denne app at patche en <b>fuld APK</ Material You Accentfarve Ægte sort - Brug ægte sorte baggrunde + Brug ægte sort i baggrunde App-sprog Nuværende sprog Oversættelser for visse sprog kan mangle eller være ufærdige For at oversætte nye sprog eller forbedre eksisterende oversættelser, gå til %s + + Startskærm + Hilsenfraser + Vis en hilsenbesked på startskærmen Appikon Standard @@ -367,9 +379,9 @@ For de bedste resultater anbefaler denne app at patche en <b>fuld APK</ Opdateringer Brug pre-release Morphe - Modtag tidlig adgang til nye Morphe-funktioner. For at få pre-release patches, skal du aktivere pre-release-knappen i hver patch-kilde separat - Opdater over mobilnet - Tillad Morphe og patch-opdateringer at downloade over mobildata + Modtag tidlig adgang til nye Morphe-funktioner. For at få pre-release patches, skal du aktivere pre-release-knappen i hver patchkilde separat + Opdater over mobildata + Tillad Morphe og patchopdateringer downloader over mobildata Baggrundsopdateringsmeddelelser Tjek periodisk for opdateringer i baggrunden og informér når de er tilgængelige. Modtag push-beskeder om nye udgivelser, selvom appen er lukket @@ -387,20 +399,29 @@ For de bedste resultater anbefaler denne app at patche en <b>fuld APK</ Version %1$s er klar til download Tryk for at åbne Morphe og opdatere Tillad notifikationer - Morphe har brug for notifikationsadgang for at advare dig, når opdateringer er tilgængelige i baggrunden + Morphe skal bruge notifikationstilladelse for at advare dig når opdateringer er tilgængelige i baggrunden Vil du gerne blive notificeret, når opdateringer er tilgængelige? Opdateringer af notifikationer - Notifikationer om nye Morphe- og patch-udgivelser + Notifikationer om nye Morphe- og patchudgivelser Patching Vises mens Morphe patcher en app i baggrunden Avancerede indstillinger Eksperttilstand - Aktiver komplicerede Morphe patch-indstillinger og tilpasningsmuligheder + Aktiver komplicerede Morphe patchindstillinger og tilpasningsmuligheder Aktiver Eksperttilstand? Eksperttilstand giver mere kontrol over hvordan patches bliver anvendt, men en forkert konfiguration af indstillinger i eksperttilstand kan resultere i en ikke-funktionel app - Fjern ubrugte native biblioteker - Slet ubrugte native biblioteker for ikke-understøttede CPU-arkitekturer fra patchet apps + Optimer for enhedens arkitektur + "Overspring split APK-moduler for understøttede CPU-arkitekturer, sprogområder og skærmdensiteter under sammensmeltning. +For almindelige APK'er fjernes native biblioteker for understøttede arkitekturer efter patching" + + Bytecodetilstand + Styrer, hvordan bytecode behandles under patching. Påvirker patching-hastighed, hukommelsesforbrug og output APK-størrelse + Hurtig (Anbefalet) + Hurtig + Hurtigere patching og lavere hukommelsesforbrug, på bekostning af en større APK. Anbefales til enheder med lav RAM eller ældre enheder + Fuld + Langsommere patching, genskaber ældre adfærd. Brug kun, hvis hurtig tilstand forårsager problemer GitHub PAT Nødvendigt for pull request-kilder @@ -415,7 +436,7 @@ For de bedste resultater anbefaler denne app at patche en <b>fuld APK</ Patchvalg Tema, branding og sidehovedindstillinger Ændring af patch indstillinger kræver re-patching af appen for at træde i kraft - I eksperttilstand vil patchvalg være tilgængelige under patchprocessen + I Eksperttilstand vil patchvalg være tilgængelige under patchprocessen App-temafarver Ændre baggrundsfarve @@ -465,21 +486,21 @@ Valgfrit indeholder stien et notifikationsikon i et af følgende formater: drawable-xxhdpi/morphe_notification_icon_custom.png (72x72 px) drawable-xxxhdpi/morphe_notification_icon_custom.png (96x96 px)" - Brugerdefineret headerlogo - Ændre headerlogo - "Mapp med bilder til bruk som brukerdefinert headerlogo. + Brugerdefineret sidehovedlogo + Ændre sidehovedlogo + "Mappe med billeder til brug som brugerdefineret sidehovedlogo. -Mappen må inneholde en eller flere av følgende mapper, avhengig av DPI på enheten: +Mappen skal indeholde en eller flere af følgende mapper, afhængigt af DPI på enheden: - drawable-hdpi - drawable-xhdpi - drawable-xxhdpi - drawable-xxxhdpi -Hver av mappene må inneholde alle følgende filer: +Hver af mapperne skal indeholde alle følgende filer: morphe_header_custom_light.png morphe_header_custom_dark.png -Bildeformateringen må være som følger: +Størrelsesformatet skal være som følger: - drawable-hdpi: 194x72 px - drawable-xhdpi: 258x96 px - drawable-xxhdpi: 387x144 px @@ -510,13 +531,7 @@ Bildeformateringen må være som følger: Den installerede Shizuku-version er ikke understøttet Åbn Shizuku Installationen mislykkedes. Prøv igen eller skift til en anden installer - Installationen blev blokeret af Android. Tjek Play Protect eller sikkerhedsindstillinger En anden app med dette pakkenavn er allerede installeret. Fjern den før du fortsætter - APK\'en er ikke kompatibel med denne enhed eller Android-version - APK\'en er ugyldig eller korrumperet - Der er ikke nok lagringsplads til installationen - Installationen tog for længe. Prøv igen - Installationen blev afbrudt Åbner %1$s… %1$s bekræftede ikke installationen. Tjek den anden app og prøv igen %1$s meldte om en succesfuld installation @@ -543,10 +558,11 @@ Bildeformateringen må være som følger: Importér keystore Importér en brugerdefineret keystore Indtast keystore legitimationsoplysninger - Du vil skulle indtaste keystore\'ens legitimationsoplysninger for at importere den + Du skal indtaste keystore\'ens legitimationsoplysninger for at importere den Brugernavn (Alias) Adgangskode Importér + Keystore format Forkerte keystore legitimationsoplysninger Keystone importeret Importering af Keystone mislykkedes @@ -554,6 +570,7 @@ Bildeformateringen må være som følger: Eksportér den nuværende keystore Ingen keystore tilgængelig til eksportering Keystore blev eksporteret + Fejl under eksport af nøglelager Vis adgangskode Skjul adgangskode @@ -567,11 +584,11 @@ Bildeformateringen må være som følger: Morphe-indstillinger eksporteret Fejlfinding - Eksportér fejlsøgningslogfiler + Eksportér fejlsøgningslogge Gem systemlog for fejlfinding Kunne ikke læse log (udgangskode %s) - Kunne ikke eksportere logfiler - Logfiler blev eksporteret + Kunne ikke eksportere logge + Logge blev eksporteret Administration af lager Samlet størrelse: %s @@ -588,28 +605,28 @@ Bildeformateringen må være som følger: Ingen lagrede originale APK-er Original APK slettet Slet original APK? - Slette original APK for %s? Du vil ikke kunne repatch uden at tilbyde en ny APK + Slet original APK for %s? Du vil ikke kunne repatche uden at tilbyde en ny APK - Patch-valg - Håndter de gemte patch-valg og indstillinger. Disse bevares efter app-afinstallation + Patchvalg + Håndter de gemte patchvalg og indstillinger. Disse bevares efter app-afinstallation Nulstil alle valg? Nulstil pakkevalg? Nulstil kildevalg? - Ingen gemte patch-valg + Ingen gemte patchvalg %1$s over %2$s %1$s i %2$s Kilde #%s Dette vil slette: - Dette vil permanent slette alle gemte patch-valg for alle pakker og kilder - Dette vil permanent slette alle gemte patch-valg for %s over alle kilder - Dette vil permanent slette gemte patch-valg for %1$s i kilde #%2$s + Dette vil permanent slette alle gemte patchvalg for alle pakker og kilder + Dette vil permanent slette alle gemte patchvalg for %s over alle kilder + Dette vil permanent slette gemte patchvalg for %1$s i kilde #%2$s Kunne ikke eksportere kilde-data Kilde-data eksporteret med succes Kunne ikke importere kilde-data Kilde-data importeret med succes - Patch-oplysninger + Patchoplysninger Valgte patches (%s) - Patch-valg (%s) + Patchvalg (%s) Ingen patches eller valg gemt Om @@ -658,8 +675,8 @@ Bildeformateringen må være som følger: Mindre Udvid Udvidet - Fold op - Redusert + Minimer + Minimeret Tilgængelig Fortsæt Version @@ -682,19 +699,20 @@ Bildeformateringen må være som følger: Tillad Detaljer Skjul - Standard - Logfiler + Skjult + Vis + Logge Kilder Installerer… Montering… Afmontere… Importerer… Importeret med succes - Se ændringslogfiler + Se ændringslogge Se de seneste ændringer i denne opdatering - Kunne ikke downloade ændringslogfil: %s + Kunne ikke downloade ændringslog: %s Prøv igen - Ingen ændringslogfil tilgængelig + Ingen ændringslog tilgængelig Ingen internetforbindelse tilgængelig En opdatering er tilgængelig Klar til at installere opdatering diff --git a/app/src/main/res/values-de-rDE/plurals.xml b/app/src/main/res/values-de-rDE/plurals.xml index a8ae95f76..a918a20ba 100644 --- a/app/src/main/res/values-de-rDE/plurals.xml +++ b/app/src/main/res/values-de-rDE/plurals.xml @@ -17,8 +17,8 @@ %s Pakete - Führe %s Patch aus - Führe %s Patches aus + %s Patch angewendet + %s Patches angewendet %s APK-Datei @@ -36,4 +36,12 @@ %s Patch ausgewählt %s Patches ausgewählt + + %s App anzeigen + %s Apps anzeigen + + + %s ausgeblendete App + %s ausgeblendete Apps + diff --git a/app/src/main/res/values-de-rDE/strings.xml b/app/src/main/res/values-de-rDE/strings.xml index 7e030094e..08d77dead 100644 --- a/app/src/main/res/values-de-rDE/strings.xml +++ b/app/src/main/res/values-de-rDE/strings.xml @@ -32,6 +32,8 @@ Keine Apps verfügbar Eine Patch-Quelle hinzufügen oder eine vorhandene in %s aktivieren Keine Apps passen zu \"%1$s\" + Alle Apps sind ausgeblendet + Sie haben alle Apps vom Startbildschirm ausgeblendet Welche App möchten Sie patchen? @@ -91,10 +93,14 @@ Um <b>%s</b> zu patchen, benötigen Sie eine ungepatchte APK der Versionen: Empfohlen Ungepatcht + Inkompatibel Ja, hilf mir eine APK zu finden Nein, ich habe bereits eine APK Verwende die gespeicherte APK (v%s) Keine gespeicherte APK. Das Patching erfordert die erneute Auswahl der APK-Datei + Benötigt Android %1$s+ + Wird auf diesem Gerät nicht unterstützt + <b>%1$s</b> hat keine unterstützten Versionen für Android %2$d (API %3$d). Alle deklarierten Versionen erfordern eine höhere Android-Version Laden Sie die originale APK herunter Anleitung: @@ -221,6 +227,7 @@ Für optimale Ergebnisse empfiehlt diese App eine <b>vollständige APK< Eine neuere Version von Patches ist verfügbar. Repatchen Sie Ihre App, um die neuesten Verbesserungen und Fehlerbehebungen zu erhalten App wurde deinstalliert Diese App wurde außerhalb von Morphe deinstalliert. Repatchen Sie sie, um die Funktionalität wiederherzustellen + APK-Größe Root-Modus erfordert, dass die originale APK-Datei auf Ihrem Gerät installiert ist, bevor Sie mit dem Patchen beginnen GmsCore-Unterstützungspatch ist im Root-Modus ausgeschlossen @@ -308,16 +315,13 @@ Für optimale Ergebnisse empfiehlt diese App eine <b>vollständige APK< %s konnte nicht angewendet werden Gepatchte App für später gespeichert Speichern der gepatchten App fehlgeschlagen - APK-Datei herunterladen Benutzerinteraktion ist erforderlich, um dieses Plugin fortzusetzen Der Patcher-Prozess ist mit dem Code %1$s beendet worden Erfolgreich installiert Installation der App fehlgeschlagen: %s - Die Installation wurde nicht abgeschlossen. Überprüfen Sie den Systeminstallationsdialog und versuchen Sie es erneut APK gespeichert Export der gepatchten App fehlgeschlagen Gespeicherte gepatchte App entfernt - Gespeicherte Kopie entfernt Installieren Sie die App vor dem Öffnen Paketname Mounten fehlgeschlagen: %s @@ -337,6 +341,10 @@ Für optimale Ergebnisse empfiehlt diese App eine <b>vollständige APK< Gitter Partikel Keine + Zufällig + Beim Start + Täglich + Alle 3 Tage Parallax-Effekt Glatte Hintergrundverschiebung beim Neigen des Geräts @@ -353,6 +361,10 @@ Für optimale Ergebnisse empfiehlt diese App eine <b>vollständige APK< Aktuelle Sprache Für einige Sprachen können Übersetzungen fehlen oder unvollständig sein Um neue Sprachen zu übersetzen oder existierende Übersetzungen zu verbessern, besuchen Sie %s + + Startbildschirm + Begrüßungsformeln + Auf dem Startbildschirm eine Begrüßungsnachricht anzeigen App-Icon Standard @@ -399,8 +411,17 @@ Für optimale Ergebnisse empfiehlt diese App eine <b>vollständige APK< Aktiviere komplexe Morphe-Patch-Einstellungen und Anpassungsoptionen Experten-Modus aktivieren? Der Experten-Modus gewährt mehr Kontrolle darüber, wie Patches angewendet werden, kann aber bei falscher Konfiguration zu einer nicht funktionsfähigen App führen - Entferne nicht verwendete, native Bibliotheken - Lösche native Bibliotheken für nicht unterstützte CPU-Architekturen aus gepatchten Apps + Für die Gerätearchitektur optimieren + "Split-APK-Module für nicht unterstützte CPU-Architekturen, Regionaleinstellungen und Bildschirmdichten während des Zusammenführens auslassen. +Für einfache APKs werden nach dem Patchen native Bibliotheken für nicht unterstützte Architekturen entfernt" + + Bytecode-Verarbeitungsmodus + Steuert, wie Bytecode während des Patch-Vorgangs verarbeitet wird. Beeinflusst Patch-Geschwindigkeit, Speichernutzung und Größe der Ausgabedatei APK + Schnell (Empfohlen) + Schnell + Schnelleres Patchen und geringerer Speicherverbrauch, jedoch bei größerer APK-Größe. Für Geräte mit wenig RAM oder ältere Geräte empfohlen + Vollständig + Langsameres Patchen, repliziert das Legacy-Verhalten. Nur verwenden, wenn der Schnellmodus Probleme verursacht GitHub-PAT Erforderlich für Pull-Request-Quellen @@ -510,13 +531,7 @@ Die Bildgrößen müssen wie folgt sein: Die installierte Shizuku-Version wird nicht unterstützt Shizuku öffnen Die Installation ist fehlgeschlagen. Versuchen Sie es erneut oder wechslen Sie zu einem anderen Installer - Die Installation wurde von Android blockiert. Überprüfen Sie Play Protect oder die Sicherheitseinstellungen Eine andere App mit diesem Paketnamen ist bereits installiert. Deinstalliere Sie sie bevor Sie fortfahren - Die APK ist nicht mit diesem Gerät oder Android-Version kompatibel - Die APK ist ungültig oder beschädigt - Es ist nicht genug Speicherplatz für die Installation vorhanden - Die Installation hat ein Zeitlimit überschritten. Versuchen Sie es erneut - Die Installation wurde abgebrochen Öffne %1$s… %1$s hat die Installation nicht bestätigt. Überprüfen Sie die andere App und versuchen Sie es erneut %1$s meldete eine erfolgreiche Installation @@ -547,6 +562,7 @@ Die Bildgrößen müssen wie folgt sein: Benutzername (Alias) Passwort Importieren + Keystore-Format Falsche Keystore-Anmeldeinformationen Keystore importiert Fehler beim Importieren des Keystores @@ -554,6 +570,7 @@ Die Bildgrößen müssen wie folgt sein: Den aktuellen Keystore exportieren Kein Keystore zum Exportieren verfügbar Keystore exportiert + Exportieren des Keystores fehlgeschlagen Passwort anzeigen Passwort ausblenden @@ -682,7 +699,8 @@ Die Bildgrößen müssen wie folgt sein: Erlauben Details Verbergen - Standard + Ausgeblendet + Anzeigen Protokolle Quellen Wird installiert… diff --git a/app/src/main/res/values-el-rGR/plurals.xml b/app/src/main/res/values-el-rGR/plurals.xml index 651fbc48c..ca236e2a0 100644 --- a/app/src/main/res/values-el-rGR/plurals.xml +++ b/app/src/main/res/values-el-rGR/plurals.xml @@ -36,4 +36,12 @@ %s επιλεγμένη τροποποίηση %s επιλεγμένες τροποποιήσεις + + Εμφάνιση %s εφαρμογής + Εμφάνιση %s εφαρμογών + + + %s κρυφή εφαρμογή + %s κρυφές εφαρμογές + diff --git a/app/src/main/res/values-el-rGR/strings.xml b/app/src/main/res/values-el-rGR/strings.xml index 8d986dc2f..b90a405c1 100644 --- a/app/src/main/res/values-el-rGR/strings.xml +++ b/app/src/main/res/values-el-rGR/strings.xml @@ -32,6 +32,8 @@ Δεν υπάρχουν διαθέσιμες εφαρμογές Προσθέστε μια πηγή τροποποίησης ή ενεργοποιήστε ήδη μια υπάρχουσα στο %s Καμία εφαρμογή δεν ταιριάζει με το \"%1$s\" + Όλες οι εφαρμογές είναι κρυφές + Έχετε κρύψει όλες τις εφαρμογές από την αρχική οθόνη Ποια εφαρμογή θέλετε να τροποποιήσετε; @@ -91,10 +93,14 @@ Για την τροποποίηση του <b>%s</b>, χρειάζεστε τις μη τροποποιημένες εκδόσεις του APK: Προτεινόμενη Μη τροποποιημένο + Ασύμβατο Ναι, βοήθησέ με να βρω ένα APK Όχι, ήδη έχω ένα APK Χρήση αποθηκευμένου APK (v%s) Δεν υπάρχει αποθηκευμένο APK. Η τροποποίηση κώδικα θα απαιτήσει την επιλογή του αρχείου APK ξανά + Απαιτεί Android %1$s+ + Δεν υποστηρίζεται σε αυτή τη συσκευή + <b>%1$s</b> δεν έχει υποστηριζόμενες εκδόσεις για το Android %2$d (API %3$d). Όλες οι δηλωμένες εκδόσεις απαιτούν μια υψηλότερη έκδοση Android Κατεβάστε το πρωτότυπο APK Οδηγίες: @@ -221,6 +227,7 @@ Μια καινούρια έκδοση τροποποιήσεων είναι διαθέσιμη. Τροποποιήσετε ξανά την εφαρμογή για να λάβετε τις τελευταίες βελτιώσεις και διορθώσεις Η εφαρμογή έχει απεγκατασταθεί Η εφαρμογή αυτή απεγκαταστάθηκε εκτός του Morphe. Τροποποιήστε την ξανά για την επαναφορά λειτουργικότητά της + Μέγεθος APK Η λειτουργία root απαιτεί το πρωτότυπο APK να είναι εγκατεστημένο στη συσκευή πριν τροποποιηθεί Η υποστήριξη της τροποποίησης GmsCore εξαιρείται στη λειτουργία root @@ -308,16 +315,13 @@ Αποτυχία εφαρμογής %s Η τροποποιημένη εφαρμογή αποθηκεύτηκε για αργότερα Αποτυχία αποθήκευσης της τροποποιημένης εφαρμογής - Λήψη αρχείου APK Απαιτείται αλληλεπίδραση χρήστη για να συνεχίσετε με αυτό το πρόσθετο. Η διαδικασία του τροποποιητή σταμάτησε με κωδικό %1$s Επιτυχής εγκατάσταση Αποτυχία εγκατάστασης εφαρμογής: %s - Η εγκατάσταση δεν ολοκληρώθηκε. Ελέγξτε το παράθυρο διαλόγου του εγκαταστάτη συστήματος και προσπαθήστε ξανά Το APK αποθηκεύτηκε Η εξαγωγή της τροποποιημένης εφαρμογής απέτυχε Η αποθηκευμένη τροποποιημένη εφαρμογή αφαιρέθηκε - Το αποθηκευμένο αντίγραφο αφαιρέθηκε Εγκαταστήστε την εφαρμογή πριν την ανοίξετε Όνομα πακέτου Αδυναμία προσάρτησης: %s @@ -337,6 +341,10 @@ Στίγματα Σωματίδια Κανένα + Τυχαίο + Κατά την εκκίνηση + Καθημερινά + Κάθε 3 ημέρες Εφέ παράλλαξης Ομαλή μετατόπιση φόντου όταν αλλάξει κλίση η συσκευή @@ -353,6 +361,10 @@ Τρέχουσα γλώσσα Οι μεταφράσεις για ορισμένες γλώσσες μπορεί να λείπουν ή να μην είναι ολοκληρωμένες Για να μεταφράσετε νέες γλώσσες ή να βελτιώσετε τις υπάρχουσες μεταφράσεις, επισκεφτείτε %s + + Αρχική οθόνη + Φράσεις υποδοχής + Εμφάνιση μηνύματος υποδοχής στην αρχική οθόνη Εικονίδιο εφαρμογής Προεπιλεγμένο @@ -399,8 +411,17 @@ Ενεργοποιήστε περίπλοκες ρυθμίσεις τροποποιήσεων του Morphe και προσαρμοσμένες επιλογές Ενεργοποίηση της λειτουργίας εξειδικευμένου χρήστη; Η λειτουργία εξειδικευμένου χρήστη δίνει περισσότερο έλεγχο για την εφαρμογή των τροποποιήσεων, αλλά η λάθος ρύθμιση στη λειτουργία αυτή μπορεί να προκαλέσει τη δυσλειτουργικότητα της εφαρμογής - Εκκαθάριση μη χρησιμοποιούμενων εγγενών βιβλιοθηκών - Διαγραφή των εγγενών βιβλιοθηκών για ανυποστήρικτες αρχιτεκτονικές επεξεργαστών από τροποποιημένες εφαρμογές + Βελτιστοποίηση για αρχιτεκτονική συσκευής + "Παράλειψη split APK modules για μη υποστηριζόμενες αρχιτεκτονικές CPU, τοποθεσίες και πυκνότητες οθόνης κατά τη συγχώνευση. +Για απλά APK, αφαιρεί τις αρχικές βιβλιοθήκες για μη υποστηριζόμενες αρχιτεκτονικές μετά την επιδιόρθωση" + + Λειτουργία επεξεργασίας bytecode + Ελέγχει τον τρόπο επεξεργασίας του bytecode κατά τη διάρκεια της επιδιόρθωσης. Επηρεάζει την ταχύτητα επιδιόρθωσης, τη χρήση μνήμης και το μέγεθος του APK εξόδου + Γρήγορος (Συνιστάται) + Γρήγορα + Ταχύτερη επιδιόρθωση και χαμηλότερη χρήση μνήμης, με αντάλλαγμα ένα μεγαλύτερο APK. Συνιστάται για συσκευές με χαμηλή μνήμη RAM ή παλαιότερες + Πλήρες + Αργότερη επιδιόρθωση, αναπαράγει τη συμπεριφορά legacy. Χρησιμοποιήστε το μόνο εάν ο γρήγορος τρόπος προκαλεί προβλήματα GitHub PAT Απαιτείται για πηγές pull request @@ -510,13 +531,7 @@ morphe_header_custom_dark.png Η εγκατεστημένη έκδοση του Shizuku δεν υποστηρίζεται. Άνοιγμα Shizuku Η εγκατάσταση απέτυχε. Προσπαθήστε ξανά ή χρησιμοποιήστε διαφορετικό εγκαταστάτη. - Η εγκατάσταση αποκλείστηκε από το Android. Ελέγξτε το Play Protect ή τις ρυθμίσεις ασφαλείας. Άλλη εφαρμογή με το ίδιο όνομα πακέτου είναι ήδη εγκατεστημένη. Απεγκαταστήσετε την πριν συνεχίσετε. - Το APK δεν είναι συμβατό με αυτήν τη συσκευή ή έκδοση Android. - Το APK είναι λανθασμένο ή διεφθαρμένο. - Δεν υπάρχει αρκετός χώρος αποθήκευσης για την εγκατάσταση. - Η εγκατάσταση σταμάτησε λόγω λήξης χρονικού ορίου. Προσπαθήστε ξανά. - Η εγκατάσταση ακυρώθηκε. Άνοιγμα του %1$s… %1$s δεν επιβεβαίωσε την εγκατάσταση. Ελέγξτε την άλλη εφαρμογή και προσπαθήστε ξανά. %1$s αναφέρει επιτυχή εγκατάσταση. @@ -547,6 +562,7 @@ morphe_header_custom_dark.png Όνομα χρήστη(Ψευδώνυμο) Κωδικός Εισαγωγή + Μορφή καταστήματος κλειδιών Λανθασμένα διαπιστευτήρια αποθήκης κλειδιών Επιτυχής εισαγωγή αποθήκης κλειδιών Αποτυχία εισαγωγής αποθήκης κλειδιών @@ -554,6 +570,7 @@ morphe_header_custom_dark.png Εξαγωγή τρέχοντος αποθήκης κλειδιών Δεν υπάρχει διαθέσιμη αποθήκη κλειδιών για εξαγωγή Επιτυχής εξαγωγή της αποθήκης κλειδιών + Αποτυχία εξαγωγής αποθήκης κλειδιών Εμφάνιση κωδικού Απόκρυψη κωδικού @@ -682,7 +699,8 @@ morphe_header_custom_dark.png Επιτρέψτε Λεπτομέρειες Απόκρυψη - Προεπιλεγμένο + Κρυφό + Εμφάνιση Εγγραφές Πηγές Εγκατάσταση… diff --git a/app/src/main/res/values-es-rES/plurals.xml b/app/src/main/res/values-es-rES/plurals.xml index 75de01d5b..841bb6b9c 100644 --- a/app/src/main/res/values-es-rES/plurals.xml +++ b/app/src/main/res/values-es-rES/plurals.xml @@ -17,8 +17,8 @@ %s paquetes - Ejecutar %s parche - Ejecutar %s parches + %s parche ejecutado + %s parches ejecutados %s archivo APK @@ -36,4 +36,12 @@ %s parche seleccionado %s parches seleccionados + + Mostrar aplicación %s + Mostrar aplicaciones %s + + + %s aplicación oculta + %s aplicaciones ocultas + diff --git a/app/src/main/res/values-es-rES/strings.xml b/app/src/main/res/values-es-rES/strings.xml index bfa0b855a..3a1700cf2 100644 --- a/app/src/main/res/values-es-rES/strings.xml +++ b/app/src/main/res/values-es-rES/strings.xml @@ -32,6 +32,8 @@ No hay aplicaciones disponibles Añadir una fuente de parche o habilitar una existente en %s No se encontraron aplicaciones que coincidan con \"%1$s\" + Todas las aplicaciones están ocultas + Ha ocultado todas las aplicaciones de la pantalla de inicio ¿Qué aplicación quieres parchear? @@ -39,7 +41,7 @@ Abróchate el cinturón, es hora de parchear - Otro día, otra obra maestra bloqueada por anuncios + Otro día, otra obra maestra con anuncios bloqueados ¿Quieres ver anuncios? Yo tampoco @@ -91,10 +93,14 @@ Para parchear <b>%s</b>, necesitas un APK sin parchear de las versiones: Recomendado No parcheado + Incompatible Sí, ayúdame a encontrar un APK No, ya tengo un APK Usar APK guardado (v%s) No hay APK guardado. Para aplicar los parches será necesario seleccionar el archivo APK nuevamente. + Requiere Android %1$s o superior + No compatible en este dispositivo + <b>%1$s</b> no tiene versiones compatibles para Android %2$d (API %3$d). Todas las versiones declaradas requieren una versión de Android superior Descargar el APK original Instrucciones: @@ -221,6 +227,7 @@ Para obtener los mejores resultados, esta aplicación recomienda parchear un < Hay disponible una versión más reciente de los parches. Vuelve a parchear tu aplicación para obtener las últimas mejoras y correcciones La app fue desinstalada Esta app fue desinstalada fuera de Morphe. Vuelve a parchearla para restaurar la funcionalidad + Tamaño de la APK El modo raíz requiere que el APK original esté instalado en tu dispositivo antes de aplicar parches. El parche de soporte de GmsCore está excluido en el modo raíz @@ -308,16 +315,13 @@ Para obtener los mejores resultados, esta aplicación recomienda parchear un < Error al aplicar %s App parcheada guardada para más tarde Error al guardar la app parcheada - Descargar archivo APK Se requiere interacción del usuario para continuar con este complemento. El proceso del parcheador terminó con el código %1$s Instalado correctamente Error al instalar la aplicación: %s - La instalación no se completó. Revisa el diálogo del instalador del sistema y vuelve a intentarlo. APK guardado Error al exportar la aplicación parcheada Aplicación parcheada guardada eliminada - Copia guardada eliminada Instala la aplicación antes de abrirla. Nombre del paquete Error al montar: %s @@ -337,6 +341,10 @@ Para obtener los mejores resultados, esta aplicación recomienda parchear un < Cuadrícula Partículas Ninguna + Aleatorio + Al inicio + Diario + Cada 3 días Efecto de paralaje Desplazamiento suave del fondo al inclinar el dispositivo @@ -353,6 +361,10 @@ Para obtener los mejores resultados, esta aplicación recomienda parchear un < Idioma actual Las traducciones para algunos idiomas pueden faltar o estar incompletas Para traducir nuevos idiomas o mejorar las traducciones existentes, visite %s + + Pantalla de inicio + Frases de saludo + Mostrar un mensaje de saludo en la pantalla de inicio Icono de la aplicación Predeterminado @@ -399,8 +411,17 @@ Para obtener los mejores resultados, esta aplicación recomienda parchear un < Activa ajustes complejos de parcheo de Morphe y opciones de personalización ¿Activar el modo experto? El modo experto ofrece más control sobre cómo se aplican los parches, pero configurar incorrectamente los ajustes en modo experto puede resultar en una aplicación no funcional - Eliminar bibliotecas nativas no utilizadas - Eliminar bibliotecas nativas para arquitecturas de CPU no compatibles de las aplicaciones parcheadas + Optimizar para la arquitectura del dispositivo + "Omite los módulos APK divididos para arquitecturas de CPU, idiomas y densidades de pantalla no compatibles durante la fusión. +Para APK sencillos, elimina las bibliotecas nativas para arquitecturas no compatibles después de parchear" + + Modo de procesamiento de bytecode + Controla cómo se procesa el bytecode durante el parcheo. Afecta a la velocidad de parcheo, el uso de memoria y el tamaño del APK de salida + Rápido (Recomendado) + Rápido + Parcheo más rápido y menor uso de memoria, a costa de un APK más grande. Recomendado para dispositivos con poca memoria RAM o más antiguos. + Completo + Parcheo más lento, replica el comportamiento heredado. Utilizar solo si el modo rápido causa problemas Token de acceso personal de GitHub Requerido para fuentes de solicitud de extracción @@ -510,13 +531,7 @@ Las dimensiones de la imagen deben ser las siguientes: La versión de Shizuku instalada no es compatible. Abrir Shizuku La instalación falló. Vuelve a intentarlo o cambia a un instalador diferente. - La instalación fue bloqueada por Android. Revisa Play Protect o las configuraciones de seguridad. Otra aplicación con este nombre de paquete ya está instalada. Desinstálala antes de continuar. - El APK no es compatible con este dispositivo o versión de Android. - El APK es inválido o corrupto. - No hay suficiente espacio de almacenamiento para la instalación. - La instalación tardó demasiado. Vuelve a intentarlo. - La instalación se canceló. Abriendo %1$s… %1$s no confirmó la instalación. Revisa la otra aplicación y vuelve a intentarlo. %1$s informó una instalación exitosa. @@ -547,6 +562,7 @@ Las dimensiones de la imagen deben ser las siguientes: Nombre de usuario (Alias) Contraseña Importar + Formato de almacén de claves Credenciales de almacén de claves incorrectas Almacén de claves importado No se pudo importar el almacén de claves @@ -554,6 +570,7 @@ Las dimensiones de la imagen deben ser las siguientes: Exportar el almacén de claves actual No hay almacén de claves disponible para exportar Almacén de claves exportado + Error al exportar el almacén de claves Mostrar contraseña Ocultar contraseña @@ -682,7 +699,8 @@ Las dimensiones de la imagen deben ser las siguientes: Permitir Detalles Ocultar - Predeterminado + Oculto + Mostrar Registros Fuentes Instalando… diff --git a/app/src/main/res/values-et-rEE/strings.xml b/app/src/main/res/values-et-rEE/strings.xml index 180ea1689..48f97ef60 100644 --- a/app/src/main/res/values-et-rEE/strings.xml +++ b/app/src/main/res/values-et-rEE/strings.xml @@ -39,10 +39,12 @@ + + diff --git a/app/src/main/res/values-eu-rES/strings.xml b/app/src/main/res/values-eu-rES/strings.xml index 180ea1689..48f97ef60 100644 --- a/app/src/main/res/values-eu-rES/strings.xml +++ b/app/src/main/res/values-eu-rES/strings.xml @@ -39,10 +39,12 @@ + + diff --git a/app/src/main/res/values-fa-rIR/strings.xml b/app/src/main/res/values-fa-rIR/strings.xml index 695b31842..276a7ea7d 100644 --- a/app/src/main/res/values-fa-rIR/strings.xml +++ b/app/src/main/res/values-fa-rIR/strings.xml @@ -192,11 +192,13 @@ + حالت متخصص + diff --git a/app/src/main/res/values-fi-rFI/plurals.xml b/app/src/main/res/values-fi-rFI/plurals.xml index fc6dffda0..c44751de9 100644 --- a/app/src/main/res/values-fi-rFI/plurals.xml +++ b/app/src/main/res/values-fi-rFI/plurals.xml @@ -17,8 +17,8 @@ %s paketteja - Suorita %s paikkaus - Suorita %s korjausa + Suoritettiin %s paikka + Suoritettiin %s paikkausta %s APK-tiedosto @@ -36,4 +36,12 @@ %s paikkaus valittu %s korjausa valittu + + Näytä %s sovellus + Näytä %s sovellusta + + + %s piilotettu sovellus + %s piilotettua sovellusta + diff --git a/app/src/main/res/values-fi-rFI/strings.xml b/app/src/main/res/values-fi-rFI/strings.xml index e24576711..21341c245 100644 --- a/app/src/main/res/values-fi-rFI/strings.xml +++ b/app/src/main/res/values-fi-rFI/strings.xml @@ -32,6 +32,8 @@ Sovelluksia ei saatavilla Lisää päivityslähde tai ota olemassa oleva käyttöön %s Yhtään sovellusta ei löytynyt hakusanalla \"%1$s\" + Kaikki sovellukset on piilotettu + Olet piilottanut kaikki sovellukset aloitusnäytöltä Minkä sovelluksen haluat korjata? @@ -91,10 +93,14 @@ Paikataksesi <b>%s</b> tarvitset alkuperäisen, paikkaamattoman APK:n versioista: Suositeltu Ei korjattuja + Yhteensopimaton Kyllä, auta minua etsimään APK Ei, minulla on jo APK Käytä tallennettua APK:tä (v%s) Ei tallennettua APK:tä. Patching vaatii APK-tiedoston uudelleenvalinnan + Vaatii Android %1$s+ + Ei tueta tällä laitteella + <b>%1$s</b> ei sisällä tuettuja versioita Androidille %2$d (API %3$d). Kaikki ilmoitetut versiot vaativat korkeamman Android-version Lataa alkuperäinen APK Ohjeet: @@ -220,6 +226,7 @@ Parhaan tuloksen saavuttamiseksi sovellus suosittelee kokonaisen <b>APK-ti Uudempi korjausversio on saatavilla. Korjaa sovelluksesi uudelleen saadaksesi uusimmat parannukset ja korjaukset Sovellus poistettiin Tämä sovellus poistettiin Morphen ulkopuolelta. Pätkä se uudelleen palauttamaan toiminnallisuuden + APK:n koko Juurtimemoodi vaatii alkuperäisen APK-tiedoston asennuksen laitteellesi ennen korjausta. GmsCore-tukikorjaus on poissuljettu juuritilassa @@ -307,16 +314,13 @@ Parhaan tuloksen saavuttamiseksi sovellus suosittelee kokonaisen <b>APK-ti Virhe %s:n suorittamisessa Muokattu sovellus tallennettu myöhempää käyttöä varten Muokatun sovelluksen tallennus epäonnistui - Lataa APK-tiedosto Tätä liitännöä varten tarvitaan käyttäjän interaktiivisuutta. Patcher-prosessi poistui koodilla %1$s Asennettu onnistuneesti Sovelluksen asentaminen epäonnistui: %s - Asennus ei päätytty. Tarkista järjestelmän asennusdialogi ja yritä uudelleen. APK tallennettu Muokattujen sovellusten vieminen epäonnistui Tallennettu sovellus poistettu - Tallennuskopio poistettu Asenna sovellus ennen avaamista. Paketin nimi Liitoksen muodostaminen epäonnistui: %s @@ -336,6 +340,10 @@ Parhaan tuloksen saavuttamiseksi sovellus suosittelee kokonaisen <b>APK-ti Ruudukko Hiukkaset Muokka appin ulkon + Satunnainen + Käynnistyksen yhteydessä + Päivittäin + Joka 3. päivä Parallaksi-efekti Smooth background shifting when tilting the device @@ -352,6 +360,10 @@ Parhaan tuloksen saavuttamiseksi sovellus suosittelee kokonaisen <b>APK-ti Nykyinen kieli Käännyt käännöt ovat puuttomia tai epätäydellisiä Muokkaa uusia kieliä tai parantaa vanhoja käännöitä, käytti sivustamme: %s + + Etusivu + Tervehdysviestit + Näytä tervehdysviesti etusivulla Sovelluskuvake Oletus @@ -398,8 +410,16 @@ Parhaan tuloksen saavuttamiseksi sovellus suosittelee kokonaisen <b>APK-ti Ota käyttöön Morphe-päivityksen asetukset ja mukautusvaihtoehdot Ota Expert-moodi käyttöön? Expert-moodi antaa enemmän valtaa päivitysten sovittamisesta, mutta väärin asetettu expert-moodi voi johtaa sovelluksen toimimattomuuteen - Poista käyttämättömät natiivikirjastot - Poista tukemattomien suoritinarkkitehtuurien natiivikirjastot paikatuista sovelluksista + Optimoi laitteen arkkitehtuuri + "Ohita jaetut APK-moduulit tuettujen CPU-arkkitehtuurien, kieliasetusten ja näytön resoluutioiden osalta yhdistämisen aikana.\nPlain APK -tiedostoissa poistetaan natiivikirjastot tuettujen arkkitehtuurien osalta korjaamisen jälkeen" + + Bytekoodin käsittelytila + Määrittää, miten bytecode käsitellään paikkatessa. Vaikuttaa paikkatuksen nopeuteen, muistin käyttöön ja ulostulon APK:n kokoon + Nopea (Suositeltu) + Nopea + Nopeampi korjaus ja pienempi muistin käyttö, mutta APK-tiedoston koko kasvaa. Suositellaan laiteille, joissa on vähän RAM-muistia tai vanhoille laitteille + Koko + Hitaampi korjaus, toistaa vanhaa toimintaa. Käytä vain, jos pika-moodi aiheuttaa ongelmia GitHub PAT Vaatimus pull request lähteille @@ -509,13 +529,7 @@ Kuvien mitat on oltava seuraavasti: Asennettu Shizuku-versio ei ole tuettu. Avaa Shizuku Asennus epäonnistui. Yritä uudelleen tai vaihda toiseen asennukseen. - Android esti asennuksen. Tarkista Play Protect tai turvaympäristöasetukset. Toinen sovellus samalla pakettinimellä on jo asennettu. Poista se ennen jatkamista. - APK ei ole yhteensopiva tällä laitteella tai Android-versiolla. - APK on epäkelpoa tai vikaista. - Tilaa ei riitä asennukseen. - Asennus aikakatkosi. Yritä uudelleen. - Asennus peruutettiin. Avataan kohdetta %1$s… %1$s ei vahvistanut asennusta. Tarkista toinen sovellus ja yritä uudelleen. %1$s raportoi onnistuneen asennuksen. @@ -546,6 +560,7 @@ Kuvien mitat on oltava seuraavasti: Väärin keystore-tunnus (alias) Väärin keystore-tunnus (alias) Tuo + Keystore-muoto Väärin keystore-tunnus Väärin keystore-tunnus Keystore tuotu onnistui @@ -553,6 +568,7 @@ Kuvien mitat on oltava seuraavasti: Vie nykyinen keystore Ei keystorea saatavilla vientiin Keystore vietyty onnistui + Keystoren vienti epäonnistui Näytä salasana Piilota salasana @@ -681,7 +697,8 @@ Kuvien mitat on oltava seuraavasti: Salli Tiedot Piilota - Oletus + Piilotettu + Näytä Päiväkirjat Lähteet Asennetaan… diff --git a/app/src/main/res/values-fil-rPH/plurals.xml b/app/src/main/res/values-fil-rPH/plurals.xml index 7f8fb547d..61c7eae4b 100644 --- a/app/src/main/res/values-fil-rPH/plurals.xml +++ b/app/src/main/res/values-fil-rPH/plurals.xml @@ -17,8 +17,8 @@ %s mga pakete - Magsagawa ng %s patch - Magsagawa ng %s mga patch + Isinagawa ang %s patch + Isinagawa ang %s mga patch %s APK file @@ -36,4 +36,12 @@ %s napili na patch %s napili na mga patch + + Ipakita ang %s app + Ipakita ang %s apps + + + %s nakatagong app + %s nakatagong apps + diff --git a/app/src/main/res/values-fil-rPH/strings.xml b/app/src/main/res/values-fil-rPH/strings.xml index 6fc32bb3e..4bad6f258 100644 --- a/app/src/main/res/values-fil-rPH/strings.xml +++ b/app/src/main/res/values-fil-rPH/strings.xml @@ -32,6 +32,8 @@ Walang available na mga app Magdagdag ng pinagmulan ng patch o paganahin ang isang umiiral na isa sa %s Walang app na tumutugma sa \"%1$s\" + Nakatago ang lahat ng apps + Nakatago mo ang lahat ng apps sa home screen Anong app ang gusto mong i-patch? @@ -91,10 +93,14 @@ Upang ma-patch ang <b>%s</b>, kailangan mo ng hindi pa napa-patch na APK na may mga bersyong: Inirerekomenda Hindi na-patch + Hindi tugma Oo, tulungan mo akong hanapin ang APK Hindi, mayroon na akong APK Gamitin ang nakaimbak na APK (v%s) Walang nakaimbak na APK. Ang pag-patch na ito ay mangangailangan ng muling pagpili ng APK file + Nangangailangan ng Android %1$s+ + Hindi suportado sa aparatong ito + <b>%1$s</b> ay walang sinusuportahang bersyon para sa Android %2$d (API %3$d). Lahat ng idineklarang bersyon ay nangangailangan ng mas mataas na bersyon ng Android I-download ang orihinal na APK Mga Tagubilin: @@ -221,6 +227,7 @@ Para sa pinakamahusay na resulta, nirerekomenda ng app na ito na i-patch ang isa Magagamit sa bagong bersyon ang mga patch. Ire-patch ang aplikasyon upang makuha ang mga pinakabagong pagpapabuti at mga pag-aayos Binura ang aplikasyon Binura ang aplikasyong ito sa labas ng Morphe. Ire-patch ito upang maibalik ang kanyang punksyonalidad + Laki ng APK Kinakailangan ng root mode ang orihinal na APK upang mag-install ito sa iyong device bago ito i-patch Hindi kasama ang GmsCore support patch sa root mode @@ -308,16 +315,13 @@ Para sa pinakamahusay na resulta, nirerekomenda ng app na ito na i-patch ang isa Nabigong ilapat %s Na-save ang na-patch na aplikasyon para sa ibang pagkakataon Nabigong ma-save ang na-patch na aplikasyon - I-download ang APK file Kinakailangan ng pakikipag-ugnayan sa tagagamit upang magpatuloy sa plugin na ito Lumabas ang proseso ng patcher na may code %1$s Matagumpay na na-install Nabigong ma-install ang aplikasyon: %s - Hindi natapos ang pag-install. Suriin ang diyalogo ng system installer at subukan muli Na-save ang APK Nabigong mag-export ang na-patch na aplikasyon Inalis ang naka-save na na-patch na aplikasyon - Inalis ang naka-save na kopya I-install muna ang aplikasyon bago mo ito buksan Pangalan ng pakete Nabigong i-mount: %s @@ -337,6 +341,10 @@ Para sa pinakamahusay na resulta, nirerekomenda ng app na ito na i-patch ang isa Griyá Partikulo Wala + Random + Sa paglunsad + Araw-araw + Bawat 3 araw Epekto ng parallax Maayos na paglilipat ng background kapag tinatagilid ang device @@ -353,6 +361,10 @@ Para sa pinakamahusay na resulta, nirerekomenda ng app na ito na i-patch ang isa Kasalukuyang wika Maaaring nawawala o hindi kumpleto ang pagsasalin-wika para sa ibang wika Upang isalin ang mga bagong wika o pagbutihin ang mga umiiral na pagsasalin-wika, bisitahin ang %s + + Pangunahing iskrin + Mga pagbati + Ipakita ang mensahe ng pagbati sa pangunahing iskrin Icon ng aplikasyon Panimula @@ -399,8 +411,17 @@ Para sa pinakamahusay na resulta, nirerekomenda ng app na ito na i-patch ang isa Paganahin ang masalimuot na Morphe patch settings at mga pasadyang pagpipilian Paganahin ang Expert mode? Nagbibigay ng higit na kontrol ang Expert mode sa kung paano inilalapat ang mga patch, ngunit ang maling pagkompigura ng mga pagpipilian sa expert mode ay maaaring magresulta sa isang hindi gumaganang aplikasyon - Alisin ang mga hindi nagamit na mga native library - Burahin ang mga native library para sa mga hindi suportadong arkitektura ng CPU mula sa mga na-patch na aplikasyon + I-optimize para sa arkitektura ng device + "Laktawan ang mga split APK module para sa mga hindi sinusuportahang CPU architecture, locales, at screen densities sa panahon ng merge. +Para sa plain APKs, tinatanggal ang mga native library para sa mga hindi sinusuportahang architecture pagkatapos mag-patch" + + Paraan ng pagproseso ng bytecode + Kinokontrol kung paano ipinoproseso ang bytecode sa panahon ng pag-patch. Nakakaapekto sa bilis ng pag-patch, paggamit ng memorya, at sukat ng output na APK + Mabilis (Inirerekomenda) + Mabilis + Mas mabilis na pag-patch at mas mababang paggamit ng memorya, sa kapinsalaan ng mas malaking APK. Inirerekomenda para sa mga low-RAM o mas lumang device + Buo + Mas mabagal na pag-patch, gumagaya sa legacy na pag-uugali. Gamitin lamang kung nagiging sanhi ng isyu ang fast mode GitHub PAT Kailangan para sa mga pull request sources @@ -510,13 +531,7 @@ Dapat ang mga sumusunod na sukat ng larawan ay: Hindi suportado ang naka-install na bersyon ng Shizuku Buksan ang Shizuku Nabigo ang pag-install. Subukan muli o lumipat sa ibang installer - Hinarang ng Android ang pag-install. Suriin ang Play Protect o security settings Naka-install na ang isa pang aplikasyon na may ganitong pangalan ng pakete. Burahin muna bago magpatuloy - Hindi tugma ang APK sa device na ito o bersyon ng Android - Imbalido o sira ang APK - Walang sapat na espasyo sa imbakan para sa pag-install - Nag-time out ang pag-install. Subukan muli - Kinansela ang pag-install Binubuksan ang %1$s… Hindi nakumpirma ng %1$s ang pag-install. Tingnan ang ibang aplikasyon at subukan muli Iniulat ng %1$s ang matagumpay na pag-install @@ -547,6 +562,7 @@ Dapat ang mga sumusunod na sukat ng larawan ay: Pangalan ng tagagamit (Bansag) Password I-import + Format ng Keystore Mali ang keystore credentials Na-import ang keystore Nabigong ma-import ang keystore @@ -554,6 +570,7 @@ Dapat ang mga sumusunod na sukat ng larawan ay: I-export ang kasalukuyang keystore Walang magagamit na keystore para sa pag-export Na-export ang keystore + Nabigo ang pag-export ng keystore Ipakita ang password Itago ang password @@ -682,7 +699,8 @@ Dapat ang mga sumusunod na sukat ng larawan ay: Payagan Mga Detalye Itago - Panimula + Nakatago + Ipakita Mga log Mga mapagkukunan Ini-install… diff --git a/app/src/main/res/values-fr-rFR/plurals.xml b/app/src/main/res/values-fr-rFR/plurals.xml index d90f9ab7c..d62ed09bb 100644 --- a/app/src/main/res/values-fr-rFR/plurals.xml +++ b/app/src/main/res/values-fr-rFR/plurals.xml @@ -17,8 +17,8 @@ %s packages - Exécuter %s patch - Exécuter %s patchs + %s patch exécuté + %s patchs exécutés %s fichier APK @@ -36,4 +36,12 @@ %s patch sélectionné %s patchs sélectionnés + + Afficher %s application + Afficher %s applications + + + %s application masquée + %s applications masquées + diff --git a/app/src/main/res/values-fr-rFR/strings.xml b/app/src/main/res/values-fr-rFR/strings.xml index 330cf0e7d..89905cf09 100644 --- a/app/src/main/res/values-fr-rFR/strings.xml +++ b/app/src/main/res/values-fr-rFR/strings.xml @@ -32,6 +32,8 @@ Aucune application disponible Ajoutez une source de patch ou activez-en une existante dans %s Aucune application ne correspond à \"%1$s\" + Toutes les applications sont masquées + Vous avez masqué toutes les applications de l’écran d’accueil Quelle application voulez-vous patcher ? @@ -91,10 +93,14 @@ Pour patcher <b>%s</b>, vous avez besoin d’un APK non patché des versions : Recommandé Non patchée + Incompatible Oui, aidez-moi à trouver un APK Non, j\'ai déjà un APK Utiliser l’APK enregistré (v%s) Aucun APK enregistré. Le patchage nécessitera de sélectionner à nouveau le fichier APK + Nécessite Android %1$s+ + Non pris en charge sur cet appareil + <b>%1$s</b> n\'a pas de versions compatibles pour Android %2$d (API %3$d). Toutes les versions déclarées nécessitent une version Android plus récente Télécharger l\'APK original Instructions : @@ -220,6 +226,7 @@ Pour de meilleurs résultats, cette application recommande de patcher un <b&g Une version plus récente des patchs est disponible. Repatchez votre application pour bénéficier des dernières améliorations et corrections L’application a été désinstallée Cette application a été désinstallée en dehors de Morphe. Repatchez-la pour restaurer les fonctionnalités + Taille de l\'APK Le mode root nécessite que l\'APK original soit installé sur votre appareil avant le patchage Le patch de support GmsCore est exclu en mode root @@ -307,16 +314,13 @@ Pour de meilleurs résultats, cette application recommande de patcher un <b&g Échec de l\'application de %s Application patchée enregistrée pour plus tard Échec de l\'enregistrement de l\'application patchée - Télécharger le fichier APK Une interaction de l\'utilisateur est nécessaire pour pouvoir poursuivre avec ce plugin Le processus du patcheur s’est terminé avec le code %1$s Installé avec succès Impossible d\'installer l\'application : %s - L\'installation n\'est pas terminée. Vérifiez la boîte de dialogue de l\'installateur système et réessayez APK enregistré Échec de l\'exportation de l\'application patchée Suppression de l\'application patchée enregistrée - Copie enregistrée supprimée Installez l\'application avant de l\'ouvrir Nom de package Échec du montage : %s @@ -336,6 +340,10 @@ Pour de meilleurs résultats, cette application recommande de patcher un <b&g Grille Particules Aucun + Aléatoire + Au lancement + Quotidien + Tous les 3 jours Effet de parallaxe Déplacement d\'arrière-plan fluide lors de l\'inclinaison de l\'appareil @@ -352,6 +360,10 @@ Pour de meilleurs résultats, cette application recommande de patcher un <b&g Langue actuelle Certaines traductions peuvent être manquantes ou incomplètes Pour traduire de nouvelles langues ou améliorer les traductions existantes, visitez %s + + Écran d\'accueil + Phrases de salutation + Afficher un message de salutation sur l\'écran d\'accueil Icône de l\'application Par défaut @@ -399,8 +411,17 @@ Pour obtenir les patchs en pré-version, activez l’option pré-versions sépar Activer les paramètres de patchage Morphe complexes et les options de personnalisation Passer en mode Expert ? Le mode Expert offre plus de contrôle sur la manière dont les patchs sont appliqués, mais une mauvaise configuration des paramètres en mode expert peut rendre l’application inutilisable - Supprimer les bibliothèques natives inutilisées - Supprimer les bibliothèques natives pour les architectures CPU non prises en charge des applications patchées + Optimiser pour l\'architecture de l\'appareil + "Ignorer les modules APK fractionnés pour les architectures CPU, les locales et les densités d’écran non prises en charge lors de la fusion. +Pour les APK simples, supprime les bibliothèques natives pour les architectures non prises en charge après le patchage" + + Mode de traitement du bytecode + Définit la manière dont le bytecode est traité lors du patchage. Impacte la vitesse de patchage, l’utilisation mémoire et la taille de l’APK généré + Rapide (Recommandé) + Rapide + Patchage plus rapide et utilisation mémoire réduite, au prix d’un APK plus volumineux. Recommandé pour les appareils à faible RAM ou anciens + Complet + Patchage plus lent, reproduisant le comportement précédent. À utiliser uniquement si le mode rapide pose problème Jeton d\'accès personnel GitHub Nécessaire pour les pull requests de sources @@ -512,13 +533,7 @@ Les dimensions des images doivent respecter les valeurs suivantes : La version de Shizuku installée n\'est pas prise en charge Ouvrir Shizuku L\'installation a échoué. Réessayez ou choisissez un installateur différent - L\'installation a été bloquée par Android. Vérifiez Play Protect ou les paramètres de sécurité Une autre application portant ce nom de package est déjà installée. Désinstallez-la avant de continuer - L\'APK n\'est pas compatible avec cet appareil ou cette version d\'Android - L\'APK est invalide ou corrompu - Il n\'y a pas assez d\'espace de stockage pour l\'installation - L\'installation a expiré. Veuillez réessayer - L\'installation a été annulée Ouverture de %1$s… %1$s n\'a pas confirmé l\'installation. Vérifiez l\'autre application et réessayez %1$s a signalé une installation réussie @@ -549,6 +564,7 @@ Les dimensions des images doivent respecter les valeurs suivantes : Nom d\'utilisateur (Alias) Mot de passe Importer + Format de keystore Identifiants de keystore incorrects Keystore importé Échec de l\'importation du keystore @@ -556,6 +572,7 @@ Les dimensions des images doivent respecter les valeurs suivantes : Exporter le keystore actuel Aucun keystore disponible à exporter Keystore exporté + Échec de l\'exportation du keystore Afficher le mot de passe Masquer le mot de passe @@ -684,7 +701,8 @@ Les dimensions des images doivent respecter les valeurs suivantes : Autoriser Détails Masquer - Par défaut + Masqué + Afficher Journaux Sources Installation… diff --git a/app/src/main/res/values-ga-rIE/plurals.xml b/app/src/main/res/values-ga-rIE/plurals.xml index 12485e08d..a2127574f 100644 --- a/app/src/main/res/values-ga-rIE/plurals.xml +++ b/app/src/main/res/values-ga-rIE/plurals.xml @@ -29,11 +29,11 @@ %s pacáistí - Cuir %s paiste i gcrích - Cuir %s paistí i gcrích - Cuir %s paistí i gcrích - Cuir %s paistí i gcrích - Cuir %s paistí i gcrích + Paiste %s curtha i gcrích + %s paistí curtha i gcrích + %s paistí curtha i gcrích + %s paistí curtha i gcrích + %s paistí curtha i gcrích %s comhad APK @@ -63,4 +63,18 @@ %s paistí roghnaithe %s paistí roghnaithe + + Taispeáin %s aip + Taispeáin %s aipeanna + Taispeáin %s aipeanna + Taispeáin %s aipeanna + Taispeáin %s aipeanna + + + %s aip i bhfolach + %s aipeanna i bhfolach + %s aipeanna i bhfolach + %s aipeanna i bhfolach + %s aipeanna i bhfolach + diff --git a/app/src/main/res/values-ga-rIE/strings.xml b/app/src/main/res/values-ga-rIE/strings.xml index 84b8e01e2..aaef49c56 100644 --- a/app/src/main/res/values-ga-rIE/strings.xml +++ b/app/src/main/res/values-ga-rIE/strings.xml @@ -10,12 +10,12 @@ Taispeáin aipeanna i bhfolach Gan aon aipeanna i bhfolach Aipeanna i bhfolach - Tap to dáil os cúairt + Tapáil chun é a nochtadh Fabht Android 11 Ní mór cead suiteála na haipe a ghéilleadh roimh ré chun seachaint ar earráid i gcóras Android 11 a dhéanfaidh dochar d\'eispéireas an úsáideora Add foinse - An cosúil leat é seo a chur go Morphe? - Níor add foinse ach óna dtaobh tú muintir + Ar mhaith leat an foinse paiste seo a chur le Morphe? + Cuir foinse ó fhoinsí iontaofa amháin leis Nuashonrú Morphe ar fáil Tá nuashonrú nua ar fáil @@ -28,10 +28,12 @@ Foinsí nuashonraithe críochnaithe Tá do foinsí suas chun dáta Tá na foinsí ag lódáil, fan le do thoil… - Cuardaigh appanna - Níl aon appanna ar fáil - Cuir freagar foinse pátráin nó cruthnoigh ceann atá cheana fáil ann i %s - Ní bhféachann aon appanna leis \"%1$s\" + Cuardaigh aipeanna + Níl aon aipeanna ar fáil + Cuir foinse paiste leis nó cumasaigh ceann atá ann cheana féin i %s + Níl aon aipeanna a mheaitseálann \"%1$s\" + Tá gach aipeanna i bhfolach + Tá na haipeanna go léir i bhfolach agat ón scáileán baile Cén aip ar mhaith leat paiste a chur leis? @@ -58,9 +60,9 @@ Teip ar URL a oscailt Bain úsáid as paistí réamh-eisiúna Faigh rochtain luath ar leagan nua paistí turgnamhacha - Úsáid leaganacha tástálacha - Leasú leaganacha tástálacha, má tá siad ar fáil - Tá an foinse seo curtha leis cheana féin + Úsáid leaganacha turgnamhacha d’aipeanna + Paiste spriocanna aipeanna turgnamhacha más féidir + Tá an fhoinse seo curtha leis cheana féin Ainm taispeána Athainmnigh foinse an phaiste @@ -83,18 +85,22 @@ Theip ar an pacáiste a íoslódáil: %s Theip ar an íoslódáil \'%1$s\': %2$s Níorbh fhéidir \'%1$s\' a nuashonrú: níor aimsíodh an comhad paiste JSON ag an URL cumraithe - Eolas - Cumas an modh Eolas i Suitearachta chun an leas seo a chur isteach go láimhe + Saineolaí + Cumasaigh mód saineolaithe sna Socruithe chun an paiste seo a chur san áireamh de láimh An bhfuil cabhair uait chun APK bunaidh neamhphaisteáilte a aimsiú? Chun <b>%s</b>, a phaisteáil, beidh an leagan APK neamhphaisteáilte ag teastáil uait: Chun <b>%s</b>, a phaisteáil, beidh APK neamhphaisteáilte de na leaganacha seo a leanas ag teastáil uait: Molta Díphaisteáilte + Comhroinnte Sea, cabhraigh liom APK a aimsiú Níl, tá APK agam cheana féin Úsáid an APK sábháilte (v%s) Níl aon APK sábháilte. Beidh ort an comhad APK a roghnú arís le haghaidh paisteála. + Tá Android %1$s+ de dhian + Ní thacaítear leis an ngníomh seo + <b>%1$s</b> níl aonta le foighneamhacha do Android %2$d (API %3$d). Teastaíonn leagan níos airde Android óna leaganacha a dearbhú Íoslódáil an APK bunaidh Treoracha: @@ -114,17 +120,17 @@ Leagan roghnaithe Roghnaigh APK Beartaigh an cnaipe thíos chun comhad APK d\'aon aip a roghnú le haghaidh paisteála. - ́Nó ́ćnód ́ćanéarén an comhad. Seiceán gur ́ćann ar ́ćann ́ćómhad neamhréidhte an ́ćann ́ćonn tobaró. - ́Nó ́ćearéar an comhad roghñénéar nach bhfuil ́ćo APK bhailidó. - + Níorbh fhéidir an comhad a léamh. Seiceáil nach bhfuil srian ceallraí ar \'Stóráil Sheachtrach\' + Ní APK bailí an comhad roghnaithe + Theip ar an gcomhad a oscailt. Déan iarracht arís. Mód saineolaithe Paistí cuardaigh Lean ar aghaidh leis an bpaisteáil Díchumasaigh gach rud Cumasaigh gach rud - Ná déan cinnte gur na patchanna molta atá á gcumadh - + Cumasaigh paistí molta + Athchóirigh an rogha sábháilte Níor aimsíodh aon phaistí Paistí uilíocha Nua @@ -133,19 +139,19 @@ An bhfuil tú cinnte gur mian leat dul ar aghaidh?" - APKannaíseach - Ní athruithe APK a roghnú sibh do <b>%s</b> le comhaid síniúth fhadcheart a bhagairt. Seans go bhfuil sé modhnaithe nó ón bhfoinse neamhdtrustaithe - Seans nach bhfuil an APK seo mar bhunaí - Faight APK faight - "Is comhad seo bundl APK + APK neamhfhíoraithe + Ní hionann an APK a roghnaigh tú do <b>%s</b> agus an deimhniú sínithe a bhfuiltear ag súil leis. Seans go bhfuil sé modhnaithe nó go bhfuil sé ó fhoinse neamhiontaofa. + B’fhéidir nach é an APK seo an bunleagan + APK scoilte braite + "Is pacáiste APK é an comhad seo (<b>APKM / APKS / XAPK</b>). -Le haghaidh na dtorthaí is fearr, molann an app seo míniú <b>APK iomlán</b>" +Chun na torthaí is fearr a fháil, molann an aip seo paiste a chur ar <b>APK iomlán</b>" Roghnaigh APK eile Turgnamhach - Feidir gur neamhstadach nó neamhiomlán tacaíocht turgnamhach seo - An mhaith leat a turgnamh? 🧪 - Leagan <b>%s</b> seo a bhfuil tacaíocht luath turgnamhach ann<br/><br/>🔧 Fan le do thoil dímheascailt app nó leictheoirí neamhaird mar atá na feistear á bainistiú do leagan app seo + Tá tacaíocht thurgnamhach ag an leagan seo agus d\'fhéadfadh sé a bheith éagobhsaí nó neamhiomlán + Ar mhaith leat turgnamh a dhéanamh? 🧪 + Tá tacaíocht luath turgnamhach ag an leagan seo de <b>%s</b><br/><br/>🔧 Bí ag súil le hiompar aisteach an aip nó fabhtanna anaithnid de réir mar a dhéantar na paistí a fheabhsú don leagan seo den aip Leagan Gan Tacaíocht Ní leagan molta den aip é an APK seo. D’fhéadfadh sé nach n-oibreodh an aip má leanann tú ar aghaidh Lean ar aghaidh ar aon nós @@ -159,9 +165,9 @@ Le haghaidh na dtorthaí is fearr, molann an app seo míniú <b>APK iomlá Tá sonraí soghluaiste gníomhach agus tá nuashonruithe paiste díchumasaithe D’fhéadfadh go mbeadh aip briste mar thoradh ar phaistí le paistí atá as dáta Nuashonrú & Paiste - Spás disco ísl - Achainn %1$.2f GB de spás saor ann. Éilíonn péinteáil ar a laghad %2$.2f GB de spás saor chun obair a dhéanamh go ceart - Éadh go bhféadfadh ‘file not found’ a bhraoibh, nó APK aschuir cora + Spás diosca íseal + Níl ach %1$.2f GB de spás saor in aisce ar fáil. Éilíonn paisteáil ar a laghad %2$.2f GB de spás saor in aisce le go n-oibreoidh sé i gceart. + D’fhéadfadh earráid \"comhad gan aimsiú\" nó APK aschuir truaillithe a bheith mar thoradh ar dhul ar aghaidh. Dath saincheaptha Dath heicsidheachúlach @@ -170,15 +176,15 @@ Le haghaidh na dtorthaí is fearr, molann an app seo míniú <b>APK iomlá Cuir isteach uimhir Cuir isteach deachúil Roghnaigh fillteán - Tá an luach seo cheannasach cheana féin - Ná féidir luach a bheith folaithe - Níor cuireadh aon luachanna leis fós - %s: líon isteach na roghanna uile atá de dhleathadh + Tá an luach seo ann cheana féin + Ní féidir luach a fhágáil folamh + Níl aon luachanna curtha leis fós + %s: líon isteach na roghanna riachtanacha go léir Cruthaigh deilbhín oiriúnaitheach - Roghnaigh íomhá sa tulra, roghnaigh dath an chúlra, ansin úsáid an sliothar nó gothaí pinch chun an réamhamharc a choigeartú - Réamhamharc ar Íomhá Shuiteáil - Le haghaidh torthaí is fearr, coimead do íomhá laistigh den ciorcal istigh soladach + Roghnaigh íomhá sa tulra, roghnaigh dath cúlra, ansin bain úsáid as an sleamhnán nó as gothaí pinch chun an réamhamharc a choigeartú + Réamhamharc deilbhín oiriúnaitheach + Chun an toradh is fearr a fháil, coinnigh do dheilbhín laistigh den chiorcal istigh soladach Roghnaigh íomhá Athraigh íomhá Dath an chúlra @@ -188,8 +194,8 @@ Le haghaidh na dtorthaí is fearr, molann an app seo míniú <b>APK iomlá Cruthaíodh deilbhín oiriúnaitheach go rathúil Theip ar dheilbhín oiriúnaitheach a chruthú Roghnaigh cá háit le fillteán \'%1$s/%2$s\' a chruthú leis na comhaid deilbhín a ghintear - Réamhamharc ar Íomhá Fógra - Úsáid an sliothar chun meascán a athrú. Ardrag chun athshocrú + Réamhamharc ar dheilbhín fógra + Úsáid an sleamhnán chun an méid a athrú. Tarraing chun athshuíomh. Cruthaigh ceanntásc saincheaptha Roghnaigh íomhánna le haghaidh téamaí geala agus dorcha, agus ansin coigeartaigh iad le gothaí pinch-to-súmáil @@ -198,7 +204,7 @@ Le haghaidh na dtorthaí is fearr, molann an app seo míniú <b>APK iomlá Níl aon íomhá roghnaithe Cruthaíodh ceanntásc go rathúil Theip ar cheanntásc a chruthú - Roghnaigh cá háit le fillteán \'%1$s/%2$s\' a chruthú leis na uimhreacha tairbheacha a ghintear + Roghnaigh cá háit le fillteán \'%1$s/%2$s\' a chruthú leis na comhaid ceanntásca a ghintear An bhfuil tú cinnte gur mian leat an aip seo a dhíshuiteáil? Scriosfaidh sé seo go buan: @@ -221,6 +227,7 @@ Le haghaidh na dtorthaí is fearr, molann an app seo míniú <b>APK iomlá Tá leagan níos nuaí de na paistí ar fáil. Déan paiste nua ar d\'aip chun na feabhsuithe agus na socruithe is déanaí a fháil. Díshuiteáladh an aip Díshuiteáladh an aip seo lasmuigh de Morphe. Athphaisteáil í chun feidhmiúlacht a athbhunú. + Méid APK Éilíonn mód fréimhe go mbeidh an APK bunaidh suiteáilte ar do ghléas sula ndéantar paisteáil Tá paiste tacaíochta GmsCore eiscthe i mód fréimhe @@ -235,7 +242,7 @@ Le haghaidh na dtorthaí is fearr, molann an app seo míniú <b>APK iomlá Earráid cóipeáilte chuig gearrthaisce Log earráide Sonraí teicniúla - Eolas app + Eolas faoin aip D’fhéadfadh sé go dtógfadh an chéim seo tamall. Fan le do thoil… Beagnach réidh le tosú! 🎸 Gach rud déanta! 🎉 @@ -308,16 +315,13 @@ Le haghaidh na dtorthaí is fearr, molann an app seo míniú <b>APK iomlá Theip ar %s a chur i bhfeidhm Comhadaithe aip sábháilte le haghaidh níos déanaí Theip ar aip chomhfhiosach a shábháil - Íoslódáil comhad APK Tá idirghníomhaíocht úsáideora ag teastáil chun dul ar aghaidh leis an mbreiseán seo Cuireadh deireadh leis an bpróiseas paisteála le cód %1$s Suiteáilte go rathúil Theip ar aip a shuiteáil: %s - Níor críochnaíodh an suiteáil. Seiceáil an dialóg suiteálaí córais agus déan iarracht arís. APK Sábháilte Theip ar aip paisteáilte a easpórtáil Baineadh an aip phaisteáilte sábháilte - Baineadh an chóip shábháilte Suiteáil an aip sula n-osclaíonn tú é Ainm an phacáiste Theip ar fheistiú: %s @@ -334,9 +338,13 @@ Le haghaidh na dtorthaí is fearr, molann an app seo míniú <b>APK iomlá Spás Cruthanna Sneachta - Gríd - Réaltóga + Eangach + Cáithníní Dada + Randam + Ar thosú + Laethúil + Gach 3 lae Effeact parallax Bogadh réidh an chúlra agus an gléas á chlaonadh @@ -353,6 +361,10 @@ Le haghaidh na dtorthaí is fearr, molann an app seo míniú <b>APK iomlá Teanga reatha D’fhéadfadh aistriúcháin do roinnt teangacha a bheith ar iarraidh nó neamhiomlán Chun teangacha nua a aistriú nó na haistriúcháin atá ann cheana a fheabhsú, tabhair cuairt ar %s + + Home screen + Frásaí beannachta + Taispeáin teachtaireacht beannachta ar an scáileán baile Deilbhín aip Réamhshocrú @@ -367,9 +379,9 @@ Le haghaidh na dtorthaí is fearr, molann an app seo míniú <b>APK iomlá Nuashonruithe Bain úsáid as Morphe réamh-eisiúna - Faigh rochtain luath ar fheidhmeanna nua Morphe. Chun paistí réamh-eisiúna a fháil, cumasaigh an lascán réamh-eisiúna i ngach foinse paiste ar leithligh + Faigh rochtain luath ar ghnéithe nua Morphe. Chun paistí réamh-eisiúna a fháil, cumasaigh an lasc réamh-eisiúna i ngach foinse paiste ar leithligh. Nuashonrú ar shonraí soghluaiste - Ceadaigh nuashonruithe Morphe agus paistí a íoslódáil thar shonraí soghluaiste + Ceadaigh nuashonruithe Morphe agus paiste a íoslódáil thar shonraí soghluaiste Fógraí nuashonraithe cúlra Seiceáil go tréimhsiúil le haghaidh nuashonruithe sa chúlra agus cuir in iúl nuair a bhíonn siad ar fáil Faigh fógraí brú láithreacha le haghaidh eisiúintí nua, fiú nuair a bhíonn an aip dúnta @@ -385,22 +397,31 @@ Le haghaidh na dtorthaí is fearr, molann an app seo míniú <b>APK iomlá Nuashonrú Morphe ar fáil Paistí nua ar fáil Tá leagan %1$s réidh le híoslódáil - Beartaigh chun Morphe a oscailt agus a nuashonrú + Tapáil chun Morphe a oscailt agus a nuashonrú Ceadaigh fógraí Teastaíonn cead fógra ó Morphe chun foláireamh a thabhairt duit nuair a bhíonn nuashonruithe ar fáil sa chúlra Ar mhaith leat fógra a fháil nuair a bheidh nuashonruithe ar fáil? - Nuashonruithe fógraí - Fógraí faoi leaganacha nua de Morphe agus eisisí - Paisteáil - Taispeánadh agus tá Morphe ag eisiúint app san chúlra + Fógraí nuashonraithe + Fógraí faoi eisiúintí nua Morphe agus paistí + Ag paisteáil + Taispeántar é agus Morphe ag paisteáil aip sa chúlra Socruithe saineolaithe Mód saineolaithe Cumasaigh socruithe paisteála casta Morphe agus roghanna saincheaptha Cumasaigh mód saineolaithe? Tugann mód saineolaithe níos mó smachta ar an gcaoi a gcuirtear paistí i bhfeidhm, ach is féidir le socruithe míchumraithe i mód saineolaithe aip neamhfheidhmiúil a bheith mar thoradh air. - Bain leabharlanna dúchasacha neamhúsáidte - Scrios leabharlanna dúchasacha le haghaidh ailtireachtaí LAP neamhthacaithe ó aipeanna paisteáilte + Optamaigh le haghaidh ailtireacht gléasanna + "Seachain modúil scoilte APK le haghaidh ailtireachtaí LAP, logán agus dlúis scáileáin nach dtacaítear leo le linn an chumasc. +I gcás APKanna simplí, baintear leabharlanna dúchasacha d'ailtireacht nach dtacaítear leo tar éis paisteála." + + Mód próiseála cód beart + Rialaíonn sé conas a phróiseáiltear cód beart le linn paisteála. Bíonn tionchar aige ar luas paisteála, úsáid cuimhne, agus méid aschuir APK. + Tapa (Molta) + Tapa + Paistí níos tapúla agus úsáid chuimhne níos ísle, ar chostas APK níos mó. Molta do ghléasanna íseal-RAM nó níos sine. + Lán + Paisteáil níos moille, macasamhlaíonn sé iompar oidhreachta. Úsáid ach amháin má chruthaíonn an modh tapa fadhbanna. PAT GitHub Riachtanach le haghaidh foinsí iarratais tarraingthe @@ -409,11 +430,11 @@ Le haghaidh na dtorthaí is fearr, molann an app seo míniú <b>APK iomlá Socraigh Comhartha Rochtana Pearsanta GitHub Socraigh Comhartha Rochtana Pearsanta GitHub chun go mbeidh tú in ann iarratais tarraingthe a chur leis mar fhoinsí seachtracha. Cruthaigh PAT le raon feidhme public_repo github.com Cuir san áireamh in easpórtáil socruithe - Cuireann sé an PAT seo le socruithe Morphe onnmhairithe + Cuireann an PAT seo le socruithe Morphe onnmhairithe Ná roinn do PAT. Má chuireann tú san áireamh é in easpórtáil socruithe, coinnigh an comhad sin príobháideach mar go bhfuil an comhartha ann. Roghanna paiste - Roghanna téama, brandála, ceanntásca agus Shorts + Roghanna téama, brandála agus ceanntásca Éilíonn athrú roghanna paiste athphaisteáil ar an aip le go dtiocfaidh sé i bhfeidhm I mód Saineolaí beidh roghanna paiste ar fáil le linn an phróisis paisteála @@ -432,38 +453,36 @@ Le haghaidh na dtorthaí is fearr, molann an app seo míniú <b>APK iomlá Ainm an aip Cuir isteach ainm saincheaptha Deilbhín saincheaptha - "Foldha le híomhánacha a úsáid mar íomhá saincheaptha. - -The foldha níl ann ach ceann nó níos mó de na folldaí seo a leanas, ag brabhs na DPI de mheánloigh: + "Fillteán le híomhánna le húsáid mar dheilbhín saincheaptha. Caithfidh ceann amháin nó níos mó de na fillteáin seo a leanas a bheith sa fhillteán, ag brath ar DPI an fheiste: - mipmap-mdpi - mipmap-hdpi - mipmap-xhdpi - mipmap-xxhdpi - mipmap-xxxhdpi -Caighdeán na folldaí níl ann ach gach ceann de na comhaltaí seo a leanas: +Ní mór na comhaid seo a leanas a bheith i ngach fillteán: morphe_adaptive_background_custom.png morphe_adaptive_foreground_custom.png -Ní féidir na tomhais an íomhá a bheith mar a leanas: +Ní mór toisí na híomhá a bheith mar seo a leanas: - mipmap-mdpi: 108x108 px - mipmap-hdpi: 162x162 px - mipmap-xhdpi: 216x216 px - mipmap-xxhdpi: 324x324 px - mipmap-xxxhdpi: 432x432 px -Mar rogha, tá an t-rath i n-eolach 'drawable' le comhalta íomhá monocrómach: +De rogha air sin, tá fillteán 'intarraingthe' le deilbhín monacrómach sa chonair. comhad: morphe_adaptive_monochrome_custom.xml -Mar rogha, tá an t-rath i n-eolach comhartha fógra ina ceann de na formáidí seo a leanas: -- XML vector drawable i 'drawable': - morphe_notification_icon_custom.xml -- Íomhánacha raster PNG i 'drawable-dpi' folldaí: - drawable-mdpi/morphe_notification_icon_custom.png (24x24 px) - drawable-hdpi/morphe_notification_icon_custom.png (36x36 px) - drawable-xhdpi/morphe_notification_icon_custom.png (48x48 px) - drawable-xxhdpi/morphe_notification_icon_custom.png (72x72 px) - drawable-xxxhdpi/morphe_notification_icon_custom.png (96x96 px)" +De rogha air sin, tá deilbhín fógra sa chonair i gceann de na formáidí seo a leanas: +- Veicteoir XML intarraingthe i 'drawable': +morphe_notification_icon_custom.xml +- Íomhánna rastair PNG i bhfillteáin 'drawable-dpi': +drawable-mdpi/morphe_notification_icon_custom.png (24x24 px) +drawable-hdpi/morphe_notification_icon_custom.png (36x36 px) +drawable-xhdpi/morphe_notification_icon_custom.png (48x48 px) +drawable-xxhdpi/morphe_notification_icon_custom.png (72x72 px) +drawable-xxxhdpi/morphe_notification_icon_custom.png (96x96 px)" Lógó ceanntásc saincheaptha Athraigh lógó an cheanntásca @@ -510,13 +529,7 @@ Ní mór toisí na híomhá a bheith mar seo a leanas: Ní thacaítear leis an leagan Shizuku atá suiteáilte Oscail Shizuku Theip ar an suiteáil. Bain triail eile as nó aistrigh go suiteálaí eile - Cuireadh bac ar an suiteáil ag Android. Seiceáil Play Protect nó socruithe slándála Tá aip eile leis an ainm pacáiste seo suiteáilte cheana féin. Díchumasaigh é sula leanann tú ar aghaidh - Níl an APK comhoiriúnach leis an bhfeiste seo nó leis an leagan Android seo - Tá an APK neamhbhailí nó truaillithe - Níl dóthain spáis stórála ann don suiteáil - Chuaigh an t-am suiteála isteach. Déan iarracht arís - Cuireadh an suiteáil ar ceal Ag oscailt %1$s… %1$s ní chugainn an instealladh. Glaoigh ar an gcló eile agus tairgfidh tú arís Thuairisc %1$s suiteáil rathúil @@ -537,7 +550,7 @@ Ní mór toisí na híomhá a bheith mar seo a leanas: D’fhéadfadh teorainn íseal chuimhne teipeanna a chur faoi deara agus aipeanna níos mó á bpaisteáil Teorainn %s MB Cliceáil chun cumrú - Is fearúr seo a dhícheanleacht Android 11 nó níos déanaidh + Éilíonn an ghné seo Android 11 nó níos déanaí Iompórtáil & easpórtáil Iompórtáil stór eochracha @@ -547,6 +560,7 @@ Ní mór toisí na híomhá a bheith mar seo a leanas: Ainm úsáideora (Ainm eile) Pasfhocal Iompórtáil + Formáid stóir eochrach Dintiúir mhíchearta stórais eochrach Eochairstóras allmhairithe Theip ar an stór eochrach a iompórtáil @@ -554,14 +568,15 @@ Ní mór toisí na híomhá a bheith mar seo a leanas: Easpórtáil an stór eochrach reatha Níl aon stór eochrach ar fáil le honnmhairiú Easpórtáladh an stór eochrach + Theip ar easpórtáil an stór eochrach Taispeáin an focal faire Folaigh an focal faire - Socruithe Morphe allmhairithe + Iompórtáil socruithe Morphe Athchóirigh socruithe Morphe ó chomhad JSON Theip ar shocruithe Morphe a iompórtáil: %s Socruithe Morphe allmhairithe - Socruithe Morphe onnmhairithe + Easpórtáil socruithe Morphe Sábháil socruithe Morphe chuig comhad JSON Theip ar shocruithe Morphe a easpórtáil: %s Socruithe Morphe onnmhairithe @@ -615,19 +630,19 @@ Ní mór toisí na híomhá a bheith mar seo a leanas: Maidir Comhroinn suíomh Gréasáin Comhroinn suíomh Gréasáin oifigiúil Morphe - "Práisteán samhail ar an’ foinse oscair, le haghaidh leasuithe’ straimlíneacha ar’ a’ ri’ app’anna Android co’ coitianta, ar’ mhian le hamharas’ an phobail agus le learíonna’" - Crèidteacha + "Tionscadal foinse oscailte le haghaidh paisteáil nua-aimseartha, sruthlínithe ar aipeanna coitianta Android, á thiomáint ag aiseolas agus ranníocaíochtaí ón bpobal." + Creidmheasanna Forbairt reatha - Forbairt roimh réidh - Licùidóidóis oscair-fóin + Forbairt roimhe seo + Ceadúnais foinse oscailte Paistí Baile Socruithe Níl - Uilig - Scagóir + Gach + Scagaire Athshocraigh Athshocraigh Gach Rud Deimhnigh @@ -682,14 +697,15 @@ Ní mór toisí na híomhá a bheith mar seo a leanas: Ceadaigh Sonraí Folaigh - Réamhshocrú + I bhfolach + Nochtadh Logaí Foinsí Ag suiteáil… Ag ceangal… Ag dícheangal… Ag iompórtáil… - Importáil go rathúil + Iompórtáladh go rathúil Féach ar logaí athruithe Féach ar na hathruithe is déanaí sa nuashonrú seo Theip ar an logála hathruithe a íoslódáil: %s diff --git a/app/src/main/res/values-gl-rES/plurals.xml b/app/src/main/res/values-gl-rES/plurals.xml index ae69c2570..00fc2bd14 100644 --- a/app/src/main/res/values-gl-rES/plurals.xml +++ b/app/src/main/res/values-gl-rES/plurals.xml @@ -16,10 +16,6 @@ %s paquete %s paquetes - - Executar parche %s - Executar parches %s - %s arquivo APK %s arquivos APK diff --git a/app/src/main/res/values-gl-rES/strings.xml b/app/src/main/res/values-gl-rES/strings.xml index f33746140..ace14767c 100644 --- a/app/src/main/res/values-gl-rES/strings.xml +++ b/app/src/main/res/values-gl-rES/strings.xml @@ -202,6 +202,7 @@ Sistema + Predefinido @@ -210,6 +211,7 @@ Parcheando Modo Experto + Opcións de parche @@ -242,5 +244,4 @@ Importar Permitir Detalles - Predefinido diff --git a/app/src/main/res/values-gu-rIN/plurals.xml b/app/src/main/res/values-gu-rIN/plurals.xml index 2d5ad1a40..07a78d98a 100644 --- a/app/src/main/res/values-gu-rIN/plurals.xml +++ b/app/src/main/res/values-gu-rIN/plurals.xml @@ -16,10 +16,6 @@ %s પેકેજ %s પેકેજો - - %s પેચ એક્ઝિક્યુટ કરો - %s પેચો એક્ઝિક્યુટ કરો - %s APK ફાઇલ %s APK ફાઇલો diff --git a/app/src/main/res/values-gu-rIN/strings.xml b/app/src/main/res/values-gu-rIN/strings.xml index 12d243f8d..be8890ace 100644 --- a/app/src/main/res/values-gu-rIN/strings.xml +++ b/app/src/main/res/values-gu-rIN/strings.xml @@ -150,6 +150,7 @@ વર્તમાન ભાષા કેટલીક ભાષાઓ માટે અનુવાદો ખૂટતા અથવા અપૂર્ણ હોઈ શકે છે. નવી ભાષાઓનો અનુવાદ કરવા અથવા હાલના અનુવાદોને સુધારવા માટે, %s ની મુલાકાત લો. + ડિફૉલ્ટ આકાશ @@ -162,6 +163,7 @@ મોર્ફે અદ્યતન ઉપલબ્ધ છે + પેચ વિકલ્પો @@ -227,5 +229,4 @@ morphe_header_custom_dark.png સેટિંગ્સ શોધો કોઈ પરિણામ મળ્યું નથી. - ડિફૉલ્ટ diff --git a/app/src/main/res/values-hi-rIN/plurals.xml b/app/src/main/res/values-hi-rIN/plurals.xml index a6ff3bb87..0077f10d7 100644 --- a/app/src/main/res/values-hi-rIN/plurals.xml +++ b/app/src/main/res/values-hi-rIN/plurals.xml @@ -17,8 +17,8 @@ %s पैकेज - अनुप्रयोग %s पटच को चलाएं - अनुप्रयोग %s पटच को चलाएं + %s पैच निष्पादित किया गया + %s पैच निष्पादित किए गए %s एपीके फाइल @@ -36,4 +36,12 @@ %s पटच चुना गया %s पटच चुने गए + + %s ऐप दिखाएं + %s ऐप्स दिखाएं + + + %s छिपा ऐप + %s छिपे हुए ऐप + diff --git a/app/src/main/res/values-hi-rIN/strings.xml b/app/src/main/res/values-hi-rIN/strings.xml index 071ecfc96..705e2da91 100644 --- a/app/src/main/res/values-hi-rIN/strings.xml +++ b/app/src/main/res/values-hi-rIN/strings.xml @@ -32,6 +32,8 @@ कोई ऐप्स उपलब्ध नहीं हैं पैच स्रोत जोड़ें या %s में मौजूदा स्रोत सक्षम करें कोई ऐप्स \"%1$s\" से मेल नहीं खाते हैं + सभी ऐप्स छिपे हुए हैं + आपने होम स्क्रीन से सभी ऐप्स छिपा दिए हैं आप किस ऐप को पैच करना चाहते हैं? @@ -91,10 +93,14 @@ पैच करने के लिए <b>%s</b>, अपatched एपीके की आवश्यकता है: सुझाया गया अपडेट किए बिना + असंगत हाँ, मुझे APK ढूंढने में मदद करें नहीं, मेरे पास पहले से ही एक एपीके है प्रतिलिपि APK (v%s) का उपयोग करें कोई प्रतिलिपि APK नहीं है। पट्च करना पुनः APK फ़ाइल का चयन करना आवश्यक होगा + Android %1$s+ की आवश्यकता है + इस डिवाइस पर समर्थित नहीं है + <b>%1$s</b> के पास Android %2$d (API %3$d) के लिए कोई समर्थित संस्करण नहीं है। सभी घोषित संस्करणों के लिए उच्च Android संस्करण की आवश्यकता है। ओरिजनल APK डाउनलोड करें निर्देश: @@ -221,6 +227,7 @@ पथचों की नई संस्करण उपलब्ध है। अपने एप्लिकेशन को पुनः पथच करें नई सुधार और त्रुटि प्राप्त करने के लिए। एप्लिकेशन उन्मोदन किया गया इस ऐप को बाहर से उन्मोदन किया गया था। मॉर्फी में पुनः स्थापित करें कार्यक्षमता पुनः प्राप्त करने के लिए + APK आकार रूट मोड के लिए, पैचिंग शुरू करने से पहले आपके डिवाइस पर APK को इंस्टॉल करना आवश्यक है। श्ळ लेरे सेप्नि साधार्य लैि लुन्यिानि ला @@ -308,16 +315,13 @@ Failed to execute %s Patched app saved for later Failed to save patched app - एपीके फाइल डाउनलोड करें इस प्लगइन के लिए आगे बढ़ने के लिए उपयोगकर्ता इंटरैक्शन आवश्यक है। पैचर प्रक्रिया कोड %1$s के साथ समाप्त हुई सफलतापूर्वक इंस्टॉल हो गया अप्लिकेशन को तैयार करना असफल रहा: %s - इंस्टॉलेशन पूर्ण नहीं हुई. सिस्टम इंस्टॉलर डायलॉग को जाँचें और पुन: प्रयास करें। एपीके सहेजा गया \"पैच किया गया ऐप एक्सपोर्ट करने में विफल।\" हटाया गया सहेजा गया पैच किया हुआ ऐप - हटाया गया सहेजा गया कॉपी ऐप को खोलने से पहले इंस्टॉल करें। Package name %s को माउंट करने में विफल @@ -337,6 +341,10 @@ ग्रिड कण कुछ + यादृच्छिक + लॉन्च पर + दैनिक + हर 3 दिन में परालैक्स प्रभाव डिवाइस को झुकाने पर पृष्ठभूमि को मधयम गति से बदलना @@ -353,6 +361,10 @@ वर्तमान भाषा कुछ भाषाओं के लिए अनुवाद या अपूर्ण अनुवाद उपलब्ध हो सकते हैं नई भाषाओं को अनुवाद करने के लिए या मौजूदा अनुवाद को सुधारने के लिए, %s पर जाएं + + मुखपृष्ठ + अभिवादन वाक्यांश + मुखपृष्ठ पर अभिवादन संदेश दिखाएँ एप्लिकेशन आइकॉन डिफॉल्ट @@ -399,8 +411,17 @@ अनुमोदित करना जटिल मॉर्फ पट्टी सेटिंग्स और कस्टमाइजेशन विकल्प एक्सपर्ट मोड को अनुमोदित करना? एक्सपर्ट मोड पट्चों को लागू करने के तरीके पर अधिक नियंत्रण देता है, लेकिन एक्सपर्ट मोड में सेटिंग्स गलत करने से अप्लिकेशन में काम नहीं करने का खतरा हो सकता है - अनदरसरी नेटिव लाइब्रेरीज हटाएं - पुराने डिवाइस आर्किटेक्चर के लिए अनदरसरी नेटिव लाइब्रेरीज हटाएं + डिवाइस आर्किटेक्चर के लिए अनुकूलित करें + "सादे APK के लिए, पैचिंग के बाद असमर्थित आर्किटेक्चर के लिए मूल लाइब्रेरीज़ को हटा देता है। +असमर्थित CPU आर्किटेक्चर, लोकेल और स्क्रीन घनत्व के लिए स्प्लिट APK मॉड्यूल को मर्ज करते समय छोड़ दें।" + + बाइटकोड प्रोसेसिंग मोड + पैचिंग के दौरान बाइटकोड कैसे प्रोसेस किया जाता है, यह नियंत्रित करता है। पैचिंग की गति, मेमोरी उपयोग और आउटपुट APK आकार को प्रभावित करता है + तेज़ (अनुशंसित) + तेज़ + तेज़ पैचिंग और कम मेमोरी का उपयोग, लेकिन APK का आकार बड़ा होगा। कम RAM या पुराने उपकरणों के लिए अनुशंसित + पूर्ण + धीमी पैचिंग, विरासत व्यवहार को दोहराती है। केवल तभी उपयोग करें जब तेज़ मोड में समस्या हो GitHub PAT प्रकाशन अनुरोध स्रोतों के लिए आवश्यक @@ -510,13 +531,7 @@ morphe_header_custom_dark.png इंस्टॉल किया गया शिज़ुकू वर्जन समर्थित नहीं है Open Shizuku इंस्टॉलेशन विफल रही. पुन: प्रयास करें या अलग इंस्टॉलर का उपयोग करें। - एंड्रॉइड द्वारा इंस्टॉलेशन ब्लॉक की गई. प्ले प्रोटेक्ट या सुरक्षा सेटिंग्स को जाँचें। इस पैकेज नाम के साथ अन्य ऐप पहले से ही इंस्टॉल किया गया है. आगे बढ़ने से पहले इसे अनइंस्टॉल करें। - एपीके इस डिवाइस या एंड्रॉइड वर्जन के साथ संगत नहीं है - एपीके अमान्य या क्षतिग्रस्त है - इंस्टॉलेशन के लिए पर्याप्त स्टोरेज स्पेस नहीं है - इंस्टॉलेशन टाइमआउट हुई. पुन: प्रयास करें - इंस्टॉलेशन रद्द की गई %1$s खोला जा रहा है %1$s ने इंस्टॉलेशन की पुष्टि नहीं की. अन्य ऐप को जाँचें और पुन: प्रयास करें %1$s ने सफल इंस्टॉलेशन रिपोर्ट की @@ -547,6 +562,7 @@ morphe_header_custom_dark.png यूज़रनेम (एलियास) पासवर्ड आयम + कीस्टोर प्रारूप गलत कीस्टोर क्रेडेंशियल कीस्टोर आयम किया गया कीस्टोर आयम करने में विफल @@ -554,6 +570,7 @@ morphe_header_custom_dark.png वर्तमान कीस्टोर एक्सपोर्ट करें एक्सपोर्ट करने के लिए कीस्टोर उपलब्ध नहीं है कीस्टोर एक्सपोर्ट किया गया + कीस्टोर एक्सपोर्ट करने में विफल। पासवर्ड दिखाएं पासवर्ड छिपाएं @@ -682,7 +699,8 @@ morphe_header_custom_dark.png अनुमति दें विवरण छुपायें - डिफ़ॉल्ट + छिपा हुआ + अनहाइड करें लॉग्स स्रोत इंस्टॉल हो रहा है… diff --git a/app/src/main/res/values-hr-rHR/plurals.xml b/app/src/main/res/values-hr-rHR/plurals.xml index 5fcbbecf0..8220dc5fb 100644 --- a/app/src/main/res/values-hr-rHR/plurals.xml +++ b/app/src/main/res/values-hr-rHR/plurals.xml @@ -20,11 +20,6 @@ %s paketa %s paketa - - Izvrši %s patch - Izvrši %s patcheva - Izvrši %s popravke - %s aplikacija %s aplikacija diff --git a/app/src/main/res/values-hr-rHR/strings.xml b/app/src/main/res/values-hr-rHR/strings.xml index 50385ccd1..74ee06e70 100644 --- a/app/src/main/res/values-hr-rHR/strings.xml +++ b/app/src/main/res/values-hr-rHR/strings.xml @@ -301,16 +301,13 @@ Za najbolje rezultate, patchanje ove aplikacija preporučuje <b>cijeli AP Nije moguće primijeniti %s Patchana aplikacija spremljena za kasnije Spremanje patchane aplikacije nije uspjelo - Preuzmite aplikaciju Za nastavak rada s ovim dodatkom potrebna je interakcija korisnika Proces patchera završio se kodom %1$s Uspješno instalirano Nije moguće instalirati aplikaciju: %s - Instalacija nije završena. Provjerite dijalog instalacijskog programa sustava i pokušajte ponovno. Aplikacija je spremljena Izvoz patchane aplikacije nije uspio Uklonjena spremljena patchirana aplikacija - Spremljena kopija izbrisana Instalirajte aplikaciju prije otvaranja Naziv paketa Nije moguće učitati: %s @@ -346,6 +343,7 @@ Za najbolje rezultate, patchanje ove aplikacija preporučuje <b>cijeli AP Trenutni jezik Prijevodi za neke jezike mogu nedostajati ili biti nepotpuni Da biste preveli nove jezike ili poboljšali postojeće prijevode, posjetite %s + Ikona aplikacije Zadano @@ -392,8 +390,7 @@ Za najbolje rezultate, patchanje ove aplikacija preporučuje <b>cijeli AP Omogući napredne postavke i opcije za prilagodbu za složeno Morphe ispravljanje i prilagodbe Napredni način? Napredni način omogućuje više kontrole nad primjenom ispravljava, ali pogrešno konfiguriranje postavki u naprednom načinu može rezultirati nefunkcionalnom aplikacijom - Uklonite nekorištene izvorne biblioteke - Izbrišite izvorne biblioteke za nepodržane arhitekture procesora iz patchanih aplikacija + GitHub PAT Potrebno za izvlačenje zahtjeva @@ -503,13 +500,7 @@ Dimenzije slike moraju biti sljedeće: Instalirana verzija Shizuku nije podržana Otvorite Shizuku Instalacija nije uspjela. Pokušajte ponovno ili promjenite instalacijski program. - Instalacija je blokirana od strane Androida. Provjerite Play Protect ili postavke sigurnosti Već je instalirana druga aplikacija s ovim nazivom paketa. Deinstalirajte je prije nastavka. - Aplikacija nije kompatibilna s ovim uređajem ili verzijom Androida - Aplikacija je nevažeća ili oštećena - Nema dovoljno prostora za instalaciju. - Vremensko ograničenje instalacije je isteklo. Pokušajte ponovno. - Instalacija je otkazana Otvaranje %1$s… %1$s nije potvrdio instalaciju. Provjerite drugu aplikaciju i pokušajte ponovno. %1$s je prijavio uspješnu instalaciju @@ -674,7 +665,6 @@ Dimenzije slike moraju biti sljedeće: Dopusti Detalji Sakrij - Zadano Zapisnici Instaliranje… Učitavanje… diff --git a/app/src/main/res/values-hu-rHU/plurals.xml b/app/src/main/res/values-hu-rHU/plurals.xml index 7ee6ccb66..4c04867df 100644 --- a/app/src/main/res/values-hu-rHU/plurals.xml +++ b/app/src/main/res/values-hu-rHU/plurals.xml @@ -18,7 +18,7 @@ %s módosítás végrehajtása - %s módosítás végrehajtása + %s módosítások végrehajtása %s APK fájl @@ -36,4 +36,12 @@ %s módosítás kiválasztva %s módosítás kiválasztva + + Mutassa a(z) %s alkalmazást + Mutassa a(z) %s alkalmazásokat + + + %s rejtett alkalmazás + %s rejtett alkalmazások + diff --git a/app/src/main/res/values-hu-rHU/strings.xml b/app/src/main/res/values-hu-rHU/strings.xml index 3b3587046..0c7606856 100644 --- a/app/src/main/res/values-hu-rHU/strings.xml +++ b/app/src/main/res/values-hu-rHU/strings.xml @@ -30,8 +30,10 @@ Források betöltése... Alkalmazások keresése Nincsenek elérhető alkalmazások - Add patch forrását adhatja hozzá, vagy engedélyezhet egy meglévő forrást a(z) %s alatt - Nincsenek egyező alkalmazások a(z) \"%1$s\" kifejezésre + Adj hozzá egy módosításforrást, vagy kapcsolj be egyet itt: %s + Nincs találat erre: „%1$s” + Minden alkalmazás el van rejtve + Minden alkalmazást elrejtettél a kezdőképernyőről Melyik alkalmazást szeretnéd módosítani? @@ -53,63 +55,67 @@ Koffein és jó módosítások hajtják - Módosítások forrásai + Módosításforrások Megnyitás böngészőben Hiba történt az URL megnyitásakor - Előzetes kiadású módosítások használata - Korai hozzáférés az új, kísérleti módosítások verzióihoz + Kiadás előtti módosítások használata + Kísérleti módosítások korai elérése Kísérleti alkalmazásverziók használata Kísérleti alkalmazások módosítása, ha elérhetők Ez a forrás már hozzá lett adva Megjelenítendő név - Patchforrás átnevezése - Ezzel a névvel már létezik patchforrás - Nem sikerült frissíteni ezt a patchforrást + Módosításforrás átnevezése + Ezzel a névvel már létezik módosításforrás + A módosításforrás frissítése sikertelen Bármelyik verzió Bármelyik csomag - Patchforrás hozzáadása + Módosításforrás hozzáadása Törlöd a forrást: \"%s\"? Ez nem visszavonható Távoli Forrás URL Példák: Helyi - Válaszd ki a patchforrás fájlját - Válassz ki egy .mpp patchforrásfájlt a tárhelyről + Forrásfájl kiválasztása + Válassz ki egy .mpp módosításforrást a tárhelyről Fájl megváltoztatása Előretelepített - A frissítés sikeres + Sikeres frissítés Nincs elérhető frissítés Nem sikerült letölteni a forrásokat: %s A \'%1$s\' letöltése sikertelen: %2$s - Nem sikerült frissíteni ezt: \'%1$s\' - A módosítás JSON fájlja nem található a beállított URL-n + Nem sikerült frissíteni: „%1$s”. A módosításokat tartalmazó JSON-fájl nem található a megadott URL-címen. Szakértő - Engedélyezze a szakértői módot a Beállításokban a javítás manuális hozzáadásához + A módosítás manuális hozzáadásához engedélyezd a szakértői módot a beállításokban. - Szükséged van segítségre az eredeti, módosítások nélküli APK megtalálásához? - A(z) <b>%s</b> módosításához szükséges egy módosítás nélküli APK, amely verziója: - A(z) <b>%s</b> módosításához szükséges egy módosítás nélküli APK, amelyek verziói: + Segítségre van szükséged az eredeti, módosítatlan APK megtalálásához? + ​A(z) <b>%s</b> módosításához a következő módosítatlan APK-verzióra van szükséged: + ​A(z) <b>%s</b> módosításához a következő módosítatlan APK-verziók egyikére van szükséged: Javasolt Nem módosított + Nem kompatibilis Igen, segíts megtalálni az APK-t Nem, már megvan az APK - Használjon mentett APK-t (v%s) - Nincs mentett APK. A módosításhoz újra ki kell választani az APK fájlt. + Mentett APK használata (v%s) + Nincs mentett APK. A módosításhoz újra ki kell választanod az APK-fájlt. + Android %1$s+ verziót igényel + Nem támogatott ezen az eszközen + A(z) <b>%1$s</b> nem rendelkezik az Android %2$d (API %3$d) verziójával kompatibilis verziókkal. A megadott összes verzió magasabb Android verziót igényel - Az eredeti APK fájl letöltése + Az eredeti APK letöltése Útmutató: - Nyomd meg a(z) \'%1$s\' gombot alul - Görgess lefelé a weblapon, és nyomd meg a - A weblapon nyomd meg a letöltést, és ne itt 😊 + Koppints az alábbi „%1$s” gombra + Görgess le a weboldalon, majd nyomd meg: + ​A weboldalon nyomj a letöltésre, ne itt 😊 Várd meg, amíg a letöltés befejeződik, és <b>ne telepítsd</b> az APK-t Várd meg, amíg a letöltés befejeződik, majd <b>telepítsd az APK-t</b> - ​A letöltés után térj vissza a Morphe alkalmazásba. + A letöltés után térj vissza a Morphe alkalmazásba Az eredeti APK telepítése után térj vissza Morphe alkalmazásba - Ugrás az APKMirror.com oldalra + Tovább az APKMirror.com oldalra Válaszd ki a letöltött APK-t Válaszd ki a most letöltött <b>%1$s</b> APK fájlt - APK fájl megnyitása + APK megnyitása Ajánlott verzió Kiválasztott verzió APK kiválasztása @@ -221,6 +227,7 @@ A legjobb eredmény érdekében az alkalmazás javasolja a <b>teljes APK&l Egy újabb módosításverzió elérhető. Módosítsa újra az alkalmazását, hogy megkapja a legújabb fejlesztéseket és javításokat Az alkalmazást eltávolították Ezt az alkalmazást a Morphe-n kívül törölték. Módosítsa újra a funkcionalitás helyreállításához + APK méret A root módhoz az eredeti APK-nak telepítve kell lennie az eszközön a módosítás előtt A GmsCore támogatási patch nem elérhető root módban @@ -308,16 +315,13 @@ A legjobb eredmény érdekében az alkalmazás javasolja a <b>teljes APK&l Nem sikerült alkalmazni: %s A módosított alkalmazás mentve későbbre A módosított alkalmazás mentése sikertelen - APK fájl letöltése A bővítmény használatához felhasználói beavatkozásra van szükség A módosítási folyamat %1$s kóddal kilépett Sikeresen telepítve Nem sikerült telepíteni az alkalmazást: %s - A telepítés nem fejeződött be. Ellenőrizze a rendszertelepítő párbeszédpaneljét, majd próbálkozzon újra APK mentve Nem sikerült exportálni a módosított alkalmazást A módosított alkalmazás mentése eltávolítva - Mentett másolat eltávolítva Telepítse az alkalmazást, mielőtt megnyitná Csomagnév Nem sikerült csatlakoztatni: %s @@ -337,6 +341,10 @@ A legjobb eredmény érdekében az alkalmazás javasolja a <b>teljes APK&l Rács Részecskék Egyik sem + Véletlenszerű + Indításkor + Napi + Minden 3. napon Parallax effektus Sima háttérváltás a készülék döntésekor @@ -353,6 +361,10 @@ A legjobb eredmény érdekében az alkalmazás javasolja a <b>teljes APK&l Jelenlegi nyelv Egyes nyelvek fordításai hiányozhatnak vagy hiányosak lehetnek Új nyelvek fordításához vagy a meglévő fordítások javításához látogasson el a következő oldalra: %s + + Kezdőképernyő + Üdvözlő mondatok + Üdvözlő üzenet megjelenítése a kezdőképernyőn Alkalmazásikon Alapértelmezett @@ -399,8 +411,17 @@ A legjobb eredmény érdekében az alkalmazás javasolja a <b>teljes APK&l Engedélyezi a Morphe komplex módosítási beállításait és a testreszabási lehetőségeket Engedélyezi a szakértői módot? A szakértői mód nagyobb ellenőrzést biztosít a módosítások alkalmazása felett, de ebben a módban a beállítások helytelen konfigurálása az alkalmazás működésképtelenségét eredményezheti - Távolítsa el a nem használt natív könyvtárakat - Törölje a nem támogatott CPU-architektúrákhoz tartozó natív könyvtárakat a módosított alkalmazásokból + Optimalizálj a készülék architektúrájához + "Hagyja ki a felosztott APK modulokat a nem támogatott CPU architektúrák, nyelvek és képernyőfelbontások számára az egyesítés során. +Plain APK-k esetén a javítást követően eltávolítja a nem támogatott architektúrákhoz tartozó natív könyvtárakat" + + Bájtkód feldolgozási mód + Szabályozza, hogy a bájtkódot hogyan dolgozzák fel a javítás során. Hatással van a javítás sebességére, a memória használatára és a kimeneti APK méretére + Gyors (Ajánlott) + Gyors + Gyorsabb javítás és alacsonyabb memóriahasználat, nagyobb APK méret árán. Ajánlott alacsony RAM-mal vagy régebbi eszközök esetén + Teljes + Lassabb javítás, a régi viselkedést tükrözi. Csak akkor használja, ha a gyors mód problémákat okoz GitHub PAT Kötelező a pull request forrásokhoz @@ -510,13 +531,7 @@ A képek méreteinek a következőknek kell lennie: A telepített Shizuku verzió nem támogatott Shizuku megnyitása A telepítés sikertelen volt. Próbálja újra, vagy váltson másik telepítőre - A telepítést az Android blokkolta. Ellenőrizze a Play Protectet vagy a biztonsági beállításokat Ezzel a csomagnévvel már telepítve van egy másik alkalmazás. Távolítsa el, mielőtt folytatná - Az APK nem kompatibilis ezzel az eszközzel vagy Android-verzióval - Az APK érvénytelen vagy sérült - Nincs elegendő tárhely a telepítéshez - A telepítés időtúllépés miatt megszakadt. Próbálja újra - Telepítés megszakítva %1$s megnyitása… %1$s nem erősítette meg a telepítést. Ellenőrizze a másik alkalmazást, és próbálja újra %1$s sikeres telepítést jelentett @@ -547,6 +562,7 @@ A képek méreteinek a következőknek kell lennie: Felhasználónév (Alias) Jelszó Importálás + Kulcstároló formátum Hibás kulcstároló hitelesítőadatok Kulcstároló importálva Nem sikerült importálni a kulcstárolót @@ -554,6 +570,7 @@ A képek méreteinek a következőknek kell lennie: Az aktuális kulcstároló exportálása Nincs exportálható kulcstároló Kulcstároló sikeresen exportálva + Sikertelen kulcstár export Jelszó megjelenítése Jelszó elrejtése @@ -683,7 +700,8 @@ Forrás: #%2$s Engedélyezés Részletek Elrejtés - Alapértelmezett + Rejtett + Feloldás Naplók Forrás Telepítés… diff --git a/app/src/main/res/values-hy-rAM/strings.xml b/app/src/main/res/values-hy-rAM/strings.xml index 180ea1689..48f97ef60 100644 --- a/app/src/main/res/values-hy-rAM/strings.xml +++ b/app/src/main/res/values-hy-rAM/strings.xml @@ -39,10 +39,12 @@ + + diff --git a/app/src/main/res/values-in-rID/plurals.xml b/app/src/main/res/values-in-rID/plurals.xml index 80591fbd9..e9f9a97e9 100644 --- a/app/src/main/res/values-in-rID/plurals.xml +++ b/app/src/main/res/values-in-rID/plurals.xml @@ -27,4 +27,10 @@ %s tambalan dipilih + + Tampilkan %s aplikasi + + + %s aplikasi disembunyikan + diff --git a/app/src/main/res/values-in-rID/strings.xml b/app/src/main/res/values-in-rID/strings.xml index 1808e4413..78f726e2a 100644 --- a/app/src/main/res/values-in-rID/strings.xml +++ b/app/src/main/res/values-in-rID/strings.xml @@ -32,6 +32,8 @@ Tidak ada aplikasi yang tersedia Tambahkan sumber tambalan atau aktifkan yang sudah ada di %s Tidak ada aplikasi yang cocok \"%1$s\" + Semua aplikasi disembunyikan + Anda telah menyembunyikan semua aplikasi dari layar utama Aplikasi apa yang ingin Anda tambal? @@ -91,10 +93,14 @@ Untuk menambal <b>%s</b>, Anda membutuhkan APK yang belum ditambal dengan versi: Direkomendasikan Tidak ditambal + Tidak kompatibel Ya, bantu saya mencari APK Tidak, saya sudah memiliki APK Gunakan APK yang disimpan (v%s) Tidak ada APK yang disimpan. Penambalan akan memerlukan memilih berkas APK lagi + Memerlukan Android %1$s+ + Tidak didukung di perangkat ini + <b>%1$s</b> tidak memiliki versi yang didukung untuk Android %2$d (API %3$d). Semua versi yang dideklarasikan memerlukan versi Android yang lebih tinggi Unduh APK asli Petunjuk: @@ -222,6 +228,7 @@ Harap bersiap untuk perilaku aplikasi yang tidak biasa atau bug yang belum terid Versi tambalan lebih baru tersedia. Tambal ulang aplikasi Anda untuk mendapatkan pembaruan terbaru dan perbaikan Aplikasi telah dihapus Aplikasi ini telah dihapus di luar Morphe. Tambal ulang untuk memulihkan fungsinya + Ukuran APK Mode root memerlukan APK asli untuk dipasang di perangkat Anda sebelum menambal Dukungan tambalan GmsCore dikecualikan dalam mode root @@ -309,16 +316,13 @@ Harap bersiap untuk perilaku aplikasi yang tidak biasa atau bug yang belum terid Gagal menerapkan %s Aplikasi yang ditambal disimpan untuk nanti Gagal menyimpan aplikasi yang ditambal - Unduh berkas APK Interaksi pengguna diperlukan untuk melanjutkan plugin ini Proses Penambal dihentikan dengan kode %1$s Berhasil dipasang Gagal memasang aplikasi: %s - Pemasangan tidak selesai. Periksa dialog pembatalan dan coba lagi APK Tersimpan Gagal mengekspor aplikasi hasil patch Entri aplikasi patch tersimpan dihapus - Salinan tersimpan dihapus Pasang aplikasi sebelum membukanya Nama paket Gagal mount: %s @@ -338,6 +342,10 @@ Harap bersiap untuk perilaku aplikasi yang tidak biasa atau bug yang belum terid Jaring Partikel Tidak ada + Acak + Saat diluncurkan + Harian + Setiap 3 hari Efek Paralaks Perpindahan latar belakang yang halus saat menggeser perangkat @@ -354,6 +362,10 @@ Harap bersiap untuk perilaku aplikasi yang tidak biasa atau bug yang belum terid Bahasa saat ini Terjemahan untuk beberapa bahasa mungkin belum tersedia atau belum lengkap. Untuk menerjemahkan bahasa baru atau meningkatkan terjemahan yang ada, kunjungi %s + + Layar beranda + Frasa sapaan + Tampilkan pesan sapaan di layar beranda Ikon aplikasi Bawaan @@ -400,8 +412,17 @@ Harap bersiap untuk perilaku aplikasi yang tidak biasa atau bug yang belum terid Aktifkan pengaturan penambalan Morphe yang kompleks dan pilihan kustomisasi Aktifkan mode Ahli? Mode ahli memberikan kontrol yang lebih banyak terhadap bagaimana penambalan diterapkan, tetapi pengaturan yang salah dalam mode ahli dapat menyebabkan aplikasi tidak berfungsi - Hapus pustaka bawaan yang tidak digunakan - Hapus pustaka bawaan untuk arsitektur CPU yang tidak didukung dari aplikasi yang ditambal + Optimalkan untuk arsitektur perangkat + "Lewati modul APK terpisah untuk arsitektur CPU, lokal, dan kerapatan layar yang tidak didukung selama penggabungan. +Untuk APK biasa, hapus pustaka asli untuk arsitektur yang tidak didukung setelah menambal" + + Mode pemrosesan bytecode + Mengontrol bagaimana bytecode diproses selama penambalan. Memengaruhi kecepatan penambalan, penggunaan memori, dan ukuran APK yang dihasilkan + Cepat (Direkomendasikan) + Cepat + Penambalan lebih cepat dan penggunaan memori lebih rendah, namun dengan ukuran APK yang lebih besar. Direkomendasikan untuk perangkat dengan RAM rendah atau perangkat lama + Lengkap + Penambalan yang lebih lambat, meniru perilaku versi lama. Gunakan hanya jika mode cepat menimbulkan masalah PAT GitHub Dibutuhkan untuk permintaan tarik sumber @@ -433,16 +454,16 @@ Harap bersiap untuk perilaku aplikasi yang tidak biasa atau bug yang belum terid Nama aplikasi Masukkan nama khusus Ikon khusus - "Folder dengan gambar untuk digunakan sebagai ikon khusus. + "Folder yang berisi gambar untuk digunakan sebagai ikon khusus. -Folder harus berisi satu atau beberapa folder berikut, tergantung pada DPI perangkat: +Folder harus berisi satu atau lebih folder berikut, tergantung pada DPI perangkat: - mipmap-mdpi - mipmap-hdpi - mipmap-xhdpi - mipmap-xxhdpi - mipmap-xxxhdpi -Setiap folder harus berisi semua berkas berikut: +Setiap folder harus berisi semua file berikut: morphe_adaptive_background_custom.png morphe_adaptive_foreground_custom.png @@ -451,15 +472,15 @@ Dimensi gambar harus sebagai berikut: - mipmap-hdpi: 162x162 px - mipmap-xhdpi: 216x216 px - mipmap-xxhdpi: 324x324 px -- mipmap-xxxhdpi: 432x432 px +- mipmap-xxxhdpi: 432x432 piksel -Sebagai pilihan, jalur yang berisi folder 'drawable' dengan berkas ikon monokrom: +Opsional, jalur tersebut berisi folder ‘drawable’ dengan file ikon monokrom: morphe_adaptive_monochrome_custom.xml -Sebagai pilihan, jalur yang berisi ikon pemberitahuan dalam salah satu format berikut: -- Gambar vektor XML di 'drawable': - morphe_notification_icon_custom.xml -- Gambar raster PNG di folder 'drawable-dpi': +Opsional, jalur yang berisi ikon notifikasi dalam salah satu format berikut: +- Gambar vektor XML di dalam ‘drawable’: + morphe_notification_icon_custom.xml +- Gambar raster PNG di folder ‘drawable-dpi’: drawable-mdpi/morphe_notification_icon_custom.png (24x24 px) drawable-hdpi/morphe_notification_icon_custom.png (36x36 px) drawable-xhdpi/morphe_notification_icon_custom.png (48x48 px) @@ -468,23 +489,23 @@ Sebagai pilihan, jalur yang berisi ikon pemberitahuan dalam salah satu format be Logo header khusus Ubah header logo - "Folder berisi gambar yang akan digunakan sebagai logo header khusus. + "Folder yang berisi gambar untuk digunakan sebagai logo header khusus. -Folder tersebut harus berisi satu atau lebih dari folder berikut, tergantung pada DPI perangkat: +Folder tersebut harus berisi satu atau lebih folder berikut, tergantung pada DPI perangkat: - drawable-hdpi - drawable-xhdpi - drawable-xxhdpi - drawable-xxxhdpi -Setiap folder harus berisi semua berkas berikut: +Setiap folder harus berisi semua file berikut: morphe_header_custom_light.png morphe_header_custom_dark.png -Ukuran gambar harus sebagai berikut: -- drawable-hdpi: 194x72 piksel -- drawable-xhdpi: 258x96 piksel -- drawable-xxhdpi: 387x144 piksel -- drawable-xxxhdpi: 512x192 piksel" +Dimensi gambar harus sebagai berikut: +- drawable-hdpi: 194x72 px +- drawable-xhdpi: 258x96 px +- drawable-xxhdpi: 387x144 px +- drawable-xxxhdpi: 512x192 px" Sembunyikan fitur Shorts Sembunyikan pintasan aplikasi Shorts @@ -511,13 +532,7 @@ Ukuran gambar harus sebagai berikut: Versi Shizuku yang terpasang tidak didukung Buka Shizuku Pemasangan gagal. Coba lagi atau ganti pemasang - Pemasangan diblokir oleh Android. Periksa Play Protect atau pengaturan keamanan Aplikasi lain dengan nama paket ini sudah terpasang. Copot pemasangannya terlebih dahulu sebelum melanjutkan - APK tidak kompatibel dengan perangkat ini atau versi Android - APK tidak sah atau rusak - Ruang penyimpanan tidak cukup untuk pemasangan - Waktu pemasangan habis. Coba lagi - Pemasangan dibatalkan Membuka %1$s… %1$s tidak mengonfirmasi pemasangan. Periksa aplikasi lain dan coba lagi %1$s melaporkan pemasangan berhasil @@ -548,6 +563,7 @@ Ukuran gambar harus sebagai berikut: Nama pengguna (Alias) Kata sandi Impor + Format Keystore Kredensial keystore tidak sah Keystore diimpor Gagal mengimpor keystore @@ -555,6 +571,7 @@ Ukuran gambar harus sebagai berikut: Ekspor keystore saat ini Tidak ada keystore yang tersedia untuk diekspor Keystore diekspor + Gagal mengekspor keystore Tampilkan kata sandi Sembunyikan kata sandi @@ -683,7 +700,8 @@ Ukuran gambar harus sebagai berikut: Izinkan Rincian Sembunyikan - Bawaan + Disembunyikan + Tampilkan Catatan Sumber Memasang… diff --git a/app/src/main/res/values-is-rIS/strings.xml b/app/src/main/res/values-is-rIS/strings.xml index 180ea1689..974e0a475 100644 --- a/app/src/main/res/values-is-rIS/strings.xml +++ b/app/src/main/res/values-is-rIS/strings.xml @@ -1,6 +1,7 @@ + ไทย @@ -39,10 +40,12 @@ + + diff --git a/app/src/main/res/values-it-rIT/plurals.xml b/app/src/main/res/values-it-rIT/plurals.xml index 9cdc39aa6..bab4292ea 100644 --- a/app/src/main/res/values-it-rIT/plurals.xml +++ b/app/src/main/res/values-it-rIT/plurals.xml @@ -17,8 +17,8 @@ %s pacchetti - Esegui %s patch - Esegui %s patch + Patch %s eseguito + Patch %s eseguiti %s file APK @@ -36,4 +36,12 @@ %s patch selezionata %s patch selezionate + + Mostra %s app + Mostra %s apps + + + %s app nascosta + %s app nascoste + diff --git a/app/src/main/res/values-it-rIT/strings.xml b/app/src/main/res/values-it-rIT/strings.xml index f3b23a75c..2a45f01fb 100644 --- a/app/src/main/res/values-it-rIT/strings.xml +++ b/app/src/main/res/values-it-rIT/strings.xml @@ -12,7 +12,7 @@ App nascoste Tocca per mostrare Bug di Android 11 - È necessario concedere in anticipo il permesso di installazione dell\'app per evitare un bug con il sistema operativo Android 11 che potrebbe influire negativamente sull\'esperienza dell\'utente. + È necessario concedere in anticipo il permesso per l\'installazione dell\'app per evitare un bug con il sistema operativo Android 11 che potrebbe influire negativamente sull\'esperienza dell\'utente. Aggiungi fonte Vuoi aggiungere questa fonte delle patch a Morphe? Aggiungi solo fonti da fonti di cui ti fidi @@ -32,6 +32,8 @@ Nessuna app disponibile Aggiungi una sorgente delle patch o abilita una esistente in %s Nessuna app corrisponde a \"%1$s\" + Tutte le app sono nascoste + Hai nascosto tutte le app dalla schermata Home Quale app vuoi patchare? @@ -91,10 +93,14 @@ Per patchare <b>%s</b>, è necessario un APK non patchato con le versioni: Consigliata Non patchato + Incompatibile Sì, aiutami a trovare un APK No, ho già un APK Usa l\'APK salvato (v%s) Nessun APK salvato. Il patching richiederà la selezione del file APK + Richiede Android %1$s+ + Non supportato su questo dispositivo + <b>%1$s</b> non ha versioni supportate per Android %2$d (API %3$d). Tutte le versioni dichiarate richiedono una versione Android superiore Scarica l\'APK originale Istruzioni: @@ -114,7 +120,7 @@ Versione selezionata Seleziona l\'APK Premi il pulsante qui sotto per selezionare un file APK di qualsiasi app per il patching - Impossibile leggere il file. Controlla che \'Archiviazione esterna\' non sia limitata dalla batteria + Impossibile leggere il file. Controlla che \'Archiviazione esterna\' non sia limitata nell\'uso della batteria Il file selezionato non è un APK valido Impossibile aprire il file. Riprova @@ -221,6 +227,7 @@ Per ottenere i risultati migliori, questa app raccomanda di applicare una patch È disponibile una nuova versione delle patch. Ripatcha la tua app per ottenere gli ultimi miglioramenti e correttivi L\'app è stata disinstallata Questa app è stata disinstallata al di fuori di Morphe. Ripatchala per ripristinare la funzionalità + Dimensione APK La modalità di root richiede che l\'APK originale sia installato sul tuo dispositivo prima del patching Il supporto per GmsCore è escluso in modalità di root @@ -308,16 +315,13 @@ Per ottenere i risultati migliori, questa app raccomanda di applicare una patch Impossibile applicare %s App patchata salvata per dopo Impossibile salvare l\'app modificata - Scarica il file APK È richiesta l\'interazione dell\'utente per procedere con questo plugin Il processo del patcher è terminato con codice %1$s Installato correttamente Impossibile installare l\'app: %s - L\'installazione non è stata completata. Controlla la finestra di dialogo dell\'installatore di sistema e riprova APK salvato Impossibile esportare l\'app patchata App patchata salvata rimossa - Copia salvata rimossa Installa l\'app prima di aprirla Nome pacchetto Impossibile montare: %s @@ -337,6 +341,10 @@ Per ottenere i risultati migliori, questa app raccomanda di applicare una patch Griglia Particelle Nessuna + Casuale + All\'avvio + Giornaliero + Ogni 3 giorni Effetto parallasse Sfondo regolare che si sposta quando si inclina il dispositivo @@ -353,6 +361,10 @@ Per ottenere i risultati migliori, questa app raccomanda di applicare una patch Lingua attuale Le traduzioni per alcune lingue potrebbero essere incomplete o mancanti Per tradurre nuove lingue o migliorare le traduzioni esistenti, visita %s + + Schermata home + Frasi di saluto + Mostra un messaggio di saluto nella schermata principale Icona dell\'app Predefinita @@ -399,8 +411,17 @@ Per ottenere i risultati migliori, questa app raccomanda di applicare una patch Abilita le impostazioni avanzate e le opzioni di personalizzazione del patching di Morphe Attivare la modalità Esperto? La modalità esperto fornisce un maggiore controllo su come vengono applicate le patch, ma la configurazione errata delle impostazioni in modalità esperto può produrre un\'app non funzionante - Elimina le librerie native non utilizzate - Elimina i file delle librerie native dalle app patchate per le architetture CPU non supportate + Ottimizza per l\'architettura del dispositivo + "Salta i moduli degli APK divisi per architetture CPU, lingue e densità dello schermo non supportate durante l'unione. +Per gli APK standard, rimuove le librerie native per le architetture non supportate dopo il patching" + + Modalità di elaborazione bytecode + Controlla come il bytecode viene elaborato durante l\'applicazione delle patch. Influisce sulla velocità di patching, sull\'utilizzo della memoria e sulla dimensione APK di output + Veloce (Consigliata) + Veloce + Patching più veloce e minore utilizzo della memoria, a costo di un APK più grande. Consigliato per dispositivi con poca RAM o più vecchi + Completo + Patching più lento, replica il comportamento precedente. Utilizzare solo se la modalità rapida causa problemi Token di accesso personale di GitHub Richiesto per le sorgenti delle richieste di pull @@ -510,13 +531,7 @@ Le dimensioni delle immagini devono essere le seguenti: La versione di Shizuku installata non è supportata Apri Shizuku L\'installazione è fallita. Riprova o passa a un installer diverso. - L\'installazione è stata bloccata da Android. Controlla Play Protect o le impostazioni di sicurezza Un\'altra app con questo nome del pacchetto è già installata. Disinstallala prima di continuare - L\'APK non è compatibile con questo dispositivo o versione di Android - L\'APK non è valido o è corrotto - Non c\'è spazio di archiviazione sufficiente per l\'installazione - L\'installazione ha superato il tempo massimo. Riprova. - L\'installazione è stata annullata Apertura di %1$s in corso… %1$s non ha confermato l\'installazione. Controlla l\'altra app e riprova %1$s ha segnalato un\'installazione riuscita @@ -547,6 +562,7 @@ Le dimensioni delle immagini devono essere le seguenti: Nome utente (Alias) Password Importa + Formato Keystore Credenziali del keystore errate Keystore importato Importazione del keystore non riuscita @@ -554,6 +570,7 @@ Le dimensioni delle immagini devono essere le seguenti: Esporta il keystore corrente Nessun keystore disponibile per l\'esportazione Keystore esportato + Esportazione del keystore non riuscita Mostra la password Nascondi la password @@ -615,7 +632,7 @@ Le dimensioni delle immagini devono essere le seguenti: A proposito di Condividi il sito Condividi il sito ufficiale di Morphe - "Un progetto open-source per un patching moderna e semplificata delle app Android popolari, guidato dal feedback e dai contributi della community" + "Un progetto open-source per un patching moderno e semplificato delle app Android popolari, guidato dal feedback e dai contributi della community" Crediti Sviluppo attuale Sviluppo precedente @@ -682,7 +699,8 @@ Le dimensioni delle immagini devono essere le seguenti: Consenti Dettagli Nascondi - Predefinita + Nascosto + Mostra Tracciati Fonti Installazione in corso… diff --git a/app/src/main/res/values-iw-rIL/plurals.xml b/app/src/main/res/values-iw-rIL/plurals.xml index e322533a3..16f10f6cb 100644 --- a/app/src/main/res/values-iw-rIL/plurals.xml +++ b/app/src/main/res/values-iw-rIL/plurals.xml @@ -24,12 +24,6 @@ %s חבילות %s חבילות - - התבצע תיקון אחד - התבצעו שני תיקונים - התבצעו %s תיקונים - התבצעו %s תיקונים - קובץ APK אחד שני קבצי APK @@ -54,4 +48,16 @@ %s תיקונים נבחרו %s תיקונים נבחרו + + הצג אפליקציה %s + הצג %s אפליקציות + הצג %s אפליקציות + הצג %s אפליקציות + + + %s אפליקציה מוסתרת + %s אפליקציות מוסתרות + %s אפליקציות מוסתרות + %s אפליקציות מוסתרות + diff --git a/app/src/main/res/values-iw-rIL/strings.xml b/app/src/main/res/values-iw-rIL/strings.xml index df8a0355e..b13134b52 100644 --- a/app/src/main/res/values-iw-rIL/strings.xml +++ b/app/src/main/res/values-iw-rIL/strings.xml @@ -32,6 +32,8 @@ אין אפליקציות זמינות הוסף מקור לאפליקציה או הפעל מקור קיים ב-\"%s\" אף אפליקציה לא תואמת ל-\"%1$s\" + כל האפליקציות מוסתרות + הסתרת את כל האפליקציות מהמסך הראשי איזה אפליקציה אתה רוצה לתקן? @@ -91,10 +93,14 @@ לעדכן <b>%s</b>, צריך APK לא מעודכן של הגרסאות: מומלץ לא מתקן + לא תואם כן, עזור לי למצוא APK לא, אני כבר מחזיק APK השתמש באפאק שמור (גרסה %s) אין אפאק שמור. תיקון ידרש לבחור את קובץ האפאק שוב + דורש Android %1$s+‎ + לא נתמך במכשיר זה + <b>%1$s</b> אין לו גרסאות נתמכות עבור Android %2$d (API %3$d). כל הגרסאות המוצהרות דורשות גרסת Android גבוהה יותר הורד את ה-APK המקורי הוראות: @@ -160,6 +166,7 @@ התקנה עם טיקים מיושנים עשויה להוביל לאפליקציה שבורה עדכן & טקן מחסור בשטח דיסק + רק %1$.2f GB של שטח אחסון פנוי זמין. התקנת עדכונים דורשת לפחות %2$.2f GB של שטח פנוי כדי לעבוד כראוי. המשך עלול לגרום לשגיאת \"קובץ לא נמצא\" או APK פלט פגום Цвет \"изготовителя\" @@ -220,6 +227,7 @@ גרסה חדשה של תיקונים זמינה. תיקן את האפליקציה שלך כדי לקבל את התיקונים וההערות החדשים האפליקציה הוסרה האפליקציה הוסרה מחוץ למורפ. תיקנה כדי לשחזר את הפונקציונליות + גודל APK מוד רוט דורש את APK המקורי להתקין על מכשיר שלך לפני השחזור תיקון תמיכה ב-GmsCore אינו נכלל במצב Root @@ -307,16 +315,13 @@ נכשל להחיל %s אפליקציה מתוקנת נשמרה למועד מאוחר יותר נכשל לשמור אפליקציה מתוקנת - הורד קובץ APK נדרש אינטראקציה של המשתמש כדי להמשיך עם תוסף זה פועל הפייסר יצא עם קוד %1$s הותקן בהצלחה נכשל להתקין את האפליקציה: %s - ההתקנה לא הסתיימה. בדוק את דיאלוג ההתקנה של המערכת ונסה שוב APK נשמר כשל בייצוא האפליקציה המתוקנת אפליקציה מתוקנת שנשמרה הוסרה - עותק שמור הוסר התקן את האפליקציה לפני פתיחתה שם חבילה כשל בהרכבה: %s @@ -336,6 +341,10 @@ רשת חלקיקים אין + אקראי + בפתיחה + יומיומי + כל 3 ימים אפקט פארלקס הדגמה חלקה של רקע בעת טיפוס של המכשיר @@ -352,6 +361,10 @@ שפה נוכח תרגומים עבור חלק מהשצות עשויים או לא מושלמים לתרגם שפות חדשות או לשפר את התרגומים הקיימים, עלי לבקר %s + + מסך הבית + משפטי פתיחה + הצגת הודעת פתיחה על מסך הבית אייקון האפליקציה ברירת מחדל @@ -398,8 +411,17 @@ אפשר הגדרות תיקון מורפמה מורכבות וoptions אישוריות מותאמות אישית אפשר את режим מומחה? mode מומחה מספק יותר שליטה על איך התיקונים מופעלים, אך תצורתה של הגדרות ב-mode מומחה יכולה לגרום לאפליקציה שאינה פונקציונלית - הסר את קבצים נטוים - הסרת קבצים נטוים תאפשר מהיר יותר את הפיצ\'ח, אך ייתכן שלא תעבוד על מכשירים ישנים + לייעל עבור ארכיטקטורת ההתקן + "לדלג על מודולי APK מפולחים עבור ארכיטקטורות, שפות ואזורי מסך לא נתמכים במהלך האיחוד. +עבור APK רגילים, מסיר ספריות מקומיות עבור ארכיטקטורות לא נתמכות לאחר תיקון" + + מצב עיבוד bytecode + שולט באופן עיבוד ה-bytecode במהלך תיקון. משפיע על מהירות התיקון, השימוש בזיכרון וגודל ה-APK הפלט. + מהיר (מומלץ) + מהיר + תיקון מהיר יותר ושימוש בזיכרון נמוך יותר, על חשבון קובץ APK גדול יותר. מומלץ עבור מכשירים עם זיכרון RAM נמוך או ישנים יותר + מלא + תיקון איטי יותר, משכפל התנהגות ישנה. השתמש רק אם מצב מהיר גורם לבעיות GitHub PAT נדרש עבור מקורות בקשה רכש @@ -493,13 +515,7 @@ morphe_adaptive_monochrome_custom.xml גרסת שיזוקה המותקנת אינה נתמכת פתח את Shizuku ההתקנה נכשלה. נסה שוב או שנה להתקן אחר - ההתקנה חסומה על ידי אנדרואיד. בדוק Play Protect או הגדרות ביטחון אפליקציה אחרת עם שם חבילה זה כבר מותקנת. הסר אותה לפני המשך - APK אינו תואם למכשיר או לגרסת אנדרואיד זו - APK אינו תקין או ניזוק - אין מספיק מקום אחסון להתקנה - ההתקנה ארכה יותר מדי. נסה שוב - ההתקנה בוטלה פענוח %1$s… %1$s לא אישר את ההתקנה. בדוק את האפליקציה האחרת ונסה שוב %1$s דיווח על התקנה מוצלחת @@ -530,6 +546,7 @@ morphe_adaptive_monochrome_custom.xml שם משתמש (אלטרנטיב) סיסמא ייבוא + פורמט מאגר מפתחות סיסמא לא תקינה תיקון מותאם יובא שגגה להרוס תיקון: %s @@ -537,6 +554,7 @@ morphe_adaptive_monochrome_custom.xml ייצא את התיקון הנוכח אין תיקון מותאם זמין זמין תיקון מותאם יוצא + נכשל לייצא מאגר מפתחות שגגה להרוס תיקון: %s ייבוא הגדרות של הפיצ\'ח @@ -665,7 +683,8 @@ morphe_adaptive_monochrome_custom.xml אשר פרטים הסתר - ברירת מחדל + מוסתר + הסתר יומן מקורות מתקין… diff --git a/app/src/main/res/values-ja-rJP/plurals.xml b/app/src/main/res/values-ja-rJP/plurals.xml index 9a1dcb27c..0a47d1533 100644 --- a/app/src/main/res/values-ja-rJP/plurals.xml +++ b/app/src/main/res/values-ja-rJP/plurals.xml @@ -27,4 +27,10 @@ 選択中のパッチ %s 個 + + %s 個のアプリを表示 + + + %s 個のアプリが非表示 + diff --git a/app/src/main/res/values-ja-rJP/strings.xml b/app/src/main/res/values-ja-rJP/strings.xml index 5a58c647d..b97a81de2 100644 --- a/app/src/main/res/values-ja-rJP/strings.xml +++ b/app/src/main/res/values-ja-rJP/strings.xml @@ -5,11 +5,11 @@ まだパッチを適用していません 利用可能なパッチはありません このアプリを非表示にしますか? - このアプリはホーム画面から非表示になります。後でリスト下部の 「%1$s」 ボタンから復元できます + このアプリはホーム画面から非表示になります。リスト下部の 「%1$s」 ボタンから再表示できます このアプリは非表示になります - 非表示アプリを表示する - 非表示アプリはありません - 非表示アプリ + 非表示のアプリを表示する + 非表示のアプリはありません + 非表示のアプリ 非表示のアプリを再表示するにはタップします Android 11 のバグ Android 11 では、ユーザー体験に悪影響を及ぼす不具合が確認されています。あらかじめアプリのインストール権限を許可しておいてください @@ -32,6 +32,8 @@ 利用可能なアプリはありません %s でパッチソースを追加するか、既存のパッチソースを有効にしてください 「%1$s」に一致するアプリはありません + すべてのアプリが非表示です + ホーム画面からすべてのアプリを非表示にしました どのアプリにパッチを適用しますか? @@ -81,7 +83,7 @@ 更新成功 利用可能な更新はありません ソースのダウンロードに失敗しました: %s - \"%1$s\"のダウンロードに失敗しました: %2$s + 「%1$s」のダウンロードに失敗しました: %2$s 「%1$s」 を更新できませんでした: パッチ JSON ファイルが指定した URL に存在しません エキスパート このパッチを手動で追加するには、設定でエキスパートモードを有効にしてください @@ -91,10 +93,14 @@ <b>%s</b> にパッチを適用するには、以下のバージョンの APK (未パッチ) が必要です 推奨 未パッチ + 非互換 はい、 APK を探すのを手伝ってください いいえ、既に APK を持っています 保存済みの APK (v%s) を使用する 保存済みの APK がありません。パッチを適用するには、 APK ファイルを再度選択してください + Android %1$s+が必要です + このデバイスではサポートされていません + <b>%1$s</b> は、Android %2$d (API %3$d) でサポートされているバージョンがありません。宣言されているすべてのバージョンは、より高いAndroidバージョンを必要とします。 オリジナルの APK をダウンロード 操作手順: @@ -114,17 +120,17 @@ 選択したバージョン APK を選択 以下のボタンをタップして、パッチを当てたいアプリの APK ファイルを選択してください - ファイルを読み込めませんでした。\'外部ストレージ\'がバッテリー制限されていないか確認してください - 選択されたファイルは有効なAPKではありません + ファイルを読み込めませんでした。「外部ストレージ」がバッテリー制限の対象になっていないか確認してください + 選択されたファイルは有効な APK ではありません ファイルを開けませんでした。もう一度お試しください エキスパートモード パッチを検索 パッチ処理に進む - 全て無効化する - 全て有効化する - 推奨されるパッチを有効にする - 保存済みセレクションを復元 + 全て無効化 + 全て有効化 + 推奨パッチの有効化 + 保存済みのパッチセレクションの復元 パッチが見つかりません ユニバーサルパッチ 新規 @@ -137,10 +143,10 @@ 選択した <b>%s</b> の APK の署名証明書が想定と一致しません。改ざんされているか、信頼できないソースからのものである可能性があります これはオリジナルの APK ではない可能性があります 分割 APK が検出されました - "このファイルは APK バンドルです。 -(<b>APKM / APKS / XAPK</b>) + "このファイルは APK バンドルです +(<b>APKM / APKS / XAPK</b>)。 -このアプリのパッチには、<b>フル APK </b>を使用することをお勧めします" +このアプリのパッチには、<b>フル APK</b> を使用することをお勧めします" 別の APK を選択してください 実験的 このバージョンは実験的な機能を含んでおり、不安定または不完全である可能性があります @@ -221,6 +227,7 @@ 新しいバージョンのパッチが利用可能です。最新の変更を反映するには、アプリを再パッチしてください アプリがアンインストールされました Morphe 外でアプリがアンインストールされました。機能を復元するには、再パッチしてください + APK サイズ root 権限モードでは、パッチ適用前にオリジナルの APK がインストールされている必要があります GmsCore サポートパッチは root モードでは除外されます @@ -308,16 +315,13 @@ %s の実行に失敗しました パッチ適用済みアプリを今後のために保存しました パッチ適用済みアプリの保存に失敗しました - APK ファイルをダウンロード このプラグインを続行するには、ユーザーの操作が必要です パッチ処理がコード %1$s で終了しました 正常にインストールされました アプリのインストールに失敗しました: %s - インストールが完了しませんでした。システムインストーラーのダイアログを確認して、再度お試しください APK を保存しました パッチ適用済みアプリのエクスポートに失敗しました 保存されていたパッチ適用済みアプリを削除しました - 保存済みコピーを削除しました アプリをインストールしてから開いてください パッケージ名 マウントに失敗しました: %s @@ -337,6 +341,10 @@ グリッド パーティクル なし + ランダム + 起動時 + 毎日 + 3日に1回 視差効果 デバイスを傾けた時に背景が滑らかに移動します @@ -353,6 +361,10 @@ 現在の言語 一部の言語では、翻訳が存在しない、または不完全である可能性があります 新しい言語の翻訳や既存の翻訳の改善には、%s にアクセスしてください + + ホーム画面 + 挨拶メッセージ + ホーム画面に挨拶メッセージを表示します アプリアイコン デフォルト @@ -367,7 +379,7 @@ 更新 プレリリース版の Morphe を使用 - Morphe の新機能にいち早くアクセスできます。プレリリース版のパッチを取得するには、各パッチソースごとにプレリリースの切り替えを有効にしてください。 + Morphe の新機能をいち早く利用できます。プレリリース版のパッチを取得するには、各パッチソースで設定を有効にしてください モバイルデータ通信での更新 モバイルデータ通信での Morphe およびパッチの更新のダウンロードを許可します バックグラウンドでの更新通知 @@ -394,20 +406,29 @@ パッチ処理 バックグラウンドで Morphe がアプリにパッチを適用しているときに表示されます - エキスパート設定 + 上級者向け設定 エキスパートモード Morphe の高度なパッチ設定とカスタマイズオプションを有効にします エキスパート モードを有効にしますか? エキスパートモードではパッチ適用の詳細設定が可能ですが、設定を誤るとアプリが正常に動作しなくなる可能性があります - 未使用のネイティブライブラリの削除 - パッチ適用済みアプリから、非対応の CPU アーキテクチャ向けネイティブ ライブラリを削除します + デバイスのアーキテクチャに最適化する + "マージ時に未対応の CPU アーキテクチャ、ロケール、画面密度の分割 APK モジュールをスキップします。 +通常の APK の場合、パッチ適用後に未対応のアーキテクチャのネイティブライブラリを削除します" + + バイトコードの処理モード + パッチ適用時のバイトコードの処理方法を設定します。処理速度、メモリ使用量、出力 APK のサイズに影響します + 高速 (推奨) + 高速 + パッチ処理を高速化し、メモリ使用量を抑えますが、出力 APK のサイズが大きくなります。低 RAM 環境や古いデバイスにおすすめです + フル + パッチ処理は遅くなりますが、従来の動作を再現します。高速モードで問題が発生する場合のみ使用してください GitHub PAT の設定 プルリクエスト ソースに必要です PAT が設定されました PAT を取得するには GitHub の Personal Access Token の設定 - プルリクエストを外部ソースとして追加するには、 GitHub の Personal Access Token (PAT) を設定する必要があります。スコープ `public_repo` を指定した PAT を github.com で作成してください + プルリクエストを外部ソースとして追加するには、 GitHub の Personal Access Token (PAT) を設定する必要があります。スコープ public_repo を設定した PAT を github.com で作成してください 設定のエクスポートに含める この PAT をエクスポートした Morphe の設定に追加します PAT を共有しないでください。設定のエクスポートに含める場合、PAT が含まれるそのファイルも公開しないようにしてください @@ -510,13 +531,7 @@ morphe_header_custom_dark.png インストールされている Shizuku のバージョンはサポートされていません Shizuku を開く インストールに失敗しました。もう一度試すか、別のインストーラーを使用してください - インストールは Android によってブロックされました。Play プロテクトまたはセキュリティ設定を確認してください 同じパッケージ名を持つ別のアプリが既にインストールされています。続行するには、アプリをアンインストールしてください - この APK は、このデバイスまたは Android バージョンと互換性がありません - APK が無効であるか、破損しています - インストールに必要なストレージ容量が不足しています - インストールがタイムアウトしました。再度お試しください - インストールがキャンセルされました %1$s を開いています… %1$s がインストールを確認できませんでした。別のアプリを確認して、再度お試しください %1$s がインストールに成功したことを報告しました @@ -547,6 +562,7 @@ morphe_header_custom_dark.png ユーザー名(エイリアス) パスワード インポート + キーストア形式 キーストアの資格情報が正しくありません キーストアをインポートしました キーストアのインポートに失敗しました @@ -554,6 +570,7 @@ morphe_header_custom_dark.png 現在のキーストアをエクスポートします エクスポート可能なキーストアはありません キーストアをエクスポートしました + キーストアのエクスポートに失敗しました パスワードを表示 パスワードを非表示 @@ -592,17 +609,17 @@ morphe_header_custom_dark.png パッチ セレクション 保存されたパッチ セレクションとオプションを管理します。これらはアプリのアンインストール後も保持されます - すべての選択をリセットしますか? - パッケージ選択をリセットしますか? - ソースの選択をリセットしますか? + すべてのセレクションをリセットしますか? + パッケージ セレクションをリセットしますか? + ソース セレクションをリセットしますか? 保存済みパッチ セレクションなし %1$s ~ %2$s %2$s 内の %1$s ソース #%s 以下を削除します - すべてのパッケージとソースに保存されたパッチ選択を完全に削除します - %s に保存されたパッチ選択を、すべてのソースから完全に削除します - ソース #%2$s にある %1$s の保存されたパッチ選択を完全に削除します + すべてのパッケージとソースの保存済みパッチ セレクションを完全に削除します + %s のすべてのソースの保存済みパッチ セレクションを完全に削除します + %1$s のソース #%2$s の保存済みパッチ セレクションを完全に削除します ソースデータのエクスポートに失敗しました ソースデータのエクスポートに成功しました ソースデータのインポートに失敗しました @@ -615,7 +632,7 @@ morphe_header_custom_dark.png Morphe について ウェブサイトを共有 Morphe の公式ウェブサイトを共有します - "コミュニティからのフィードバックと貢献によって推進される、人気の Android アプリの最新かつ効率的なパッチを適用するためのオープンソースプロジェクトです" + "人気の Android アプリにモダンで効率的にパッチを適用するためのオープンソースプロジェクトです。コミュニティからのフィードバックや貢献によって発展しています" クレジット 現在の開発 過去の開発 @@ -682,14 +699,15 @@ morphe_header_custom_dark.png 許可 詳細 非表示にする - デフォルト + 非表示 + 再表示 ログ ソース インストール中… マウント中… アンマウント中… インポート中… - 設定のインポートに成功しました + インポートに成功しました 変更履歴を表示 最新のアップデートでの変更点を確認します 変更履歴のダウンロードに失敗しました: %s diff --git a/app/src/main/res/values-ka-rGE/strings.xml b/app/src/main/res/values-ka-rGE/strings.xml index 180ea1689..48f97ef60 100644 --- a/app/src/main/res/values-ka-rGE/strings.xml +++ b/app/src/main/res/values-ka-rGE/strings.xml @@ -39,10 +39,12 @@ + + diff --git a/app/src/main/res/values-kk-rKZ/strings.xml b/app/src/main/res/values-kk-rKZ/strings.xml index 180ea1689..48f97ef60 100644 --- a/app/src/main/res/values-kk-rKZ/strings.xml +++ b/app/src/main/res/values-kk-rKZ/strings.xml @@ -39,10 +39,12 @@ + + diff --git a/app/src/main/res/values-km-rKH/plurals.xml b/app/src/main/res/values-km-rKH/plurals.xml index ba2c702f9..204346a05 100644 --- a/app/src/main/res/values-km-rKH/plurals.xml +++ b/app/src/main/res/values-km-rKH/plurals.xml @@ -3,9 +3,6 @@ បំណះ %s - - ប្រតិបត្តិបំណះ %s - %s ប្រភពដែលបានដំឡើង diff --git a/app/src/main/res/values-km-rKH/strings.xml b/app/src/main/res/values-km-rKH/strings.xml index 9e6a11a54..2439e599d 100644 --- a/app/src/main/res/values-km-rKH/strings.xml +++ b/app/src/main/res/values-km-rKH/strings.xml @@ -59,11 +59,13 @@ ប្រព័ន្ធ ភាសាកម្មវិធី + អាយខនកម្មវិធី + diff --git a/app/src/main/res/values-kmr-rTR/plurals.xml b/app/src/main/res/values-kmr-rTR/plurals.xml index f03239cc5..d6f67280c 100644 --- a/app/src/main/res/values-kmr-rTR/plurals.xml +++ b/app/src/main/res/values-kmr-rTR/plurals.xml @@ -17,8 +17,8 @@ %s pakêtan - Ji bo %s çareser vekînin - Ji bo %s çareserên vekînin + %s yamê hat kirin + %s yamên hatin kirin %s pelê APK @@ -36,4 +36,12 @@ %s çareser hatin hilbijartin %s çareserên hatin hilbijartin + + %s sepanê + %s sepanan + + + %s sepanêyê veşartî + %s sepanên veşartî + diff --git a/app/src/main/res/values-kmr-rTR/strings.xml b/app/src/main/res/values-kmr-rTR/strings.xml index 61989591e..9047e03a4 100644 --- a/app/src/main/res/values-kmr-rTR/strings.xml +++ b/app/src/main/res/values-kmr-rTR/strings.xml @@ -24,7 +24,7 @@ Nûvekirin çêhatin Daneya mobîl aktîv e Kanalkan sazkirinê… - %1$s pêşqesm kirin + %1$s vekiraye Sazkirina kanalan temam bû Kanalan we nû kirin Kanalan ketin, ji kerema xwe bibeku @@ -32,6 +32,8 @@ Bername nîn in. Çavkanîyê zêde bike an yek jibo %s veşêre. Hewa bernameyên \"%1$s\" tune. + Hemû serlêdanan veşartin + Ez hemû serlêdanan ji ekranê pêşîn veşartim Kudu ezîtekora kodê ji xwerêzê we yê çawa îdeal bike? @@ -91,23 +93,27 @@ Ji bo dirûtîna <b>%s</b>, hûn pêdiviya APK-yên nebixwe hebin: Pisporîn Neçapandî + Nakamkirê Ew, min alîkarê dîtina APK-yê bikîn Na, ez APK-yek heznikim APK a vekirî te (v%s) bejin APK kume ne. Berî ev, wê APK ava beke + Android %1$s+ hewê ne + Li vê cihazê de ne piştgirîkirî + <b>%1$s</b> Android %2$d (API %3$d) ji bo piştgirîkirina guhertoyên heye. Hemû guhertoyên hatine diyarkirin ji bo Android guhertoyek bilindtir hewce dike APK-a orîjinalê dakêşîne Rêberî: Li jêr çapa \'%1$s\' biketine Li ser rûbera malperê biqewime û bikşîne Li ser malpera çapa serdest bike, ne ku xwer li vir 😊 - Heke barkevtin temamm bike, û <b>newestîne</b> APK - Heke barkevtin temamm bike, paştrê <b>APK-ê saz bike</b> - Piştî ku server heta qedandina dewam bike, ji Morphe desteke be. - Piştî ku APK-ya asayî bin deçên, ji Morphe desteke be. + Li benda qedembûna daxistinê bibe, û <b>APK-ê nas neke</b> + Binevin destpêkirina daxistnê biqedîne, paşê <b>APKê nas bike</b> + Pi serîdana daxistinê bi dawî bû, vegerin ser Morphe + Pi naskindina APK\'ya bilavkirinê, vegerin Morphe Bibe APKMirror.com - APK-ya danîyê bikê + APKê daxistî hilbijêre Sêw %1$s ê têbîrijayî ku bi pêştirî daxistine Dosya APK veqêşin Versyona ku tê pêşniyar kirin @@ -168,7 +174,7 @@ Ji bo mezgînî, ev sepan pêşniyar dike ku paketek <b>full APK</b> Hejmar biqîze Desîmal biqîze Folder safînak - Evîna vê ber bi hezaran e + Ev nirx e jixye heye Qîymet nikare boçagî be Heta niha tiştek zêde nabe %s: hemû rêzika pêwist tê dîtin @@ -219,6 +225,7 @@ Ji bo mezgînî, ev sepan pêşniyar dike ku paketek <b>full APK</b> Versîyonê nû ya qelvaton ava bûye. Bo cîgirtinê sepanê bi qelbûna herî dawî, pêşbirîyên nûîn kirin û çareseriyê bibin App hate laşkirin Epp li derve ji Morphe hate laşkirin. Serpêş pêlavînê da serê ku serê bi sist ki. + Mezinahiya APK\'yê Tîrek xwestisê wî de qewımîr APK tê serê îzayê we ev pêwist be pêşbaziyê biqewmê pêş de bîtin da xestes numberê berıkirînda Android bi usayiastî bimezânê Pişta GmsCore di moda root de ji holê derdike @@ -306,16 +313,13 @@ Ji bo mezgînî, ev sepan pêşniyar dike ku paketek <b>full APK</b> Şeva pek dabeşin %s Sepeya patch kirî ji bo paşê hatiye parastin Hate parastina sepeya patch kirî - Dosye APK-yê dakavêje Bixweştana mêz rêk û pê dei bilinge da pêşbaziyê bi zaniye vê plugin you bo lazmî bîye se Prosesa pêlavînê bi koda %1$s qedand. Bi ser ketin Serketina kuştina sepanê: %s - Giraistî ber hem dawıkirin ne. Hayiyan dialogê pêşbazî sistema be dîtin Û xemm biçike APK-yê hatiye parastin Serketina paşerojê ya sepanê Hateyên parastinê hatin jêbirin - Hateyên veşartinê hatin jêbirin Bernameyê saz bike berê li hevê pê bixe Navê pakêtê Serketina çêkirinê: %s @@ -335,6 +339,10 @@ Ji bo mezgînî, ev sepan pêşniyar dike ku paketek <b>full APK</b> Şebek Partîkel Tîya + Têkildar + Dema destpêkirinê + Rojane + Her 3 roj Bandora Parallax Serdestinê pêşbazinê de girtinê tê guherandin de hîrdikê alik bê yê ew zibu âniye @@ -351,6 +359,8 @@ Ji bo mezgînî, ev sepan pêşniyar dike ku paketek <b>full APK</b> Zimana niha Wergerandin ji bo hin zimanên kêmtir an jî ne tamkirin Ji bo wergerandina zimanên nû an jî pêşxistina wergerandinên hebin, serdana %s bikin + + Frazey şahê Îkonê sepanê Serekî @@ -397,8 +407,17 @@ Ji bo mezgînî, ev sepan pêşniyar dike ku paketek <b>full APK</b> Sazgarê nêzîkên Morphe û ji bo vebijarkan nermalina xwe saz bike Modeka pispor çêbike? Moda pispor kontrola zêdetir ser bernameyên ku meriv pêdike, lê çêkirina bêreng di moda pispor de dikare bibe sedem ku sepan bibe nekar. - Kîle pêwist nakin ji rayaka orîjînalan derxîne - Rayakên orîjînal ên ji bo arîtekî têrxaniyê nedesînar, ji app vêjaer bikin + Ji arîkek cihazê ve saz bike + "Modûlên APK-yên parçabûn ji bo arîkên CPU, zimanên lokal û qasîyên ekranê nedastûrik bêgîrîk merivê. +Ji bo APK-yên sazkare, fîlimên xwecihî ji bo arîkên nedastûrik piştî çêkirinê vedigere" + + Fêrqa pêşdaraziyê kodê + Kontrol dike ka kod çawa di dema guhertinê de pêşdarêzê. Bandor li ser leza guhertinê, karanîna bîr û mezinahiya APK ya derveyî dike. + Lez (Pêşnebîr) + Çav + Bitşik pêşandan û mêjiyê kêmtir, lê APK mezintir e. Ji bo alavên RAM kêm an kevnar tê pêşniyar. + Tam + Bitşik hêzika hevpar, rewşa kevnar vejînê. Tenê bikarîne heke moda bilez xeletî çêbike. PAT GitHub Ragrapha qebul, pêvir be jibo seyarê ber dom @@ -508,13 +527,7 @@ Qasîyên wêneyan divê wiha bin: Versiyona sazkirî ya Şizuku piştgirî nadê Veşêre Shizuku Îstikrar kirin jêvir kirin. Dibe, dîsa hewil bide an li îstalatorek din zivirînê hebin - Îstikrar kirin ji aliyê Android ve hat astebijartin. Guma Play Protect an setingên ewletiyê kontrol bike Sep ne ku bi navê pakêtê ya viya berê hatî saz kirin. Berî pêşxistina hevalan, ew jêbirin be - APK bi vê cihaz an versiyona Androida nestên fêr - APK ne cudne an xeranbûne - Jibo îstikrar kirin, asta glazî heye - Îstikrar kirin heta demê pêxistine xistê. Dibe, dîsa hewil bide - Îstikrar kirin astebijartin kirin %1$s vekirine… %1$s îstikrar kirin gef ne kir. Sepa din kontrol bike û dîsa hewil bide %1$s îstikrar kirin serketin ragihand @@ -545,6 +558,7 @@ Qasîyên wêneyan divê wiha bin: Karanbera (Alias) Nexweş Importê + Formata kilîdparêzê Nivîsandina parçehuyê xeṭî ye Parçehuyê zafortir hate kirin Ji kirina parçehuyê neçîbe @@ -552,6 +566,7 @@ Qasîyên wêneyan divê wiha bin: Parçehuyê niha derxîne Parçehuyek ku were derxistin tune Parçehuy derxist + Destûr neda exportkirina keystore\'ê Şifreyê nîşana bîne Şifreyê veşêre @@ -680,7 +695,8 @@ Qasîyên wêneyan divê wiha bin: Îznaye Agahdarî Vêşêrîn - Standard + Veşartî + Neveşartîne Log Çavkanî Dikuştin… diff --git a/app/src/main/res/values-kn-rIN/strings.xml b/app/src/main/res/values-kn-rIN/strings.xml index 180ea1689..48f97ef60 100644 --- a/app/src/main/res/values-kn-rIN/strings.xml +++ b/app/src/main/res/values-kn-rIN/strings.xml @@ -39,10 +39,12 @@ + + diff --git a/app/src/main/res/values-ko-rKR/plurals.xml b/app/src/main/res/values-ko-rKR/plurals.xml index 135651fd4..98654d7da 100644 --- a/app/src/main/res/values-ko-rKR/plurals.xml +++ b/app/src/main/res/values-ko-rKR/plurals.xml @@ -27,4 +27,10 @@ %s 개의 패치 선택됨 + + %s 개의 앱 보이기 + + + %s 개의 숨겨진 앱 + diff --git a/app/src/main/res/values-ko-rKR/strings.xml b/app/src/main/res/values-ko-rKR/strings.xml index 51b1484dc..bbb4c246b 100644 --- a/app/src/main/res/values-ko-rKR/strings.xml +++ b/app/src/main/res/values-ko-rKR/strings.xml @@ -7,7 +7,7 @@ 이 앱을 숨기시겠습니까? 이 앱이 홈 화면에서 숨겨집니다. 나중에 목록 하단에 있는 \'%1$s\' 버튼을 사용하여 복원할 수 있습니다 이 앱이 숨겨집니다 - 숨겨진 앱 보기 + 숨겨진 앱 보이기 숨겨진 앱이 없습니다 숨겨진 앱 숨기기를 해제하려면 탭하세요 @@ -32,6 +32,8 @@ 사용 가능한 앱이 없습니다 %s에 패치 소스를 추가하거나 기존 패치 소스를 활성화하세요 \'%1$s\'와 일치하는 앱이 없습니다 + 모든 앱이 숨겨집니다 + 홈 화면에서 모든 앱이 숨겨집니다 어떤 앱을 패치하시겠습니까? @@ -91,10 +93,14 @@ <b>%s</b>를 패치하려면 패치되지 않은 APK가 필요합니다: 권장 버전 패치되지 않음 + 호환되지 않음 네, APK를 찾는데 도움을 주세요 아니요, 이미 APK를 가지고 있습니다 저장된 APK 사용 (v%s) 저장된 APK가 없습니다. 패치하려면 APK 파일을 다시 선택하세요 + Android %1$s+ 필요 + 이 기기에서는 지원되지 않음 + <b>%1$s</b>는 Android %2$d (API %3$d)에서 지원되는 버전이 없습니다. 선언된 모든 앱 버전은 더 높은 Android 버전을 요구합니다 원본 APK 다운로드 설명서: @@ -221,6 +227,7 @@ 새 버전의 패치가 있습니다. 최신 개선 사항 및 수정 사항을 적용하려면 앱을 다시 패치하세요 앱이 제거되었습니다 이 앱은 Morphe 외부에서 제거되었습니다. 기능을 복원하려면 다시 패치하세요 + APK 크기 루트 모드에서는 패치 전에 원본 APK가 기기에 설치되어 있어야 합니다 루트 모드에서는 \'GmsCore support\' 패치가 제외됩니다 @@ -308,16 +315,13 @@ \'%s\'을 적용할 수 없습니다 패치된 앱을 나중을 위해 저장하였습니다 패치된 앱을 저장할 수 없습니다 - APK 파일을 다운로드하는 중 이 플러그인을 사용하려면 사용자 상호작용이 필요합니다 패처 프로세스를 코드 \'%1$s\'로 종료하였습니다 성공적으로 설치하였습니다 앱을 설치할 수 없습니다: %s - 설치가 완료되지 않았습니다. 시스템 설치 프로그램 다이얼로그를 확인하고 다시 시도하세요 APK를 저장하였습니다 패치된 앱을 내보낼 수 없습니다 저장된 패치된 앱을 제거하였습니다 - 저장된 사본을 제거하였습니다 열기 전에 앱을 설치하세요 패키지 이름 마운트할 수 없습니다: %s @@ -337,6 +341,10 @@ 격자 입자 없음 + 임의 + 앱 실행 시 + 매일 + 3일마다 페럴랙스 효과 기기를 기울이면 배경이 부드럽게 이동합니다 @@ -353,6 +361,10 @@ 현재 언어 일부 언어의 번역이 누락되거나 불완전할 수 있습니다 새 언어를 번역하거나 기존 번역을 개선하려면 %s 를 방문하세요 + + 홈 화면 + 인사말 + 홈 화면에 인사말을 표시합니다 앱 아이콘 기본값 @@ -399,8 +411,17 @@ 고급 Morphe 패치 설정과 커스텀 옵션을 활성화합니다 전문가 모드 활성화 전문가 모드에서는 패치를 적용하는 방식을 더 세밀하게 조절할 수 있지만, 설정을 잘못 건드리면 앱이 정상적으로 작동하지 않을 수 있습니다 - 사용되지 않는 라이브러리 제거 - 이 기기에서 지원되지 않는 CPU 아키텍처에 대한 네이티브 라이브러리를 패치된 앱에서 삭제합니다 + 기기 아키텍처에 맞게 최적화 + "병합 과정에서 이 기기에서 지원되지 않는 CPU 아키텍처, 언어 및 화면 밀도에 대한 분할 APK 모듈을 건너뜁니다. +일반 APK의 경우에는 패치 후에 지원되지 않는 아키텍처에 대한 네이티브 라이브러리를 제거합니다" + + 바이트코드 처리 모드 + 패치 과정에서 바이트코드를 처리하는 방식을 제어합니다. 패치 속도, 메모리 사용량 및 출력 APK 크기에 영향을 줍니다 + 빠름 (권장) + 빠름 + 빠른 패치 속도와 낮은 메모리 사용량을 제공하지만, APK 크기가 커집니다. 저사양 또는 RAM이 적은 기기에 권장됩니다 + 최대 + 느린 패치 속도로 기존 동작 방식과 동일하게 작동합니다. 빠른 모드에서 문제가 발생할 경우만 사용하세요 GitHub 개인 액세스 토큰 풀 리퀘스트 소스에 필요합니다 @@ -511,13 +532,7 @@ morphe_header_custom_dark.png 설치된 Shizuku 버전은 지원되지 않습니다 Shizuku 열기 설치할 수 없습니다. 다시 시도하거나 다른 설치 프로그램으로 변경하세요 - Android에서 설치가 차단되었습니다. Play Protect 또는 보안 설정을 확인하세요 이 패키지 이름을 가진 다른 앱이 이미 설치되어 있습니다. 계속하기 전에 제거하세요 - APK가 이 기기 또는 Android 버전과 호환되지 않습니다 - APK가 잘못되었거나 손상되어 있습니다 - 설치에 필요한 저장소 공간이 충분하지 않습니다 - 설치 시간이 초과되었습니다. 다시 시도하세요 - 설치가 취소되었습니다 \'%1$s\'을 여는 중… \'%1$s\'에서 설치를 확인하지 못하였습니다. 다른 앱을 확인하고 다시 시도하세요 \'%1$s\'이 설치 성공을 보고하였습니다 @@ -532,7 +547,7 @@ morphe_header_custom_dark.png 프로세스 런타임 안정성 향상을 위해 패치를 별도의 프로세스에서 실행합니다 메모리 제한 - 메모리 제한을 높이면 패치 속도가 빨라질 수 있지만 구형 기기에서는 작동되지 않을 수 있습니다 + 메모리 제한을 높이면 패치 속도가 빨라질 수 있지만 저사양 기기에서는 작동되지 않을 수 있습니다 프로세스 런타임 활성화 패치에 사용 가능한 메모리 메모리 제한이 낮으면 대용랑 앱을 패치할 경우에 오류가 발생할 수 있습니다 @@ -548,6 +563,7 @@ morphe_header_custom_dark.png 사용자 이름 (별칭) 비밀번호 가져오기 + 키스토어 형식 키스토어 자격 증명이 잘못되었습니다 키스토어를 가져왔습니다 키스토어를 가져올 수 없습니다 @@ -555,6 +571,7 @@ morphe_header_custom_dark.png 현재 사용되는 키스토어를 내보냅니다 내보낼 키스토어가 없습니다 키스토어를 내보냈습니다 + 키스토어를 내보낼 수 없습니다 비밀번호 표시 비밀번호 숨기기 @@ -683,7 +700,8 @@ morphe_header_custom_dark.png 허용 세부 정보 숨기기 - 기본값 + 숨겨짐 + 숨기기 해제 로그 소스 설치하는 중… diff --git a/app/src/main/res/values-ky-rKG/strings.xml b/app/src/main/res/values-ky-rKG/strings.xml index 180ea1689..48f97ef60 100644 --- a/app/src/main/res/values-ky-rKG/strings.xml +++ b/app/src/main/res/values-ky-rKG/strings.xml @@ -39,10 +39,12 @@ + + diff --git a/app/src/main/res/values-lo-rLA/strings.xml b/app/src/main/res/values-lo-rLA/strings.xml index 180ea1689..48f97ef60 100644 --- a/app/src/main/res/values-lo-rLA/strings.xml +++ b/app/src/main/res/values-lo-rLA/strings.xml @@ -39,10 +39,12 @@ + + diff --git a/app/src/main/res/values-lt-rLT/strings.xml b/app/src/main/res/values-lt-rLT/strings.xml index e0f7685de..fb1ac9ac8 100644 --- a/app/src/main/res/values-lt-rLT/strings.xml +++ b/app/src/main/res/values-lt-rLT/strings.xml @@ -57,10 +57,12 @@ + + Pataisų nustatymai diff --git a/app/src/main/res/values-lv-rLV/strings.xml b/app/src/main/res/values-lv-rLV/strings.xml index f4948f6cd..f45200595 100644 --- a/app/src/main/res/values-lv-rLV/strings.xml +++ b/app/src/main/res/values-lv-rLV/strings.xml @@ -40,10 +40,12 @@ Nav neviena pakalpojuma + + diff --git a/app/src/main/res/values-mai-rIN/strings.xml b/app/src/main/res/values-mai-rIN/strings.xml index 180ea1689..48f97ef60 100644 --- a/app/src/main/res/values-mai-rIN/strings.xml +++ b/app/src/main/res/values-mai-rIN/strings.xml @@ -39,10 +39,12 @@ + + diff --git a/app/src/main/res/values-mk-rMK/strings.xml b/app/src/main/res/values-mk-rMK/strings.xml index 180ea1689..48f97ef60 100644 --- a/app/src/main/res/values-mk-rMK/strings.xml +++ b/app/src/main/res/values-mk-rMK/strings.xml @@ -39,10 +39,12 @@ + + diff --git a/app/src/main/res/values-mn-rMN/strings.xml b/app/src/main/res/values-mn-rMN/strings.xml index 180ea1689..48f97ef60 100644 --- a/app/src/main/res/values-mn-rMN/strings.xml +++ b/app/src/main/res/values-mn-rMN/strings.xml @@ -39,10 +39,12 @@ + + diff --git a/app/src/main/res/values-mr-rIN/plurals.xml b/app/src/main/res/values-mr-rIN/plurals.xml index 3ea04e700..c85d73d0d 100644 --- a/app/src/main/res/values-mr-rIN/plurals.xml +++ b/app/src/main/res/values-mr-rIN/plurals.xml @@ -1,2 +1,47 @@ - + + + %s सुधारणा + %s अनेक सुधारणा + + + %s सुधारणा पर्याय + %s अनेक सुधारणा पर्याय + + + %s मूळ + %s मुळे + + + %s संच + %s अनेक संच + + + %s सुधारणा पूर्ण केली + %s सुधारणा पूर्ण केल्या + + + %s अँड्रॉइड संच संचिका + %s अनेक अँड्रॉइड संच संचिका + + + जतन केलेल्या निवडीमधून %s जुन्या सुधारणा काढल्या आहेत. + जतन केलेल्या निवडीमधून %s जुन्या अनेक सुधारणा काढल्या आहेत. + + + %s मूळ यशस्वीरित्या स्थापित केले + %s अनेक मूळ यशस्वीरित्या स्थापित केले + + + %s सुधारणा निवडली + %s सुधारणा निवडल्या + + + %s ॲप दाखवा + %s ॲप्स दाखवा + + + %s लपवलेले ॲप + %s लपवलेले ॲप्स + + diff --git a/app/src/main/res/values-mr-rIN/strings.xml b/app/src/main/res/values-mr-rIN/strings.xml index f950371a3..935fc2f8c 100644 --- a/app/src/main/res/values-mr-rIN/strings.xml +++ b/app/src/main/res/values-mr-rIN/strings.xml @@ -1,30 +1,123 @@ + इतर ॲप्स + अजून सुधारणा केलेली नाही + सुधारणा उपलब्ध नाहीत + हे ॲप लपवायचे का + हे ॲप मुख्य पानावरून लपवले जाईल. तुम्ही नंतर यादीच्या तळाशी असलेल्या \'%1$s\' बटणाचा वापर करून ते पुन्हा मिळवू शकता. + हे ॲप लपवले जाईल. + लपवलेली ॲप्स दाखवा + कोणतेही लपवलेले ॲप नाही + लपवलेली ॲप्स \"Tap to unhide\" + अँड्रॉइड 11 दोष + अँड्रॉइड 11 सिस्टिममधील एक दोष टाळण्यासाठी, ॲप स्थापनेची परवानगी आधीच देणे आवश्यक आहे; अन्यथा त्याचा वापरकर्त्याच्या अनुभवावर वाईट परिणाम होईल. + मूळ जोडा + तुम्हाला \'Morphe\' मध्ये ही सुधारणा मूळ जोडायची आहे का? + फक्त तुमच्या विश्वासार्ह मुळांमधूनच मूळ जोडा. + Morphe नवीन सुधारणा उपलब्ध आहे + नवीन सुधारणा स्थापनेसाठी तयार आहे + सुधारणा अयशस्वी झाली + तुमचे नेट कनेक्शन तपासा आणि पुन्हा प्रयत्न करा. + सुधारणा वगळल्या + मोबाईल डेटा/नेट चालू आहे + मुळांमध्ये सुधारणा करत आहे + %1$s प्रक्रिया पूर्ण झाली + मुळांमधील सुधारणा पूर्ण झाली + तुमची मुळे आधीच नवीन आहेत. + मुळे लोड होत आहेत, कृपया वाट पहा ॲप्स शोधा ॲप्स उपलब्ध नाहीत पॅच स्रोत जोडा किंवा %s मध्ये বিদ্যমান सक्षम करा \"%1$s\" शी जुळणारे कोणतेही ॲप(s) नाहीत + सर्व ॲप्स लपवले आहेत + तुम्ही होम स्क्रीनवरून सर्व ॲप्स लपवले आहेत + तुम्हाला कोणत्या ॲपमध्ये सुधारणा करायची आहे? + ॲप्सचा नाद घुमवायला तयार आहात का? + कंबर कसा, सुधारणेची वेळ आली आहे! + नवा दिवस, जाहिरात-मुक्त आणखी एक महाकलाकृती! + जाहिराती बघायची इच्छा आहे? माझी पण नाही! + चला, आता तुमच्या ॲप्सना सरळ करण्याची वेळ आलीये! + आजचा अंदाज: 100% खात्री फक्त \'सुधारणा\' होण्याचीच! + साध्या सुध्या गोष्टींना जबरदस्त बनवायला तयार आहात का? + आजपासून ॲप्सना त्यांच्याच दोषांपासून वाचवत आहे + कॉफी आणि उत्तम सुधारणांच्या जोरावर काम सुरू + सुधारणांची मुळे + लिंक बाहेर नेटवर उघडा + लिंक उघडता आली नाही + नवीन प्रायोगिक सुधारणा वापरा + नवीन प्रायोगिक सुधारणा सर्वांच्या आधी वापरा + चाचणीसाठी असलेल्या ॲप आवृत्त्या वापरा + शक्य असल्यास प्रायोगिक ॲप्सवर पॅच लागू करा + हे मूळ आधीच जोडले गेले आहे + दिसणारे नाव + सुधारणांच्या मुळाचे नाव बदला + या नावाचे सुधारणा मूळ आधीपासून उपलब्ध आहे + हे सुधारणा मूळ अपडेट करता आले नाही + कोणतीही आवृत्ती + कोणताही संच + सुधारणा मूळ जोडा + \"%s\" हे मूळ हटवायचे का? ही कृती पुन्हा बदलता येणार नाही + बाहेरून + मुळाची लिंक + येथे काही उदाहरणे दिली आहेत: + याच फोनमधील + सुधारणा मूळ फाईल निवडा + स्टोरेजमधून .mpp सुधारणा मूळ फाईल निवडा + फाईल बदला + पूर्व-स्थापित + अपडेट यशस्वी + कोणतेही अपडेट उपलब्ध नाही + मुळ डाउनलोड होऊ शकली नाहीत: %s \'%1$s\' डाउनलोड करण्यात अयशस्वी: %2$s + \'%1$s\' अपडेट होऊ शकले नाही: दिलेल्या लिंकवर सुधारणा JSON फाईल उपलब्ध नाही. तज्ञ हा पॅच मॅन्युअली समाविष्ट करण्यासाठी सेटिंग्जमध्ये तज्ञ मोड सक्षम करा + मूळ APK शोधण्यासाठी मदत हवी आहे का? + <b>%s</b>, मध्ये बदल करण्यासाठी, तुमच्याकडे बदल न केलेले APK आवृत्ती असणे आवश्यक आहे: + <b>%s</b>, मध्ये बदल करण्यासाठी, तुमच्याकडे बदल न केलेले APK आवृत्ती असणे आवश्यक आहे: + सुचवलेले + बदल न केलेले + असंगत + हो, मला APK शोधण्यात मदत करा. + नाही, माझ्याकडे आधीपासूनच APK आहे. + आधीच साठवलेली APK (v%s) वापरा + कोणतीही साठवलेली APK नाही. तुम्हाला पुन्हा एकदा APK फाईल निवडावी लागेल. + Android %1$s+ आवश्यक आहे + या डिव्हाइसवर समर्थित नाही + <b>%1$s</b> Android %2$d (API %3$d) साठी समर्थित आवृत्त्या नाहीत. घोषित केलेल्या सर्व आवृत्त्यांसाठी उच्च Android आवृत्ती आवश्यक आहे. + मूळ APK डाउनलोड करा + सूचना + खालील \'%1$s\' बटण दाबा + वेबपेजवर खाली स्क्रोल करा आणि दाबा + वेबसाइटवर असलेल्या डाउनलोड बटणावर दाबा, येथे नाही. 😊 + डाउनलोड पूर्ण होऊ द्या, पण APK <b>इन्स्टॉल करू नका.<b> + डाउनलोड पूर्ण होण्याची प्रतीक्षा करा, त्यानंतर <b>APK इन्स्टॉल करा<b> + डाउनलोड झाल्यावर पुन्हा Morphe उघडा. + इन्स्टॉलेशन पूर्ण झाल्यावर Morphe कडे परत वळा. + APKMirror.com उघडा + डाउनलोड केलेली APK निवडा + तुम्ही आत्ताच डाउनलोड केलेली <b>%1$s</b> APK फाईल निवडा + APK फाईल उघडा + सुचवलेली आवृत्ती + निवडलेली आवृत्ती फाईल वाचण्यात अयशस्वी. \'External Storage\' बॅटरी निर्बंधीत नसल्याची खात्री करा निवडलेली फाइल वैध APK नाही. फाईल उघडण्यात अयशस्वी. कृपया पुन्हा प्रयत्न करा @@ -39,6 +132,7 @@ + APK आकार ॲप माहिती @@ -54,12 +148,30 @@ + randomly + चालू केल्यावर + दैनिक + प्रत्येक 3 दिवसांनी + + मुख्य स्क्रीन + शुभेच्छा संदेश + मुख्य स्क्रीनवर शुभेच्छा संदेश दर्शवा + उपकरणाच्या आर्किटेक्चरसाठी अनुकूल करा + "साध्या APK साठी, पॅचिंगनंतर असमर्थित आर्किटेक्चरसाठी नेटिव्ह लायब्ररी काढल्या जातात." + + बाईटकोड प्रक्रिया मोड + patching दरम्यान बाईटकोड कसा process केला जातो हे नियंत्रित करते. patching गती, मेमरी वापर आणि आउटपुट APK आकार यावर परिणाम होतो + जलद (शिफारस केलेले) + जलद + मोठा APK आकार या बदल्यात, जलद पॅचिंग आणि कमी मेमरी वापर. कमी RAM किंवा जुन्या उपकरणांसाठी शिफारसित + पूर्ण + धीमे पॅचिंग, जुन्या वर्तणुकीची नक्कल करते. जलद मोड समस्या निर्माण करत असल्यास फक्त ते वापरा @@ -69,6 +181,8 @@ + कीस्टोअर स्वरूप + कीस्टोअर निर्यात करण्यात अयशस्वी @@ -82,6 +196,8 @@ सर्व गाळण डिमाउंट + लपलेले + उघड करा स्त्रोत यशस्वीरित्या आयात केले diff --git a/app/src/main/res/values-ms-rMY/plurals.xml b/app/src/main/res/values-ms-rMY/plurals.xml index 4d043f1f5..34a528ccb 100644 --- a/app/src/main/res/values-ms-rMY/plurals.xml +++ b/app/src/main/res/values-ms-rMY/plurals.xml @@ -12,9 +12,6 @@ %s pakej - - Jalankan %s tampalan - %s fail APK diff --git a/app/src/main/res/values-ms-rMY/strings.xml b/app/src/main/res/values-ms-rMY/strings.xml index 81e7ab617..a6e667ddd 100644 --- a/app/src/main/res/values-ms-rMY/strings.xml +++ b/app/src/main/res/values-ms-rMY/strings.xml @@ -297,16 +297,13 @@ Teruskannya mungkin boleh menyebabkan aplikasi tidak dapat berfungsi Gagal untuk memasang %s Aplikasi telah ditampal disimpan untuk lain kali Gagal untuk menyimpan aplikasi yang telah ditampal - Muat turun fail APK Interaksi pengguna diperlukan untuk meneruskan dengan pemalam ini Proses penampalan keluar dengan kod %1$s Pemasangan berjaya Gagal untuk dipasang: %s - Pemasangan tidak selesai. Semak dialog sistem pemasang dan cuba semula APK tersimpan Gagal untuk membawa keluar aplikasi yang tertampal Aplikasi tertampal yang disimpan telah dibuang - Salinan yang disimpan telah dibuang Pasang aplikasi sebelum membukanya Nama pakej Gagal untuk ditunggang: %s @@ -342,6 +339,7 @@ Teruskannya mungkin boleh menyebabkan aplikasi tidak dapat berfungsi Bahasa semasa Terjemahan untuk sesetengah bahasa mungkin kurang atau tidak lengkap Untuk menterjemahkan bahasa baharu atau memperbaiki terjemahan yang sedia ada, lawati %s + Ikon aplikasi Lalai @@ -388,8 +386,7 @@ Teruskannya mungkin boleh menyebabkan aplikasi tidak dapat berfungsi Aktifkan tetapan dan pilihan penyesuaian Morphe yang kompleks Aktifkan Mod Pakar? Mod Pakar memberikan kawalan yang lebih besar ke atas cara tampalan digunakan, tetapi tetapan yang salah konfigurasi dalam Mod Pakar boleh mengakibatkan aplikasi yang tidak berfungsi - Buang pustaka asli yang tidak digunakan - Padamkan pustaka asli untuk seni bina CPU yang tidak disokong dari aplikasi yang ditampal + GitHub PAT Diperlukan untuk sumber permintaan sedekah @@ -499,13 +496,7 @@ Dimensi gambar mestilah seperti berikut: Pemasangan gagal. Cuba lagi atau tukar kepada pemasang yang berbeza Buka Shizuku Pemasangan disekat oleh Android. Periksa Play Protect atau tetapan keselamatan - Mula Shizuku atau Sui dan cuba lagi Satu lagi aplikasi dengan nama pakej ini sudah dipasang. - APK tidak serasi dengan peranti atau versi Android ini - APK tidak sah atau rosak - Tiada ruang simpanan yang mencukupi untuk pemasangan - Pemasangan telah tamat tempoh. Cuba lagi - Pemasangan dibatalkan Membuka %1$s… %1$s tidak mengesahkan pemasangan. Periksa aplikasi lain dan cuba lagi %1$s melaporkan pemasangan berjaya @@ -667,7 +658,6 @@ Dimensi gambar mestilah seperti berikut: Benarkan Butiran Sembunyikan - Laluan asal Log Memasang… Pemasangan… diff --git a/app/src/main/res/values-my-rMM/plurals.xml b/app/src/main/res/values-my-rMM/plurals.xml index 4caca971d..3ea04e700 100644 --- a/app/src/main/res/values-my-rMM/plurals.xml +++ b/app/src/main/res/values-my-rMM/plurals.xml @@ -1,6 +1,2 @@ - - - %s%s - - + diff --git a/app/src/main/res/values-my-rMM/strings.xml b/app/src/main/res/values-my-rMM/strings.xml index b8f153bcb..34cdea842 100644 --- a/app/src/main/res/values-my-rMM/strings.xml +++ b/app/src/main/res/values-my-rMM/strings.xml @@ -10,6 +10,7 @@ ဝှက်ထားသော အက်ပ်များကို ပြပါ + သင့်ကိုဘယ်အက်‍ပလီကေးရှင်းကို patch ချင်လဲ။ @@ -20,6 +21,7 @@ + ကိုင်းရှင်း။ @@ -46,10 +48,12 @@ + + diff --git a/app/src/main/res/values-nb-rNO/strings.xml b/app/src/main/res/values-nb-rNO/strings.xml index 180ea1689..48f97ef60 100644 --- a/app/src/main/res/values-nb-rNO/strings.xml +++ b/app/src/main/res/values-nb-rNO/strings.xml @@ -39,10 +39,12 @@ + + diff --git a/app/src/main/res/values-ne-rIN/strings.xml b/app/src/main/res/values-ne-rIN/strings.xml index 180ea1689..48f97ef60 100644 --- a/app/src/main/res/values-ne-rIN/strings.xml +++ b/app/src/main/res/values-ne-rIN/strings.xml @@ -39,10 +39,12 @@ + + diff --git a/app/src/main/res/values-nl-rNL/plurals.xml b/app/src/main/res/values-nl-rNL/plurals.xml index 96e9750a1..1a129fcc1 100644 --- a/app/src/main/res/values-nl-rNL/plurals.xml +++ b/app/src/main/res/values-nl-rNL/plurals.xml @@ -17,8 +17,8 @@ %s pakketten - Execute %s patch - Execute %s patches + %s patch uitgevoerd + %s patches uitgevoerd %s APK-bestand @@ -36,4 +36,12 @@ %s patch geselecteerd %s patches geselecteerd + + Laat %s app zien + Laat %s apps zien + + + %s verborgen app + %s verborgen apps + diff --git a/app/src/main/res/values-nl-rNL/strings.xml b/app/src/main/res/values-nl-rNL/strings.xml index c1af9f6e3..365a93e15 100644 --- a/app/src/main/res/values-nl-rNL/strings.xml +++ b/app/src/main/res/values-nl-rNL/strings.xml @@ -32,6 +32,8 @@ Geen apps beschikbaar Voeg een patchbron toe of schakel een bestaande in %s Geen apps komen overeen met \"%1$s\" + Alle apps zijn verborgen + U heeft alle apps van het startscherm verborgen Welke app wil je patchen? @@ -91,10 +93,14 @@ Om <b>%s</b> te patchen, heb je een ongepatchte APK nodig van de volgende versies: Aanbevolen Ongepatcht + Incompatibel Ja, help me een APK te vinden Nee, ik heb al een .APK Gebruik opgeslagen .APK-bestand (v%s) Er is geen APK opgeslagen. Om de patch uit te voeren, moet u het APK opnieuw selecteren + Vereist Android %1$s+ + Niet ondersteund voor dit apparaat + <b>%1$s</b> heeft geen ondersteunde versies voor Android %2$d (API %3$d). Alle vermelde versies vereisen een hogere Android-versie Download de originele .APK-bestand Instructies: @@ -221,6 +227,7 @@ Voor de beste resultaten beveelt deze app aan om een <b>volledig APK</b Er is een nieuwere versie van patches beschikbaar. Repatch uw app om de laatste verbeteringen en correcties te krijgen App was verwijderd Deze app is buiten Morphe verwijderd. Installeer de patch om de functionaliteit te herstellen + APK-grootte Root-modus vereist dat de oorspronkelijke APK op uw apparaat geïnstalleerd is voordat gepatcht wordt GmsCore-ondersteuningspatch is uitgesloten in rootmodus @@ -308,16 +315,13 @@ Voor de beste resultaten beveelt deze app aan om een <b>volledig APK</b Kon %s niet toepassen Gepatchte app opgeslagen voor later Kon gepatchte app niet opslaan - APK-bestand downloaden Gebruikersinteractie is vereist om te kunnen doorgaan met deze plugin Patcher-process is afgelopen met code %1$s Succesvol geïnstalleerd Kon app niet installeren: %s - De installatie is niet voltooid. Controleer het installatiedialoogvenster en probeer het opnieuw APK opgeslagen Kan gepatchte app niet exporteren Opgeslagen gepatchte app verwijderd - Opgeslagen kopie verwijderd Installeer de app voordat u deze opent Pakketnaam Kon %s niet koppelen @@ -337,6 +341,10 @@ Voor de beste resultaten beveelt deze app aan om een <b>volledig APK</b Raster Deeltjes Geen + Willekeurig + Bij het opstarten + Dagelijks + Om de 3 dagen Parallax-effect Vloeiende achtergrondverschuiving bij het kantelen van het apparaat @@ -353,6 +361,10 @@ Voor de beste resultaten beveelt deze app aan om een <b>volledig APK</b Huidige taal Vertalingen voor sommige talen kunnen ontbreken of onvolledig zijn Om nieuwe talen te vertalen of bestaande vertalingen te verbeteren, bezoek %s + + Startscherm + Begroetingszinnen + Toon een begroetingsbericht op de startscherm App-pictogram Standaard @@ -399,8 +411,17 @@ Voor de beste resultaten beveelt deze app aan om een <b>volledig APK</b Inschakelen complexe Morphe-patchinstellingen en aanpassingsopties Expertmodus inschakelen? De expertmodus biedt meer controle over hoe patches worden toegepast, maar een verkeerde configuratie in de expertmodus kan ertoe leiden dat de app niet meer werkt - Verwijder ongebruikte native bibliotheken - Verwijder native library\'s voor niet-ondersteunde CPU-architecturen uit gepatchte apps + Optimaliseer voor apparaatarchitectuur + "Sla split APK-modules voor niet-ondersteunde CPU-architecturen, talen en schermresoluties over tijdens het samenvoegen. +Voor gewone APK's worden native bibliotheken voor niet-ondersteunde architecturen verwijderd na het patchen" + + Bytecode-verwerkingsmodus + Regelt hoe bytecode wordt verwerkt tijdens patchen. Beïnvloedt patchesnelheid, geheugengebruik en APK-grootte + Snel (Aanbevolen) + Snel + Sneller patchen en minder geheugen gebruiken, ten koste van een grotere APK. Aanbevolen voor apparaten met weinig RAM of oudere apparaten + Volledig + Langzamer patchen, repliceert eerder gedrag. Alleen gebruiken als de snelle modus problemen veroorzaakt GitHub PAT Vereist voor pull request-bronnen @@ -510,13 +531,7 @@ De afbeelddimensionen moeten als volgt zijn: De geïnstalleerde Shizuku-versie wordt niet ondersteund Shizuku openen De installatie is mislukt. Probeer het opnieuw of schakel over naar een ander installatieprogramma - De installatie is geblokkeerd door Android. Controleer Play Protect of beveiligingsinstellingen Een andere app met hetzelfde pakketnaam is al geïnstalleerd. Verwijder deze voordat u doorgaat - De APK is niet compatibel met dit apparaat of Android-versie - De APK is niet geldig of beschadigd - Er is niet genoeg opslagruimte voor de installatie - De installatie is mislukt. Probeer het opnieuw - De installatie is geannuleerd %1$s wordt geopend… %1$s heeft de installatie niet bevestigd. Controleer de andere app en probeer het opnieuw %1$s heeft een geslaagde installatie gemeld @@ -547,6 +562,7 @@ De afbeelddimensionen moeten als volgt zijn: Gebruikersnaam (alias) Wachtwoord Importeren + Keystore-formaat Verkeerde keystore-gegevens Keystore geïmporteerd Kan keystore niet importeren @@ -554,6 +570,7 @@ De afbeelddimensionen moeten als volgt zijn: Exporteer de huidige keystore Geen keystore beschikbaar om te exporteren Keystore geëxporteerd + Kon de keystore niet exporteren Toon wachtwoord Wachtwoord verbergen @@ -682,7 +699,8 @@ De afbeelddimensionen moeten als volgt zijn: Toestaan Details Verbergen - Standaard + Verborgen + Toon Logboeken Bron-URL Installeren… diff --git a/app/src/main/res/values-or-rIN/strings.xml b/app/src/main/res/values-or-rIN/strings.xml index 180ea1689..48f97ef60 100644 --- a/app/src/main/res/values-or-rIN/strings.xml +++ b/app/src/main/res/values-or-rIN/strings.xml @@ -39,10 +39,12 @@ + + diff --git a/app/src/main/res/values-pa-rIN/strings.xml b/app/src/main/res/values-pa-rIN/strings.xml index 180ea1689..48f97ef60 100644 --- a/app/src/main/res/values-pa-rIN/strings.xml +++ b/app/src/main/res/values-pa-rIN/strings.xml @@ -39,10 +39,12 @@ + + diff --git a/app/src/main/res/values-pl-rPL/plurals.xml b/app/src/main/res/values-pl-rPL/plurals.xml index d6949e2aa..34e93ce31 100644 --- a/app/src/main/res/values-pl-rPL/plurals.xml +++ b/app/src/main/res/values-pl-rPL/plurals.xml @@ -25,10 +25,10 @@ %s pakietów - Wykonaj %s modyfikację - Wykonaj %s modyfikacje - Wykonaj %s modyfikacji - Wykonaj %s modyfikacji + Zastosowano %s modyfikację + Zastosowano %s modyfikacje + Zastosowano %s modyfikacji + Zastosowano %s modyfikacji %s plik APK @@ -54,4 +54,16 @@ %s wybranych modyfikacji %s wybranych modyfikacji + + Pokaż %s aplikację + Pokaż %s aplikacje + Pokaż %s aplikacji + Pokaż %s aplikacji + + + %s ukryta aplikacja + %s ukryte aplikacje + %s ukrytych aplikacji + %s ukrytych aplikacji + diff --git a/app/src/main/res/values-pl-rPL/strings.xml b/app/src/main/res/values-pl-rPL/strings.xml index 5fcd8ef0c..36b69ad29 100644 --- a/app/src/main/res/values-pl-rPL/strings.xml +++ b/app/src/main/res/values-pl-rPL/strings.xml @@ -32,6 +32,8 @@ Brak dostępnych aplikacji Dodaj źródło modyfikacji lub włącz istniejące w %s Brak wyników dla \"%1$s\" + Wszystkie aplikacje ukryte + Ukryto wszystkie aplikacje na ekranie głównym Jaką aplikację chcesz zmodyfikować? @@ -91,10 +93,14 @@ Aby zmodyfikować <b>%s</b>, potrzebujesz niezmodyfikowanego pliku APK w jednej z poniższych wersji: Zalecana Niezmodyfikowana + Niezgodna Tak, pomóż mi znaleźć plik APK Nie, mam już plik APK Użyj zapisanego pliku APK (v%s) Brak zapisanego pliku APK. Modyfikacja będzie wymagać ponownego wybrania pliku + Wymaga Androida %1$s+ + Niewspierane na tym urządzeniu + <b>%1$s</b> nie ma wersji wspieranych z Androidem %2$d (API %3$d). Wszystkie zadeklarowane wersje wymagają nowszego systemu Android Pobierz oryginalny plik APK Instrukcja: @@ -221,6 +227,7 @@ Aby uzyskać najlepszy efekt, zalecamy zmodyfikowanie <b>pełnego pliku AP Dostępna jest nowsza wersja modyfikacji. Zmodyfikuj aplikację ponownie, aby otrzymać najnowsze ulepszenia i poprawki Aplikacja została odinstalowana Ta aplikacja została odinstalowana poza Morphe. Zmodyfikuj ją ponownie, aby przywrócić jej funkcjonalność + Rozmiar pliku APK Tryb root wymaga zainstalowania oryginalnego pliku APK na urządzeniu przed zmodyfikowaniem Wsparcie dla GmsCore jest wyłączone w trybie root @@ -308,16 +315,13 @@ Aby uzyskać najlepszy efekt, zalecamy zmodyfikowanie <b>pełnego pliku AP Nie udało się zastosować %s Zapisano zmodyfikowaną aplikację na później Nie udało się zapisać zmodyfikowanej aplikacji - Pobierz plik APK Wymagana jest interakcja użytkownika, aby kontynuować działanie tej wtyczki Proces modyfikatora zakończył się kodem %1$s Zainstalowano pomyślnie Nie udało się zainstalować aplikacji: %s - Instalacja nie została zakończona. Sprawdź okno instalatora systemowego i spróbuj ponownie Zapisano plik APK Nie udało się wyeksportować zmodyfikowanej aplikacji Usunięto zapisaną zmodyfikowaną aplikację - Usunięto zapisaną kopię Zainstaluj aplikację, aby móc ją otworzyć Nazwa pakietu Nie udało się zamontować: %s @@ -337,6 +341,10 @@ Aby uzyskać najlepszy efekt, zalecamy zmodyfikowanie <b>pełnego pliku AP Kratka Cząsteczki Brak + Losowy + Przy uruchomieniu + Codziennie + Co 3 dni Efekt paralaksy Płynne przesuwanie tła przy nachyleniu urządzenia @@ -353,6 +361,10 @@ Aby uzyskać najlepszy efekt, zalecamy zmodyfikowanie <b>pełnego pliku AP Bieżący język Tłumaczenia dla niektórych języków mogą być niekompletne lub może ich brakować Aby przetłumaczyć aplikację na nowe języki lub poprawić istniejące tłumaczenia, odwiedź stronę %s + + Ekran główny + Powitania + Wyświetlaj powitanie na ekranie głównym Ikona aplikacji Domyślna @@ -399,8 +411,17 @@ Aby uzyskać najlepszy efekt, zalecamy zmodyfikowanie <b>pełnego pliku AP Włącz zaawansowane ustawienia modyfikacji oraz opcje personalizacji Morphe Włączyć tryb ekspercki? Tryb ekspercki zapewnia większą kontrolę nad procesem modyfikacji, jednak błędna konfiguracja ustawień może sprawić, że aplikacja przestanie działać - Usuń nieużywane biblioteki natywne - Usuń biblioteki natywne dla nieobsługiwanych architektur CPU ze zmodyfikowanych aplikacji + Optymalizacja pod architekturę + "Podczas scalania podzielonych plików APK pomija zbędne architektury, wersje językowe i gęstości ekranu. +W przypadku zwykłych plików APK, po zmodyfikowaniu usuwa nieobsługiwane biblioteki natywne" + + Tryb przetwarzania kodu bajtowego + Określa sposób przetwarzania kodu bajtowego podczas modyfikowania. Wpływa to na szybkość procesu, zużycie pamięci i końcowy rozmiar pliku APK + Szybki (Zalecane) + Szybki + Szybsze modyfikowanie i mniejsze zużycie pamięci kosztem większego pliku APK. Zalecane dla starszych urządzeń z małą ilością RAM-u + Pełny + Wolniejsze modyfikowanie klasyczną metodą. Używaj tylko wtedy, gdy tryb szybki powoduje problemy Token GitHub Wymagany dla źródeł z Pull Requestów @@ -510,13 +531,7 @@ Wymagane są następujące wymiary obrazów: Zainstalowana wersja Shizuku nie jest wspierana Otwórz Shizuku Instalacja nie powiodła się. Spróbuj ponownie lub wybierz inny instalator - Instalacja została zablokowana przez Androida. Sprawdź Play Protect lub ustawienia bezpieczeństwa Inna aplikacja o tej samej nazwie pakietu jest już zainstalowana. Odinstaluj ją, aby kontynuować - Plik APK nie jest zgodny z tym urządzeniem lub wersją Androida - Plik APK jest nieprawidłowy lub uszkodzony - Za mało miejsca w pamięci urządzenia, aby ukończyć instalację - Przekroczono czas instalacji. Spróbuj ponownie - Instalacja została anulowana Otwieranie %1$s… %1$s nie potwierdził instalacji. Sprawdź inną aplikację i spróbuj ponownie %1$s potwierdził pomyślną instalację @@ -547,6 +562,7 @@ Wymagane są następujące wymiary obrazów: Nazwa użytkownika (Alias) Hasło Importuj + Format magazynu kluczy Nieprawidłowe dane magazynu kluczy Zaimportowano magazyn kluczy Nie udało się zaimportować magazynu kluczy @@ -554,6 +570,7 @@ Wymagane są następujące wymiary obrazów: Eksportuj bieżący magazyn kluczy Brak magazynu kluczy do eksportu Wyeksportowano magazyn kluczy + Nie udało się wyeksportować magazynu kluczy Pokaż hasło Ukryj hasło @@ -682,7 +699,8 @@ Wymagane są następujące wymiary obrazów: Zezwól Szczegóły Ukryj - Domyślne + Ukryte + Odkryj Logi Źródła Instalowanie… diff --git a/app/src/main/res/values-pt-rBR/plurals.xml b/app/src/main/res/values-pt-rBR/plurals.xml index 9c09cfade..1ab5d8034 100644 --- a/app/src/main/res/values-pt-rBR/plurals.xml +++ b/app/src/main/res/values-pt-rBR/plurals.xml @@ -5,8 +5,8 @@ %s patches - %s opção de patch - %s opções de patch + %s Opção de patch + %s Opções de patch %s fonte @@ -17,8 +17,8 @@ %s pacotes - Executar %s patch - Executar %s patches + Patch %s executado + Patches %s executados %s arquivo APK @@ -36,4 +36,12 @@ %s patch selecionado %s patches selecionados + + Mostrar %s aplicativo + Mostrar %s aplicativos + + + %s aplicativo oculto + %s aplicativos ocultos + diff --git a/app/src/main/res/values-pt-rBR/strings.xml b/app/src/main/res/values-pt-rBR/strings.xml index 9010cc744..81aa75b15 100644 --- a/app/src/main/res/values-pt-rBR/strings.xml +++ b/app/src/main/res/values-pt-rBR/strings.xml @@ -7,22 +7,22 @@ Esconder este aplicativo? Este aplicativo será escondido da tela inicial. Você pode restaurá-lo mais tarde usando o botão \'%1$s\' no final da lista Este aplicativo será escondido - Mostrar aplicativos ocultos - Não há aplicativos ocultos + Mostrar aplicativos escondidos + Não há aplicativos escondidos Aplicativos ocultos Toque para exibir Bug do Android 11 A permissão de instalação do aplicativo deve ser concedida antecipadamente para evitar um bug no sistema Android 11 que afetará negativamente a experiência do usuário. Adicionar fonte - Você deseja adicionar esta fonte de patch ao Morphe? - Adicione fonte apenas de fontes confiáveis + Deseja adicionar esta fonte de correção ao Morphe? + Adicione uma fonte apenas de fontes em que você confia Atualização do Morphe disponível Nova versão pronta para instalação Falha na importação Verifique sua conexão com a internet e tente novamente Atualizações ignoradas - Dados móveis ativos + Dados móveis estão ativos Atualizando fontes… %1$s processado Atualização de fontes concluída @@ -30,40 +30,42 @@ Carregando fontes, por favor aguarde… Pesquisar aplicativos Nenhum aplicativo disponível - Adicione uma fonte de patch ou habilite uma existente em %s + Adicione uma fonte de correção ou habilite uma existente em %s Nenhum aplicativo corresponde a \"%1$s\" + Todos os aplicativos estão ocultos + Você ocultou todos os aplicativos da tela inicial - Em qual app você quer aplicar os patches? + Qual aplicativo você gostaria de corrigir? - Pronto para tornar os apps lendários novamente? + Pronto para tornar aplicativos lendários novamente? - Aperte os cintos, é hora de corrigir + Aperte os cintos, é hora da correção - Mais um dia, mais uma obra-prima com bloqueio de anúncios + Mais um dia, mais uma obra-prima com anúncios bloqueados Quer assistir a anúncios? Eu também não Vamos fazer seus apps se comportarem - Previsão de hoje: 100% de chance de patch + Previsão de hoje: 100% de chance de correção - Pronto para transformar o medíocre em incrível? + Pronto para transformar o frustrante em sensacional? Salvando apps de si mesmos desde hoje - Impulsionado por cafeína e bons remendos + Movido a cafeína e boas correções - Fontes de emenda + Fontes de correção Abrir links no navegador Falha ao abrir URL - Usar patches pré-lançamento - Receber acesso antecipado a novas versões experimentais de patches - Utilizar versões experimentais do app - Aplicar patches nas versões experimentais do aplicativo, se disponíveis + Usar correções de pré-lançamento + Receber acesso antecipado a novas versões experimentais de correções + Usar versões experimentais de aplicativos + Corrigir alvos de aplicativo experimentais, se disponível Esta fonte já foi adicionada Nome de exibição - Renomear origem de patch + Renomear origem de correção Já existe uma origem de patch com esse nome Não foi possível atualizar esta fonte de patch. Qualquer versão @@ -91,10 +93,14 @@ Para aplicar os patches em <b>%s</b>, você precisa de um APK não corrigido com a versão: Recomendado Sem patches + Incompatível Sim, me ajude a encontrar um APK Não, eu já tenho um APK Use saved APK (v%s) Não há APK salvo. O patch exigirá a seleção do arquivo APK novamente + Requer Android %1$s+ + Não suportado neste dispositivo + <b>%1$s</b> não possui versões compatíveis para o Android %2$d (API %3$d). Todas as versões declaradas exigem uma versão mais recente do Android Baixar o APK original Instruções: @@ -221,6 +227,7 @@ Para obter os melhores resultados, este aplicativo recomenda corrigir um(a) < Uma versão mais nova dos patches está disponível. Repatch seu app para obter as últimas melhorias e correções App desinstalado Este app foi desinstalado fora do Morphe. Reparche-o para restaurar a funcionalidade + Tamanho do APK O modo raiz requer que o APK original esteja instalado em seu dispositivo antes do patch O patch de suporte do GmsCore está excluído no modo root @@ -308,16 +315,13 @@ Para obter os melhores resultados, este aplicativo recomenda corrigir um(a) < Falha ao aplicar %s App patcheado salvo para mais tarde Falha ao salvar o app patcheado - Baixar arquivo APK Interação do usuário é necessária para prosseguir com este plugin Processo do Patcher saiu com código %1$s Instalado com sucesso Falha ao instalar o app: %s - A instalação não foi concluída. Verifique a janela do instalador do sistema e tente novamente APK Salvo Falha ao exportar o app patcheado App patcheado salvo removido - Cópia salva removida Instale o app antes de abri-lo Nome do pacote Falha ao montar: %s @@ -337,6 +341,10 @@ Para obter os melhores resultados, este aplicativo recomenda corrigir um(a) < Grade Partículas Nenhum + Aleatório + Na inicialização + Diariamente + A cada 3 dias Efeito de paralaxe Deslocamento suave do fundo ao inclinar o dispositivo @@ -353,6 +361,10 @@ Para obter os melhores resultados, este aplicativo recomenda corrigir um(a) < Idioma atual As traduções para alguns idiomas podem estar ausentes ou incompletas Para traduzir novos idiomas ou aperfeiçoar as traduções existentes, visite %s + + Tela inicial + Frases de saudação + Mostrar uma mensagem de saudação na tela inicial Ícone do app Padrão @@ -399,8 +411,17 @@ Para obter os melhores resultados, este aplicativo recomenda corrigir um(a) < Habilitar configurações avançadas de patch e opções de personalização Habilitar o modo expert? O modo expert oferece mais controle sobre como os patches são aplicados, mas a má configuração de settings no modo expert pode resultar em um aplicativo não funcional - Remover bibliotecas nativas desnecessárias - Excluir bibliotecas nativas para arquiteturas de CPU não suportadas a partir de aplicativos patcheados + Otimizar para a arquitetura do dispositivo + "Ignorar módulos APK divididos para arquiteturas de CPU, localidades e densidades de tela não suportadas durante a mesclagem. +Para APKs simples, remove bibliotecas nativas para arquiteturas não suportadas após o patching" + + Modo de processamento de bytecode + Controla como o bytecode é processado durante o patching. Afeta a velocidade do patching, o uso de memória e o tamanho do APK de saída + Rápido (Recomendado) + Rápido + Correção mais rápida e menor uso de memória, à custa de um APK maior. Recomendado para dispositivos com pouca RAM ou mais antigos + Completo + Correção mais lenta, replica o comportamento legado. Use apenas se o modo rápido causar problemas Token de Acesso Pessoal da GitHub (PAT) Exigido para fontes de solicitação pull @@ -510,13 +531,7 @@ As dimensões das imagens devem ser as seguintes: A versão do Shizuku instalada não é suportada Abrir Shizuku A instalação falhou. Tente novamente ou mude para um instalador diferente - A instalação foi bloqueada pelo Android. Verifique o Play Protect ou as configurações de segurança Outro aplicativo com este nome de pacote já está instalado. Desinstale-o antes de continuar - O APK não é compatível com este dispositivo ou versão do Android - O APK é inválido ou corrompido - Não há espaço de armazenamento suficiente para a instalação - A instalação atingiu o tempo limite. Tente novamente - A instalação foi cancelada Abrindo %1$s… %1$s não confirmou a instalação. Verifique o outro aplicativo e tente novamente %1$s relatou uma instalação bem-sucedida @@ -547,6 +562,7 @@ As dimensões das imagens devem ser as seguintes: Nome de usuário (Alias) Senha Importar + Formato do Keystore Credenciais do keystore incorretas Keystore importado Falha ao importar keystore @@ -554,6 +570,7 @@ As dimensões das imagens devem ser as seguintes: Exportar o keystore atual Não há keystore disponível para exportação Keystore exportado + Falha ao exportar o armazenamento de chaves Mostrar senha Esconder senha @@ -682,7 +699,8 @@ As dimensões das imagens devem ser as seguintes: Permitir Detalhes Ocultar - Padrão + Oculto + Exibir Registros Fontes Instalando… diff --git a/app/src/main/res/values-pt-rPT/plurals.xml b/app/src/main/res/values-pt-rPT/plurals.xml index fe2445105..4ea09a916 100644 --- a/app/src/main/res/values-pt-rPT/plurals.xml +++ b/app/src/main/res/values-pt-rPT/plurals.xml @@ -17,8 +17,8 @@ %s pacotes - Executar %s patch - Executar %s patches + Patch %s executado + Patches %s executados %s ficheiro APK @@ -36,4 +36,12 @@ %s patch selecionado %s patches selecionados + + Mostrar o aplicativo %s + Mostrar os aplicativos %s + + + %s aplicativo oculto + %s aplicativos ocultos + diff --git a/app/src/main/res/values-pt-rPT/strings.xml b/app/src/main/res/values-pt-rPT/strings.xml index 5ca6e43c8..aad7c8c99 100644 --- a/app/src/main/res/values-pt-rPT/strings.xml +++ b/app/src/main/res/values-pt-rPT/strings.xml @@ -32,6 +32,8 @@ Nenhum aplicativo disponível Adicionar uma fonte de patch ou habilitar uma existente em %s Nenhum aplicativo corresponde a \"%1$s\" + Todos os aplicativos estão ocultos + Você ocultou todos os aplicativos da tela inicial Que aplicativo você quer corrigir? @@ -91,10 +93,14 @@ Para corrigir <b>%s</b>, vocã precisa de um APK sem correções das versões: Recomendado Não atualizado + Incompatível Sim, me ajude a encontrar um APK Não, já tenho um APK Usar APK salvo (v%s) Não há APK salvo. O patch exigirá a seleção do arquivo APK novamente + Requer Android %1$s+ + Não suportado neste dispositivo + <b>%1$s</b> não tem versões compatíveis para o Android %2$d (API %3$d). Todas as versões declaradas requerem uma versão mais recente do Android Baixar o APK original Instruções: @@ -221,6 +227,7 @@ Para obter os melhores resultados, este aplicativo recomenda corrigir um(s) < Há uma versão mais nova das emendas disponível. Repare seu aplicativo para obter as últimas melhorias e correções App desinstalado Este app foi desinstalado fora do Morphe. Reparche-o para restaurar a funcionalidade + Tamanho do APK O modo raiz requer que o APK original esteja instalado em seu dispositivo antes do patch. O patch de suporte GmsCore está excluído no modo root @@ -308,16 +315,13 @@ Para obter os melhores resultados, este aplicativo recomenda corrigir um(s) < Falha ao aplicar %s App modificado salvo para mais tarde Falha ao salvar app modificado - Baixar arquivo APK Interação do usuário é necessária para prosseguir com este plugin. Processo do Patcher saiu com código %1$s Instalado com sucesso Falha ao instalar o app: %s - A instalação não foi concluída. Verifique o diálogo do instalador do sistema e tente novamente. APK Salvo Falha ao exportar o app modificado App modificado salvo removido - Cópia salva removida Instale o aplicativo antes de abri-lo. Nome do pacote Falha ao montar: %s @@ -337,6 +341,10 @@ Para obter os melhores resultados, este aplicativo recomenda corrigir um(s) < Grade Partículas Nenhum + Aleatório + Ao iniciar + Diariamente + A cada 3 dias Efeito de paralaxe Deslocamento suave de fundo ao inclinar o dispositivo @@ -353,6 +361,10 @@ Para obter os melhores resultados, este aplicativo recomenda corrigir um(s) < Língua atual As traduções para alguns idiomas podem estar ausentes ou incompletas Para traduzir novos idiomas ou melhorar as traduções existentes, visite %s + + Tela inicial + Frases de saudação + Exibir uma mensagem de saudação na tela inicial Ícone da aplicação Padrão @@ -399,8 +411,17 @@ Para obter os melhores resultados, este aplicativo recomenda corrigir um(s) < Habilitar configurações avançadas de patch e opções de personalização do Morphe Habilitar o modo Expert? O modo Expert concede mais controle sobre como os patches são aplicados, mas a má configuração de ajustes no modo Expert pode resultar em um aplicativo não funcional - Remover bibliotecas nativas desnecessárias - Excluir bibliotecas nativas para arquiteturas de CPU não suportadas dos aplicativos patched + Otimizar para a arquitetura do dispositivo + "Ignorar módulos APK divididos para arquiteturas de CPU, locais e densidades de tela não suportados durante a mesclagem. +Para APKs simples, remove bibliotecas nativas para arquiteturas não suportadas após o patching" + + Modo de processamento de bytecode + Controla como o bytecode é processado durante o patching. Afeta a velocidade do patching, o uso de memória e o tamanho do APK de saída + Rápido (Recomendado) + Rápido + Correção mais rápida e menor uso de memória, à custa de um APK maior. Recomendado para dispositivos com pouca memória RAM ou mais antigos + Completo + Correção mais lenta, replica o comportamento legado. Use somente se o modo rápido causar problemas GitHub PAT Requerido para fontes de solicitação pull @@ -510,13 +531,7 @@ As dimensões das imagens devem ser as seguintes: A versão do Shizuku instalada não é suportada Abrir Shizuku A instalação falhou. Tente novamente ou mude para um instalador diferente - A instalação foi bloqueada pelo Android. Verifique o Play Protect ou as configurações de segurança Outro aplicativo com este nome de pacote já está instalado. Desinstale-o antes de continuar - O APK não é compatível com este dispositivo ou versão do Android - O APK é inválido ou corrompido - Não há espaço suficiente no armazenamento para a instalação - A instalação esgotou o tempo. Tente novamente - A instalação foi cancelada Abrindo %1$s… %1$s não confirmou a instalação. Verifique o outro aplicativo e tente novamente %1$s relatou uma instalação bem-sucedida @@ -547,6 +562,7 @@ As dimensões das imagens devem ser as seguintes: Nome de usuário (Alias) Senha Importar + Formato Keystore Credenciais de keystore incorretas Keystore importado Falha ao importar keystore @@ -554,6 +570,7 @@ As dimensões das imagens devem ser as seguintes: Exportar o keystore atual Não há keystore disponível para exportar Keystore exportado + Falha ao exportar o armazenamento de chaves Mostrar senha Esconder senha @@ -682,7 +699,8 @@ As dimensões das imagens devem ser as seguintes: Permitir Detalhes Ocultar - Padrão + Oculto + Exibir Registros Fontes Instalando… diff --git a/app/src/main/res/values-ro-rRO/plurals.xml b/app/src/main/res/values-ro-rRO/plurals.xml index 0ea9e18c7..98193d5d1 100644 --- a/app/src/main/res/values-ro-rRO/plurals.xml +++ b/app/src/main/res/values-ro-rRO/plurals.xml @@ -1,13 +1,13 @@ - %s hârtie - %s hârtie + %s patch + %s patch-uri %s de patch-uri - %s opțiune de hârtie - %s opțiuni de hârtie + %s opțiune de patch + %s opțiuni de patch %s de opțiuni de patch @@ -21,9 +21,9 @@ %s de pachete - Execute %s patch - Execute %s patches - Execută %s de patch-uri + A executat patch-ul %s + A executat %s de patch-uri + A executat %s de patch-uri %s fișier APK @@ -33,7 +33,7 @@ A fost eliminat %s patch vechi din selecție salvată Eliminat %s patch-uri vechi din selecție salvată - Eliminat %s patch-uri vechi din selecție salvată + Au fost eliminate %s de patch-uri vechi din selecție salvată %s sursă instalată @@ -45,4 +45,14 @@ %s patch-uri selectate %s de patch-uri selectate + + Arată %s aplicație + Arată %s aplicații + Arată %s de aplicații + + + %s aplicație ascunsă + %s aplicații ascunse + %s aplicații ascunse + diff --git a/app/src/main/res/values-ro-rRO/strings.xml b/app/src/main/res/values-ro-rRO/strings.xml index e51034103..94c5790c2 100644 --- a/app/src/main/res/values-ro-rRO/strings.xml +++ b/app/src/main/res/values-ro-rRO/strings.xml @@ -32,6 +32,8 @@ Nicio aplicație disponibilă Adaugă o sursă de patch sau activează una existentă în %s Nicio aplicație nu se potrivește cu \"%1$s\" + Toate aplicațiile sunt ascunse + Ați ascuns toate aplicațiile de pe ecranul principal Ce aplicație dorești să patch-uiești? @@ -91,10 +93,14 @@ Pentru a patch-ui <b>%s</b>, ai nevoie de un APK de versiuni nepatch-uit: Recomandat Nepatch-uit + Incompatibil Da, ajută-mă să găsesc un APK Nu, am deja un APK Folosește APK salvat (v%s) Niciun APK salvat. Patch-uirea necesită selectarea APK-ului din nou + Necesită Android %1$s+ + Nu este suportat pe acest dispozitiv + <b>%1$s</b> nu are versiuni suportate pentru Android %2$d (API %3$d). Toate versiunile declarate necesită o versiune Android superioară Descarcă APK-ul original Instrucțiuni: @@ -221,6 +227,7 @@ Pentru cele mai bune rezultate, această aplicație recomandă patch-uirea unui O versiune mai nouă a modificărilor este disponibilă. Repătați aplicația pentru a obține ultimele îmbunătățiri și corectări Aplicația a fost dezinstalată Această aplicație a fost dezinstalată în afara Morphe. Aplica-o din nou pentru a restabili funcționalitatea + Dimensiunea APK Modul radical necesită instalarea APK-ului original pe dispozitivul dvs. înainte de a pata Suportul GmsCore este exclus în modul root @@ -271,7 +278,7 @@ Pentru cele mai bune rezultate, această aplicație recomandă patch-uirea unui Se semnează fișierul APK modificat Se aplică modificările… - Atingeți pentru a reveni la instrumentul de modificare + Atinge pentru a reveni la patcher Opriți instrumentul de modificare Sigur doriți să opriți procesul de modificare? @@ -304,20 +311,17 @@ Pentru cele mai bune rezultate, această aplicație recomandă patch-uirea unui Se patch-uiește aplicația Utilizare memorie - Aplicarea modificărilor + Se aplică patch-urile Nu s-a putut aplica %s Aplicație modificată salvată pentru mai târziu Eroare la salvarea aplicației modificate - Descarcă fișierul APK Este necesară interacțiunea utilizatorului pentru a continua cu acest plugin Procesul de patch a ieșit cu codul %1$s Instalat cu succes Nu s-a putut instala aplicația: %s - Instalarea nu s-a finalizat. Verificați dialogul instalatorului sistemului și reîncercați APK salvat Nu s-a putut exporta aplicația modificată Aplicație modificată salvată eliminată - Copie salvată eliminată Instalează aplicația înainte de deschiderea ei Nume pachet Montare eșuată: %s @@ -337,22 +341,30 @@ Pentru cele mai bune rezultate, această aplicație recomandă patch-uirea unui Grilã Particule Fără + Aleatoriu + La lansare + Zilnic + La fiecare 3 zile Efect parallax Schimbare suedea a fundalului când se înclina dispozitivul Temă Sistem - Luminos - Întunecat - Design material + Luminoasă + Întunecată + Material You Culoare accent Negru pur - Folosește fonduri negre adevărate + Folosește fundaluri negre pure Limbă aplicație - Limbajul curent + Limba curentă Traduțiile pentru unele limbi pot lipsi sau să fie incomplet Pentru a traduce în noi limbi sau să îmbunătățești traducerile existente, vizitează %s + + Ecranul principal + Fraze de salut + Afișează un mesaj de salut pe ecranul principal Pictogramă aplicație Implicit @@ -399,8 +411,17 @@ Pentru cele mai bune rezultate, această aplicație recomandă patch-uirea unui Activează setările complexe de pateâturi Morphe si opârii de personalizare Vrei sâ activezi modul expert? Modul expert oferâ mai mult control asupra modului cum pateâturile sunt aplicaâ, dar o configurare greşită a setârilor poate duce la o aplicaâ nefuncțională - Elimină bibliotecile native nefolosite - Șterge bibliotecile native pentru arhitecturile CPU nesuportate din aplicațiile patch-uite + Optimize pentru arhitectura dispozitivului + "Omite modulele APK split pentru arhitecturi CPU, locale și densități de ecran nesuportate în timpul îmbinării. +Pentru APK-urile plain, elimină bibliotecile native pentru arhitecturi nesuportate după patch" + + Mod de procesare bytecode + Controlează modul în care bytecode-ul este procesat în timpul patch-ului. Afectează viteza de patch, utilizarea memoriei și dimensiunea APK-ului de ieșire + Rapid (Recomandat) + Rapid + Reparare mai rapidă și utilizare mai mică a memoriei, în detrimentul unui APK mai mare. Recomandat pentru dispozitive cu memorie RAM redusă sau mai vechi + Complet + Reparare mai lentă, reproduce comportamentul legacy. Utilizați numai dacă modul rapid cauzează probleme Jeton GitHub Nevoie pentru surse de cerere de preluare @@ -419,7 +440,7 @@ Pentru cele mai bune rezultate, această aplicație recomandă patch-uirea unui Culori tema aplicației Schimbă culoarea de fund - Culoarea temă de fund dark + Culoare de fundal temă întunecată Culoarea temă de fund light Poate fi un culoare hexadecimal (#RRGGBB) sau o referință la resursa culoare Se încarcă opțiunile de patach… @@ -510,13 +531,7 @@ Dimensiunile imaginilor trebuie să fie următoarele: Versiunea Shizuku instalată nu este suportată Deschide Shizuku Instalarea a eșuat. Reîncercați sau schimbați la un instalator diferit - Instalarea a fost blocată de Android. Verificați Play Protect sau setările de securitate Altă aplicație cu acest nume de pachet este deja instalată. Dezinstalați-o înainte de continuare - APK-ul nu este compatibil cu acest dispozitiv sau versiunea Android - APK-ul este invalid sau corupt - Nu există suficient spațiu de stocare pentru instalarea - Instalarea a expirat. Încearcă din nou - Instalarea a fost anulată Se deschide %1$s… %1$s nu a confirmat instalarea. Verificați altă aplicație și reîncercați %1$s a raportat o instalare reușită @@ -528,32 +543,34 @@ Dimensiunile imaginilor trebuie să fie următoarele: Afișează dialogul de selecție a instalatorului fiecare dată când instalați o aplicație patată Performanță - Timp de rulare - Folosește patachizare în proces separat pentru o stabilitate mai bună + Runtime proces + Rulează patch-uirea în proces separat pentru o stabilitate mai bună Limita memorie - Limite memorie mai înalte pot accelera patachizarea dar pot nu funcționa cu dispozitive mai vechi - Activează rulare proces + Limite de memorie mai mari pot accelera patch-uirea, dar pot să nu funcționeze pe dispozitive mai vechi + Activează proces runtime Memorie disponibilă pentru patachizare O limită memorie joasă poate cauza eșecuri când patachizați aplicații mai mari Limită %s MO Clic pentru a configura Această funcție necesită Android 11 sau o versiune mai recentă - Importă și exportă - Importă cheia de semnă + Importare și exportare + Importă keystore Importă o cheie de semnă personalizată Introduceți datele cheii Vă veți trebi să introduceți credențialele keystore-ului pentru a-l importa Nume (Alias) Parolă Importă + Format Keystore Datele cheii incorecte - Cheia de semnă importată - Nu s-a putut importa cheia de semnă - Exportă cheia de semnă - Exportă cheia de semnă curentă + Keystore importat + A eșuat importarea keystore-ului + Exportă keystore + Exportă keystore-ul curent Nu există cheie de semnă disponibilă pentru export - Cheia de semnă exportată + Keystore exportat + A eșuat exportul keystore-ului Arată parolă Ascunde parolă @@ -682,15 +699,16 @@ Dimensiunile imaginilor trebuie să fie următoarele: Permite Detalii Ascunde - Implicit + Ascuns + Dezascunde Jurnale - De surse + Surse Se instalează… Se montează… Descarcare în curs… Importare în curs… Importate cu succes - Vizualizați jurnalele modificărilor + Vizualizează jurnalele modificărilor Explorează ultimele schimbări din această actualizare Nu s-a putut descărca jurnalul de modificări: %s Reîncearcă diff --git a/app/src/main/res/values-ru-rRU/plurals.xml b/app/src/main/res/values-ru-rRU/plurals.xml index 3cd3e1996..007867cfe 100644 --- a/app/src/main/res/values-ru-rRU/plurals.xml +++ b/app/src/main/res/values-ru-rRU/plurals.xml @@ -25,10 +25,10 @@ %s пакетов - Выполнить %s патч - Выполнить %s патчи - Выполнить %s патчей - Выполнить %s патчей + Выполнен %s патч + Выполнены %s патча + Выполнены %s патчи + Выполнены %s патчи %s APK файл @@ -54,4 +54,16 @@ %s патчей выбрано %s патчей выбрано + + Показать %s приложение + Показать %s приложения + Показать %s приложений + Показать %s приложений + + + %s скрытое приложение + %s скрытые приложения + %s скрытых приложений + %s скрытых приложений + diff --git a/app/src/main/res/values-ru-rRU/strings.xml b/app/src/main/res/values-ru-rRU/strings.xml index 506d92b87..bc67eeb76 100644 --- a/app/src/main/res/values-ru-rRU/strings.xml +++ b/app/src/main/res/values-ru-rRU/strings.xml @@ -18,7 +18,7 @@ Добавляйте источники только из источников, которым вы доверяете Доступно обновление Morphe - Готово к установке + Новая версия готова к установке. Обновление не удалось Проверьте подключение к Интернету и повторите попытку Обновления пропущены @@ -32,8 +32,10 @@ Нет доступных приложений Добавить источник патчей или включить существующий в %s Нет соответствующих приложений \"%1$s\" + Все приложения скрыты + Вы скрыли все приложения с главного экрана - Какое приложение вы хотите пропатчить? + Какое приложение вы хотите пропатчить Готовы сделать приложения снова легендарными? @@ -78,7 +80,7 @@ Выберите исходный файл патча .mpp из хранилища Изменить файл Предварительно установлено - Обновление успешно + Успешно обновлено Нет доступных обновлений Не удалось загрузить источники: %s Не удалось загрузить \"%1$s\": %2$s @@ -91,10 +93,14 @@ Чтобы патчить <b>%s</b>, вам нужен непатченый APK с версией: Рекомендуемый Непатченый + Несовместимый Да, помогите мне найти APK Нет, у меня уже есть APK Сохраненный APK (v%s) Нет сохраненного APK. Для патча нужно будет снова выбрать APK файл + Требуется Android %1$s+ + Не поддерживается на этом устройстве + <b>%1$s</b> не имеет поддерживаемых версий для Android %2$d (API %3$d). Все объявленные версии требуют более высокую версию Android Загрузить оригинальный APK Инструкции: @@ -109,7 +115,7 @@ Выберите загруженный APK Выберите файл <b>%1$s</b> APK, который вы только что загрузили - Выбрать APK файл + Откройте APK-файл Рекомендуемая версия Выбранная версия Выбрать APK @@ -125,7 +131,7 @@ Включить все Включить рекомендованные патчи Восстановить сохраненный выбор - Нет найденных патчей + Патчи не найдены Универсальные патчи Новые Выбрано несколько источников патчей @@ -133,10 +139,10 @@ Вы действительно хотите продолжить?" - Неподтвержденный APK + Непроверенный APK APK, выбранный для <b>%s</b>, не соответствует ожидаемому сертификату подписи. Он мог быть изменен или получен из ненадежного источника Этот APK может быть не оригинальным - Обнаружено split APK + Обнаружен split APK "Этот файл является APK пакетом (<b>APKM / APKS / XAPK</b>). @@ -145,10 +151,10 @@ Экспериментальный Эта версия имеет экспериментальную поддержку и может быть нестабильной или неполной Хотите поэкспериментировать? 🧪 - Эта версия <b>%s</b> имеет раннюю экспериментальную поддержку<br/><br/>🔧 Ожидайте странного поведения приложения или необъяснимых ошибок, поскольку исправления уточняются для этой версии приложения + Эта версия <b>%s</b> имеет раннюю экспериментальную поддержку<br/><br/>🔧 В процессе доработки исправлений для этой версии приложения возможны странные сбои в работе приложения или неустановленные ошибки. Неподдерживаемая версия Этот APK не является рекомендуемой версией приложения. Продолжение может привести к некорректной работе приложения - Продолжить по-любому + Продолжить в любом случае. Совместимые версии: Неподдерживаемые Неверно выбранный пакет @@ -161,7 +167,7 @@ Обновить и патчить Недостаточно места на диске Доступно только %1$.2f GB свободного хранилища. Для патчинга требуется как минимум %2$.2f GB свободного места для корректной работы - Продолжение может привести к ошибке \"файл не найден\" или поврежденному выходному APK + Дальнейшие действия могут привести к ошибке \"файл не найден\" или к повреждению выходного APK-файла Свой цвет Hex цвет @@ -173,16 +179,16 @@ Это значение уже существует Значение не может быть пустым Значения еще не добавлены - %s: заполните все обязательные опции + %s: заполните все обязательные поля Создать адаптивную иконку - Выберите изображение переднего плана, выберите цвет фона, затем с помощью ползунка или щепок жестом, чтобы настроить предварительный просмотр + Выберите изображение переднего плана, выберите цвет фона, а затем используйте ползунок или жест масштабирования для настройки предварительного просмотра. Предварительный просмотр адаптивного значка - Для наилучшего результата сохраняйте свой значок внутри твердого внутреннего круга + Для достижения наилучшего результата разместите свой значок внутри сплошного внутреннего круга. Выбрать изображение Изменить изображение Цвет фона - Сбросить позицию и масштаб + Сбросить положение и масштаб Безопасная зона (всегда видна) Зона маски (может быть обрезано) Адаптивный значок создан успешно @@ -192,15 +198,15 @@ Используйте ползунок для изменения размера. Перетащите, чтобы переместить Создать собственный заголовок - Выберите изображения для светлой и темной тем, затем отрегулируйте жестами \" pinch-to-zoom \" + Выберите изображения для светлой и темной тем, затем отрегулируйте их с помощью жеста масштабирования Заголовок светлой темы Заголовок темной темы Изображение не выбрано - Заголовок создан успешно + Заголовок успешно создан Не удалось создать заголовок Выберите, где можно создать папку \'%1$s/%2$s\' со сгенерированными файлами заголовков - Вы действительно хотите деинсталировать это приложение? + Вы уверены, что хотите удалить это приложение? Это навсегда удалит: Запись в базе данных Патченный APK файл @@ -209,28 +215,29 @@ Патчи Безымянный Не удалось импортировать патчи: %s - Оригинальный APK не найден. Не удалось перепаковать это приложение + Оригинальный APK-файл не найден. Невозможно повторно пропатчить это приложение. Исходное имя пакета Примененные патчи - Использован источник патчей + Использованный источник патча Тип установки Пользовательский установщик Системный установщик Пропатчен Доступно обновление патчей - Доступна более новая версия патчей. Перепатчите своё приложение, чтобы получить последние улучшения и исправления - Приложение было деинсталировано - Это приложение было удалено за пределами Morphe. Пропатчите его снова, чтобы восстановить функциональность + Доступна более новая версия патчей. Обновите приложение, чтобы получить последние улучшения и исправления. + Приложение было удалено. + Это приложение было удалено вне устройства Morphe. Переустановите его, чтобы восстановить работоспособность. + Размер APK - Root режим требует установленный непатченный APK перед установкой патчей + Для использования режима root требуется предварительная установка оригинального APK-файла на ваше устройство перед применением патча. Поддержка GmsCore отключена в режиме root Выберите способ установки - Устройство имеет доступ root. Выберите, как вы хотите установить патченное приложение + Ваше устройство имеет root-доступ. Выберите способ установки пропатченного приложения. Root монтирование - Монтировать патченный APK поверх стандартного приложения. GmsCore не требуется + Установите пропатченный APK-файл поверх стандартного приложения. GmsCore не требуется. Стандартная установка - Установить как отдельное приложение с поддержкой GmsCore. Выберите установщик (Системный установщик, Shizuku и т.д.) после патчинга - Способ установки определяет, какие патчи включены и какие нельзя изменить после патчинга + Установите как отдельное приложение с поддержкой GmsCore. После установки патча выберите установщик (Системный установщик, Shizuku и т. д.) + Способ установки определяет, какие исправления будут включены, и его нельзя изменить после установки исправлений. Ошибка скопирована в буфер обмена Журнал ошибок @@ -255,22 +262,22 @@ Один или несколько параметров патча указывают на пути, которые Morphe не может прочесть. Предоставьте доступ к хранилищу и нажмите кнопку, после чего патч начнется автоматически Один или несколько параметров патча указывают на пути, к которым нельзя получить доступ. Обновите пути в параметрах патча и повторите попытку Предоставить доступ к хранилищу - Разрешение на хранение было отклонено. Разрешите его, чтобы использовать этот путь, или переместите файлы в директорию приложения - Открыть настройки хранения - Разрешение отклонено + Отказано в доступе к хранилищу. Предоставьте разрешение на использование этого пути или переместите файлы в личный каталог приложения. + Открыть настройки хранилища + Доступ запрещен Не найдено Совет: переместите файлы в /Android/data/app.morphe.manager/files/ - дополнительные разрешения не требуются Подготовка Загрузка патчей Объединение разделенных APK - Чтение файла APK + Чтение APK файла Патчинг Сохранение Запись патченного APK файла Подпись пропатченного APK-файла - Патчинг в процессе… + Идет установка исправлений… Нажмите, чтобы вернуться к патчеру Остановить патчер Вы уверены, что хотите остановить процесс патчинга? @@ -308,17 +315,14 @@ Не удалось применить %s Исправленное приложение сохранено для последующего использования Не удалось сохранить исправленное приложение - Скачать APK-файл Требуется взаимодействие пользователя для продолжения работы с этим плагином Процесс патчера завершился с кодом %1$s Успешно установлено Не удалось установить приложение: %s - Установка не завершилась. Проверьте диалог системного установщика и повторите попытку APK сохранен Не удалось экспортировать исправленное приложение - Удалено сохраненное пропатченное приложение - Удалена сохраненная копия - Установите приложение перед запуском + Удалено сохраненное исправленное приложение + Перед открытием приложения установите его на устройство Имя пакета Не удалось смонтировать: %s Не удалось размонтировать: %s @@ -337,6 +341,10 @@ Сетка Частицы Нет + Случайный + При запуске + Ежедневно + Каждые 3 дня Эффект параллакса Плавное смещение фона при наклоне устройства @@ -344,15 +352,19 @@ Системная Светлая Тёмная - Материальное «Вы» + Material You Цвет акцента Чисто черный Использовать настоящий черный фон - Язык приложения YouTube + Язык приложения Текущий язык Переводы для некоторых языков могут отсутствовать или быть неполными Для перевода на новые языки или улучшения существующих переводов посетите %s + + Главный экран + Приветственные фразы + Показывать приветственное сообщение на главном экране Иконка приложения По умолчанию @@ -368,7 +380,7 @@ Обновления Использовать предварительную версию Morphe Получите ранний доступ к новым функциям Morphe. Чтобы получить пред релиз патчи, включите переключатель пред релиз в каждом источнике патчей отдельно - Обновление от мобильных данных + Обновление информации о мобильном трафике Разрешить загрузку обновлений Morphe и патчей через мобильные данные Уведомления о фоновых обновлениях Периодически проверять наличие обновлений в фоновом режиме и сообщать об их доступности @@ -389,21 +401,30 @@ Разрешить уведомления Morphe требует разрешения на уведомление, чтобы сообщать вам о доступных обновлениях в фоновом режиме Хотите ли вы получать уведомления, когда будут доступны обновления? - Обновления уведомлений + Уведомления об обновлении Уведомления о новых версиях Morphe и патчах Патчинг Отображается во время установки Morphe патча для приложения в фоновом режиме - Настройки эксперта + Экспертные настройки Режим эксперта Включить комплексные настройки патчей Morphe и параметры настройки Включить режим эксперта? Экспертный режим предоставляет больше контроля над применением патчей, но неправильная настройка параметров в экспертном режиме может привести к нетрудоспособности программы. - Удалить неиспользуемые нативные библиотеки - Удалить нативные библиотеки для неподдерживаемых архитектур CPU из пропатченных приложений + Оптимизация для архитектуры устройства + "Пропускать split модули APK для неподдерживаемых CPU архитектур, локализаций и DPI экрана во время объединения. +Для обычных APK удаляет нативные библиотеки для неподдерживаемых архитектур после патча" + + Режим обработки байт-кода + Управление процессом обработки байт-кода при установке патчей. Влияет на скорость установки патчей, использование памяти и размер исходного APK + Быстро (Рекомендовано) + Быстро + Быстрее патчинг и меньшее использование памяти, но размер APK будет больше. Рекомендовано для устройств с малым объемом оперативной памяти или старых устройств + Полный + Более медленный патчинг, воспроизводит устаревшее поведение. Используйте только если быстрый режим вызывает проблемы GitHub Токен - Обязательно для источников запросов на слияние + Требуется для источников запросов на слияние. Токен настроен Как получить токен Установить Персональный Токен Доступа GitHub @@ -414,7 +435,7 @@ Параметры патчей Тема, брендинг, и параметры заголовка - Чтобы изменения параметров патчей вступили в силу, нужно повторно патчить приложение + Чтобы изменения параметров патчей вступили в силу, нужно повторно запустить приложение В режиме эксперта параметры патчей будут доступны во время процесса патчинга Цвета темы приложения @@ -423,11 +444,11 @@ Цвет фона для светлой темы Может быть hex-цвет (#RRGGBB) или ссылка на ресурс цвета Загрузка настроек патча... - Ожидать, пока источник патчей будет обновлен + Дождитесь обновления исходного кода патча. Ошибка загрузки параметров патча Нет доступных настроек для этого патча - Свой брендинг + Пользовательский брендинг Изменить отображаемое имя приложения Имя приложения Введите своё имя @@ -510,13 +531,7 @@ morphe_header_custom_dark.png Версия установленного Shizuku не поддерживается Открыть Shizuku Установка не удалась. Повторите попытку или переключитесь на другой установщик - Установка была заблокирована Android. Проверьте Play Protect или настройки безопасности Другое приложение с таким же именем пакета уже установлено. Удалите его перед продолжением - APK несовместим с этим устройством или версией Android - APK недействителен или поврежден - Недостаточно места на устройстве для установки - Время установки истекло. Повторите попытку - Установка была отменена Открытие %1$s… %1$s не подтвердил установку. Проверьте другое приложение и повторите попытку %1$s сообщил об успешной установке @@ -547,6 +562,7 @@ morphe_header_custom_dark.png Имя пользователя (псевдоним) Пароль Импортировать + Формат хранилища ключей Неверные учетные данные хранилища Хранилище ключей импортировано Не удалось импортировать хранилище ключей @@ -554,6 +570,7 @@ morphe_header_custom_dark.png Экспортировать текущее хранилище ключей Нет доступного хранилища ключей для экспорта Хранилище ключей экспортировано + Не удалось экспортировать хранилище ключей Показать пароль Скрыть пароль @@ -682,9 +699,10 @@ morphe_header_custom_dark.png Разрешить Детали Скрыть - По умолчанию + Скрыто + Показать Журналы - Источника + Источники Установка… Монтаж… Размонтирование… diff --git a/app/src/main/res/values-si-rLK/strings.xml b/app/src/main/res/values-si-rLK/strings.xml index 180ea1689..48f97ef60 100644 --- a/app/src/main/res/values-si-rLK/strings.xml +++ b/app/src/main/res/values-si-rLK/strings.xml @@ -39,10 +39,12 @@ + + diff --git a/app/src/main/res/values-sk-rSK/plurals.xml b/app/src/main/res/values-sk-rSK/plurals.xml index 3ea04e700..a0a2b6c5f 100644 --- a/app/src/main/res/values-sk-rSK/plurals.xml +++ b/app/src/main/res/values-sk-rSK/plurals.xml @@ -1,2 +1,69 @@ - + + + %s záplata + %s záplaty + %s záplat + %s záplat + + + %s nastavenie záplaty + %s nastavenia záplaty + %s nastavení záplaty + %s nastavení záplaty + + + %s zdroj + %s zdroje + %s zdrojov + %s zdrojov + + + %s balík + %s balíky + %s balíkov + %s balíkov + + + Vykonaná záplata %s + Vykonané záplaty %s + Vykonané záplaty %s + Vykonané záplaty %s + + + %s APK súbor + %s APK súbory + %s APK súborov + %s APK súborov + + + Odstránená %s zastaralá záplata z uloženého výberu + Odstránené %s zastaralé záplaty z uloženého výberu + Odstránených %s zastaralých záplat z uloženého výberu + Odstránených %s zastaralých záplat z uloženého výberu + + + %s zdroj nainštalovaný + %s zdroje nainštalované + %s zdrojov nainštalovaných + %s zdrojov nainštalovaných + + + %s zvolená záplata + %s zvolené záplaty + %s zvolených záplat + %s zvolených záplat + + + Zobraziť %s aplikáciu + Zobraziť %s aplikácie + Zobraziť %s aplikácie + Zobraziť %s aplikácie + + + %s skrytá aplikácia + %s skryté aplikácie + %s skryté aplikácie + %s skryté aplikácie + + diff --git a/app/src/main/res/values-sk-rSK/strings.xml b/app/src/main/res/values-sk-rSK/strings.xml index 960d4eeff..66160d23e 100644 --- a/app/src/main/res/values-sk-rSK/strings.xml +++ b/app/src/main/res/values-sk-rSK/strings.xml @@ -10,6 +10,7 @@ Zobraziť skryté aplikácie Žiadne skryté aplikácie Skryté aplikácie + Ťuknite na skrytie Chyba v Android 11 Je potrebné vopred povoliť inštaláciu aplikácie, aby sa predišlo chybe v systéme Android 11, ktorá negatívne ovplyvní používateľskú skúsenosť Pridať zdroj @@ -27,6 +28,12 @@ Aktualizácia zdrojov dokončená Vaše zdroje sú aktuálne Zdroje sa načítavajú, prosím počkajte… + Vyhľadávanie aplikácií + Nedostupné žiadne aplikácie + Pridajte zdroj záplaty alebo povoľte existujúci v %s + Žiadne aplikácie sa nezhodujú s \"%1$s\" + Všetky aplikácie sú skryté + Skryli ste všetky aplikácie z domovskej obrazovky Ktorú aplikáciu chcete zaplátať? @@ -53,6 +60,8 @@ Nepodarilo sa otvoriť URL Použiť predbežne vydané záplaty Získajte skorší prístup k novým experimentálnym verziám záplat + Použiť experimentálne verzie aplikácií + Záplatovať experimentálne ciele aplikácií, ak sú dostupné Tento zdroj už bol pridaný Zobrazovaný názov @@ -74,17 +83,24 @@ Aktualizácia bola úspešná Neboli nájdené žiadne aktualizácie Nepodarilo sa stiahnuť zdroje: %s + Nepodarilo sa stiahnuť \'%1$s\': %2$s Nebolo možné aktualizovať \'%1$s\': súbor záplaty JSON nebol nájdený na zadanej URL + Expert + Povoľte režim experta v Nastaveniach, aby ste mohli manuálne zahrnúť túto záplatu Potrebujete pomôcť s nájdením pôvodného nezaplátaného APK súboru? Na zaplátanie <b>%s</b> potrebujete nezaplátaný APK súbor verzia: Na zaplátanie <b>%s</b> potrebujete nezaplátaný APK súbor verzia: Odporúčané Nezaplátané + Nekompatibilné Áno, pomôžte mi nájsť APK súbor Nie, už mám APK súbor Použiť uložený APK súbor (v%s) Nie je uložený žiadny súbor APK. Na zaplátanie bude potrebné znova vybrať APK súbor + Vyžaduje systém Android %1$s+ + Nepodporované na tomto zariadení + <b>%1$s</b> nemá podporované verzie pre Android %2$d (API %3$d). Všetky deklarované verzie vyžadujú vyššiu verziu Androidu Stiahnutie originálneho APK súboru Pokyny: @@ -104,19 +120,38 @@ Vybraná verzia: Vybrať APK súbor Ťuknite na tlačidlo nižšie, aby ste vybrali APK súbor aplikácie, ktorú chcete zaplátať + Nepodarilo sa prečítať súbor. Skontrolujte, či \'Externé úložisko\' nie je obmedzené v spotrebe batérie + Vybraný súbor nie je platný APK + Nepodarilo sa otvoriť súbor. Skúste to znova Režim pre skúsených Vyhľadať záplaty Prejsť na plátanie Zakázať všetko Povoliť všetko + Povoľte odporúčané záplaty + Obnoviť uloženú voľbu Neboli nájdené žiadne záplaty Univerzálne záplaty + Nový Sú vybraté viaceré zdroje záplat "Vyberali ste záplaty z viacerých zdrojov. Môže to spôsobiť problémy s kompatibilitou alebo neočakávané správanie. Naozaj chcete pokračovať?" + Neoverený APK + Vybraný APK pre <b>%s</b> sa nezhoduje s očakávaným certifikátom podpisu. Mohol byť upravený alebo pochádza z nedôveryhodného zdroja + Tento APK nemusí byť originál + Zistený rozdelený APK + "Tento súbor je APK balík +(<b>APKM / APKS / XAPK</b>). + +Pre najlepšie výsledky sa odporúča záplatovať <b>úplný APK</b>" + Vyberte iný APK + Experimentálna + Táto verzia má experimentálnu podporu a môže byť nestabilná alebo neúplná + Chcete experimentovať? 🧪 + Táto verzia <b>%s</b> má skorú experimentálnu podporu<br/><br/>🔧 Očakávajte zvláštne správanie aplikácie alebo neznalé chyby, keď sa záplaty pre túto verziu aplikácie vylepšujú Nepodporovaná verzia Tento APK súbor nie je odporúčanou verziou aplikácie. Ak budete pokračovať môže to viesť k nefunkčnej aplikácii Pokračovať napriek tomu @@ -130,6 +165,9 @@ Naozaj chcete pokračovať?" Mobilné dáta sú aktívne a aktualizácie záplat sú zakázané Používanie zastaraných záplat môže spôsobiť nefunkčnosť aplikácie Aktualizovať & záplatu + Nedostatok miesta na disku + Dostupných je iba %1$.2f GB voľného úložiska. Záplatovanie vyžaduje na správnu funkciu aspoň %2$.2f GB voľného miesta + Pokračovanie môže viesť k chybe \"súbor sa nenašiel\" alebo poškodenému výstupnému APK Vlastná farba Hex hodnota farby @@ -141,15 +179,23 @@ Naozaj chcete pokračovať?" Táto hodnota už existuje Hodnota nesmie byť prázdna Zatiaľ neboli pridané žiadne hodnoty + %s: vyplňte všetky požadované možnosti Vytvoriť adaptívnu ikonu + Vyberte obrázok popredia, zvoľte farbu pozadia a potom pomocou posúvača alebo gestami štipnutím upravte náhľad + Náhľad adaptívnej ikony + Pre najlepší výsledok majte svoju ikonu vo vnútri pevného vnútorného kruhu Vybrať obrázok Zmeniť obrázok Farba pozadia Obnovenie pozície a mierky Bezpečná zóna (vždy viditeľná) + Zóna masky (môže byť orezaná) Adaptívna ikona bola úspešne vytvorená Nepodarilo sa vytvoriť adaptívnu ikonu + Vyberte, kde vytvoriť priečinok \'%1$s/%2$s\' so súbormi vytvorených ikon + Náhľad ikony upozornenia + Použite posúvač na zmenu veľkosti. Potiahnite pre premiestnenie Vytvoriť vlastnú hlavičku Vyberte obrázky pre svetlý aj tmavý motív a potom ich veľkosť prispôsobte gestami približovania @@ -181,18 +227,22 @@ Naozaj chcete pokračovať?" Je dostupná novšia verzia záplat. Znovu zaplátajte aplikácie aby ste získali najnovšie vylepšenia a opravy. Aplikácia bola odinštalovaná Táto aplikácia bola odinštalovaná mimo Morphe. Znovu ju zaplátajte aby ste obnovili jej funkčnosť + Veľkosť APK Režim root vyžaduje inštaláciu pôvodného APK pred aplikovaním záplat Podporná záplata GmsCore je vylúčená v root režime Vyberte spôsob inštalácie Vaše zariadenie má root prístup. Vyberte, ako chcete nainštalovať upravenú aplikáciu + Pripojenie koreňového systému Nainštaluje zaplátaný APK súbor skrz pôvodnú aplikáciu. GmsCore nie je potrebný Štandardná inštalácia + Inštalácia ako samostatná aplikácia s podporou GmsCore. Zvoľte inštalátor (Systémový inštalátor, Shizuku atď.) po záplatovaní Metóda inštalácie určuje, ktoré záplaty sú zahrnuté a po aplikovaní ich nemožno zmeniť Chyba bola skopírovaná do schránky Záznam o chybách Technické detaily + Informácie o aplikácii Tento krok môže chvíľu trvať. Čakajte prosím… Ešte chvíľu a môžeme to naplno roztočiť! 🎸 Všetko je hotové! 🎉 @@ -265,16 +315,13 @@ Naozaj chcete pokračovať?" Nepodarilo sa aplikovať %s Zaplátaná aplikácia uložená na neskôr Nepodarilo sa uložiť zaplátanú aplikáciu - Stiahnuť súbor APK Vyžaduje sa interakcia používateľa, aby bolo možné pokračovať s týmto pluginom Proces plátania skončil s kódom %1$s Úspešne nainštalované Nepodarilo sa nainštalovať aplikáciu: %s - Inštalácia nebola dokončená. Skontrolujte dialógové okno systémového inštalátora a skúste to znova APK súbor uložený Nepodarilo sa exportovať zaplátanú aplikáciu Uložená zaplátaná aplikácia bola odstránená - Kópia aplikácie odstránená Nainštalujte aplikáciu pred jej otvorením Názov balíka Nepodarilo sa pripojiť: %s @@ -291,13 +338,22 @@ Naozaj chcete pokračovať?" Vesmír Tvary Sneh + Mriežka Častice Žiadna + Náhodné + Pri spustení + Dennev + Každé 3 dni + Paralaxový efekt + Plynulé posúvanie pozadia pri nakláňaní zariadenia Motív Systém Svetlý Tmavý + Materiál You + Farba akcentu Čistá čierna Použiť úplne čierne pozadie @@ -305,6 +361,10 @@ Naozaj chcete pokračovať?" Aktuálny jazyk Preklady pre niektoré jazyky sa môžu byť neúplné Pre preklad nových jazykov alebo zlepšenie existujúcich prekladov navštívte %s + + Domovská obrazovka + Pozdravné frázy + Zobrazovať pozdravnú správu na domovskej obrazovke Ikona aplikácie Predvolená @@ -351,8 +411,17 @@ Naozaj chcete pokračovať?" Zapnúť rozšírené nastavenia Morphe pre plátanie a personalizáciu Povoliť režim pre skúsených? Režim pre skúsených poskytuje rozsiahlejšiu kontrolu nad tým, ako sa aplikujú záplaty, no nesprávna konfigurácia nastavení v tomto režime môže viesť k nefunkčnej aplikácii - Odstrániť nepoužívané natívne knižnice - Odstráňte natívne knižnice pre nepodporované CPU architektúry zo zaplátaných aplikácií + Optimalizovať pre architektúru zariadenia + "Preskočiť moduly rozdeleného APK pre nepodporované architektúry CPU, jazykové prostredia a hustoty obrazovky počas zlúčenia. +Pre jednoduché APK odstráni natívne knižnice pre nepodporované architektúry po záplatovaní" + + Režim spracovania bytecode + Ovláda, ako sa bytecode spracováva počas záplatovania. Ovplyvňuje rýchlosť záplatovania, využitie pamäte a veľkosť výstupného APK + Rýchle (Odporúčané) + Rýchle + Rýchlejšie záplatovanie a nižšia spotreba pamäte na úkor väčšieho APK. Odporúčané pre zariadenia s nízkou RAM alebo staršie zariadenia + Plné + Pomalšie záplatovanie, replikuje staršie správanie. Používajte iba vtedy, ak rýchly režim spôsobuje problémy GitHub PAT Vyžadované pre pull request zdroje @@ -365,6 +434,7 @@ Naozaj chcete pokračovať?" Nezdieľajte vaše PAT. Ak ho zahrniete do exportu nastavení, uchovajte tento súbor v súkromí, pretože obsahuje token Nastavenia záplaty + Možnosti vzhľadu, značky a hlavičky Ak chcete aplikovať zmeny možností záplat, musíte aplikáciu znova zaplátať V režime pre skúsených budú aj počas plátania dostupné možnosti záplat @@ -372,15 +442,49 @@ Naozaj chcete pokračovať?" Zmeniť farbu pozadia Farba pozadia tmavého motívu Farba pozadia svetlého motívu + Môže to byť hexadecimálna farba (#RRGGBB) alebo odkaz na farebný zdroj Načítavam možnosti záplat… Počkajte, kým sa zdroj záplat aktualizuje Nepodarilo sa načítať možnosti záplaty Pre túto záplatu nie sú dostupné žiadne možnosti + Vlastná značka Zmeniť zobrazovaný názov aplikácie Názov aplikácie Zadajte vlastný názov Vlastná ikona + "Priečinok s obrázkami, ktoré sa majú použiť ako vlastná ikona. + +Priečinok musí obsahovať jeden alebo viac nasledujúcich priečinkov, v závislosti od DPI zariadenia: +- mipmap-mdpi +- mipmap-hdpi +- mipmap-xhdpi +- mipmap-xxhdpi +- mipmap-xxxhdpi + +Každý z priečinkov musí obsahovať všetky nasledujúce súbory: +morphe_adaptive_background_custom.png +morphe_adaptive_foreground_custom.png + +Rozmery obrázkov musia byť nasledovné: +- mipmap-mdpi: 108x108 px +- mipmap-hdpi: 162x162 px +- mipmap-xhdpi: 216x216 px +- mipmap-xxhdpi: 324x324 px +- mipmap-xxxhdpi: 432x432 px + +Voliteľne priečinok obsahuje priečinok 'drawable' so súborom monochromatickej ikony: +morphe_adaptive_monochrome_custom.xml + +Voliteľne priečinok obsahuje ikonu upozornenia v jednom z nasledujúcich formátov: +- XML vektorový obrázok v 'drawable': + morphe_notification_icon_custom.xml +- PNG rastrové obrázky v priečinkoch 'drawable-dpi': + drawable-mdpi/morphe_notification_icon_custom.png (24x24 px) + drawable-hdpi/morphe_notification_icon_custom.png (36x36 px) + drawable-xhdpi/morphe_notification_icon_custom.png (48x48 px) + drawable-xxhdpi/morphe_notification_icon_custom.png (72x72 px) + drawable-xxxhdpi/morphe_notification_icon_custom.png (96x96 px)" Vlastné logo hlavičky Zmeniť logo hlavičky @@ -412,23 +516,22 @@ Rozmery obrázka musia byť nasledovné: Vybrať inštalátor Systémový inštalátor Použite nástroj na inštaláciu balíkov Android + Inštalátor rootovaného mountu + Mount patched APK nad nainštalovanou aplikáciou Nainštalovať na pozadí pomocou Shizuku alebo Sui + Žiadna záloha Žiadny záložný inštalátor nie je nakonfigurovaný Vyžaduje prístup s root oprávneniami Nie je podporované pre túto akciu Nainštalujte Shizuku alebo Sui aby ste mohli využiť túto možnosť Otvorte Shizuku alebo Sui a skúste to znova Udeľte Morphe povolenie priamo z aplikácie Shizuku + Váš primárny inštalátor je nastavený na inštalátor rootovaného mountu. Táto uložená aplikácia nebola inštalovaná pomocou mountu + Verzia stock aplikácie (%2$s) sa nezhoduje s verziou patched aplikácie (%1$s). Použite mount pre zodpovedajúcu verziu stock aplikácie a skúste to znova Nainštalovaná verzia Shizuku nie je podporovaná Otvoriť Shizuku Inštalácia zlyhala. Skúste to znova alebo s iným inštalátorom - Inštalácia bola zablokovaná Androidom. Skontrolujte Play Protect alebo bezpečnostné nastavenia Už je nainštalovaná iná aplikácia s týmto názvom balíka. Pred pokračovaním ju odinštalujte - APK súbor nie je kompatibilný s týmto zariadením alebo verziou systému Android - Neplatný alebo poškodený APK súbor - Nie je dostatok voľného miesta na disku pre túto inštaláciu - Časový limit inštalácie vypršal. Skúste to znova - Inštalácia bola zrušená Otvára sa %1$s... %1$s nepotvrdil inštaláciu. Skontrolujte danú aplikáciu a skúste to znova %1$s potvrdil úspešnú inštaláciu @@ -452,9 +555,22 @@ Rozmery obrázka musia byť nasledovné: Táto funkcia vyžaduje Android 11 alebo vyšší Import a export + Importovať keystore + Import vlastného keystoru + Zadajte údaje pre prístup ku keystoru + Na importovanie budete musieť zadať údaje pre prístup ku keystoru Používateľské meno (alias) Heslo Importovať + Formát keystoru + Nesprávne údaje pre prístup ku keystoru + Keystore bol importovaný + Nepodarilo sa importovať keystore + Exportovať keystore + Exportovať aktuálny keystore + Žiadny dostupný keystore na export + Keystore bol exportovaný + Nepodarilo sa exportovať keystore Zobraziť heslo Skryť heslo @@ -497,6 +613,8 @@ Rozmery obrázka musia byť nasledovné: Obnoviť výbery balíkov? Obnoviť výbery zdrojov? Žiadne uložené výbery záplat + %1$s z %2$s + %1$s v %2$s Zdroj #%s Týmto odstránite: Týmto trvalo vymažete všetky uložené výbery záplat pre všetky balíky a zdroje @@ -514,15 +632,19 @@ Rozmery obrázka musia byť nasledovné: O aplikácii Zdieľať webstránku Zdieľať oficiálnu webstránku Morphe + "Projekt otvoreného zdroja pre moderné a zjednodušené záplatovanie populárnych aplikácií Android, poháňaný spätnou väzbou a príspevkom komunity" Poďakovanie Aktuálny vývoj Predchádzajúci vývoj + Licencie otvoreného zdroja Záplaty Domov Nastavenia Áno Nie + Všetko + Filter Obnoviť Obnoviť všetko Potvrdiť @@ -563,6 +685,7 @@ Rozmery obrázka musia byť nasledovné: Otvoriť Pripojiť Pripojené + Odpojiť Odpojené Znovu pripojiť Inštalovať @@ -576,12 +699,15 @@ Rozmery obrázka musia byť nasledovné: Povoliť Podrobnosti Skryť - Predvolené + Skrytý + Zobraziť Záznamy + Zdroje Inštaluje sa… Pripájanie... Odpájanie... Importovanie… + Úspešne importované Zobraziť zoznam zmien Pozrite si najnovšie zmeny v tejto aktualizácii Nepodarilo sa stiahnuť zoznam zmien: %s diff --git a/app/src/main/res/values-sl-rSI/plurals.xml b/app/src/main/res/values-sl-rSI/plurals.xml index 3ea04e700..8ecbcfa02 100644 --- a/app/src/main/res/values-sl-rSI/plurals.xml +++ b/app/src/main/res/values-sl-rSI/plurals.xml @@ -1,2 +1,69 @@ - + + + %s popravilo + %s popravka + %s popravki + %s popravki + + + %s možnost + %s možnosti popravka + %s možnosti popravkov + %s možnosti popravka + + + %s vir + %s vira + %s viri + %s virov + + + %s paket + %s paketa + %s paketi + %s paketi + + + Izveden %s popravek + Izvedena %s popravka + Izvedena %s popravka + Izvedeni %s popravki + + + %s datoteka APK + %s datoteki APK + %s datoteke APK + %s datoteke APK + + + Odstranjena %s zastarela posodobitev iz shranjenega izbora + Odstranjena %s zastarela popravka iz shranjene izbire + Odstranjenih %s zastarelih popravkov iz shranjene izbire + Odstranjenih %s zastarelih popravkov iz shranjene izbire + + + %s vir nameščen + %s vira sta nameščena + %s viri so nameščeni + %s viri so nameščeni + + + %s popravek je izbran + %s popravka sta izbrana + %s popravki so izbrani + %s popravki so izbrani + + + Prikaži %s aplikacijo + Pokaži %s aplikaciji + Pokaži %s aplikacije + Pokaži %s aplikacije + + + %s skrita aplikacija + %s skriti aplikaciji + %s skrite aplikacije + %s skrite aplikacije + + diff --git a/app/src/main/res/values-sl-rSI/strings.xml b/app/src/main/res/values-sl-rSI/strings.xml index 180ea1689..59ce9a15c 100644 --- a/app/src/main/res/values-sl-rSI/strings.xml +++ b/app/src/main/res/values-sl-rSI/strings.xml @@ -1,63 +1,734 @@ + Ostale aplikacije + Še ni patchano + Ni nobenih popravkov + Ali bi skrili to aplikacijo? + Ta aplikacija bo skrita iz začetnega menija. Spet jo lahko pokažete s klikom na \'%1$s\' gumb, ki se nahaja na spodnjem predelu strani. + Ta aplikacija bo skrita + Pokaži skrite aplikacije + Ni skritih aplikacij + Skrite aplikacije + Klikni da pokažeš + Android 11 napaka + Aplikaciji je treba dovoliti dovoljenje za namestitev, da se izognete napaki v sistemu Android 11, ki bo negativno vplivala na uporabniško izkušnjo. + Dodaj vir + Ali bi dodal/a ta vir popravkov v Morphe? + Dodaj vir samo iz virov, ki jim zaupaš + Morphe posodobitev je pripravljena + Nova verzija je pripravljena za prenos + Posodobitev ni uspela + Preveri internetno povezavo in poskusi znova + Preskočena posodobitev + Mobilni podatki so vklopljeni + Posodabljanje virov... + %1$s obdelano + Posodabljanje virov končano + Viri so posodobljeni + Viri se naložujejo, počakaj… + Poišči aplikacije + Ni na voljo aplikacij + Dodaj vir popravkov ali omogoči že obstoječega v %s + Nobena aplikacija ne ustreza \"%1$s\" + Vse aplikacije so skrite + Skrile ste vse aplikacije z začetnega zaslona + Katero aplikacijo zelis popraviti? + Si pripravljen ponovno narediti aplikacije legendarnimi? + Privzasi se, popravljanje je v polnem teku + Se en dan, se eno mojstrstvo brez oglasov + Želiš gledati oglase? Jaz tudi ne + Poskrbimo, da se vaše aplikacije obnašajo spodobno + Danes napoved: 100-odstotna verjetnost popravka + Si pripravljen spremeniti nezadostno v fantastično? + Rešujemo aplikacije same sebe, od danes naprej + Deluje na kofein in dobrih popravkih + Posodobi vire + Odprtje v brskalniku + Ni mogoče odpreti URL + Uporabi pred-izidne popravke + Prejmi zgodnji dostop do novih eksperimentalnih različic popravkov + Uporaba eksperimentalnih različic aplikacij + Popravljanje eksperimentalnih aplikacijskih ciljev, če so na voljo + Ta vir že obstaja + Prikazno ime + Preimenuj vir popravka + Vir s tem imenom že obstaja + Ni mogoče posodobiti tega vira popravkov. + Katera koli verzija + Katero koli paket + Dodaj vir popravka + Izbrisati vir \"%s\"? To se ne more razveljaviti. + Oddaljen + URL vira + Primeri: + Lokalni + Izberi datoteko za popravek + Izberi datoteko .mpp za vir popravka iz shrambe + Spremeni datoteko + Prednameščen + Posodobitev uspešna + Posodobitev ni na voljo + Napaka pri prenosu virov: %s + Ni bilo mogoče prenesti \'%1$s\': %2$s + Ni mogoče posodobiti \'%1$s\': datoteka JSON popravka ni bila najdena na konfigurirani naslov URL + Izkušen + Omogoči način strokovnjaka v Nastavitvah, da vključite ta popravek ročno + Potrebujete pomoč pri iskanju izvirnega, nepopravljenega APK? + Za popravilo <b>%s</b> potrebujete nepopravljeno različico APK: + Za popravilo <b>%s</b> potrebujete nepopravljene APK različice: + Priporočeno + Nepopravljeno + Nezdružljivo + Da, pomagajte mi najti APK + Ne, že imam APK + Uporabite shranjeno APK (v%s) + Ni shranjene APK. Popravilo bo zahtevalo ponovno izbiro datoteke APK. + Zahteva Android %1$s+ + Ni podprto na tej napravi + <b>%1$s</b> nima podprtih različic za Android %2$d (API %3$d). Vse deklarirane različice zahtevajo višjo različico Androida + Prenesi izvirni APK + Navodila: + Pritisni gumb \'%1$s\' spodaj + Pomikaj navzdol na spletni strani in pritisni + Pritisni na gumb za prenos na spletni strani, ne tukaj 😊 + Počakaj, da se prenos konča, in <b>ne namešči</b> APK + Počakaj, da se prenos konča, nato <b>namešči APK</b> + Po končanem prenosu se vrni v Morphe + Po namestitvi izvirnega APK se vrni v Morphe + Naprej na APKMirror.com + Izberi prenesen APK + Izberi <b>%1$s</b> APK datoteko, ki si je pravkar prenesel + Odpri datoteko APK + Priporocena verzija + Izbrana različica + Izberi APK + Dotakni se gumba spodaj, da izberes APK datoteko katere koli aplikacije za popravilo + Datoteke ni bilo mogoče prebrati. Preverite, ali \'Zunanji pomnilnik\' ni omejen zaradi porabe baterije + Izbrana datoteka ni veljavna datoteka APK + Ni bilo mogoče odpreti datoteke. Poskusite znova + Strokovni način + Išči popravke + Pojdi na popravilo + Onemogoči vse + Omogoči vse + Omogoči priporočene popravke + Obnovi izbrano izbiro + Ni najdenih popravkov + Univerzalne popravki + Novo + Izbrani so večji viri popravkov + "Izbrali ste popravke iz več virov popravkov. To lahko povzroči težave s kompatibilnostjo ali nepričakovano obnašanje. + +Želite nadaljevati?" + Nepotrdiljena APK + APK, ki ste ga izbrali za <b>%s</b>, ne ustreza predvidenemu potrdilu za podpis. Morda je bila spremenjena ali pa izvira iz nepridobljivega vira + Ta APK morda ni original + Zaznana je split APK + "Ta datoteka je paket APK +(<b>APKM / APKS / XAPK</b>). + +Za najboljše rezultate ta aplikacija priporoča popravljanje <b>polne APK</b>" + Izberite drugo APK + Eksperimentalno + Ta različica ima eksperimentalno podporo in je morda nestabilna ali nepopolna + Želite eksperimentirati? 🧪 + Ta različica <b>%s</b> ima zgodnjo eksperimentalno podporo<br/><br/>🔧 Pričakujte nenavadno obnašanje aplikacije ali neprepoznane napake, saj se popravki prilagajajo za to različico aplikacije + Ne podprta različica + Ta APK ni priporočena različica aplikacije. Nadaljevanje lahko povzroči, da aplikacija ne bo delovala. + Naprej kljub temu + Združljive različice: + Ni podprto + Izbrana napačna datoteka + Izbrana datoteka APK ni za aplikacijo, ki jo želite popraviti. + Pričakovan paket: + Izbrani paket: + Popravki so lahko zastareli + Mobilni podatki so aktivni in posodobitve popravkov so onemogočene + Popravljanje s zastarelimi popravki lahko povzroči okvarjeno aplikacijo + Posodobi & popravi + Nizko prosto mesto na disku + Na voljo je samo %1$.2f GB prostega prostora. Za delovanje popravkov je potrebnih najmanj %2$.2f GB prostega prostora + Napredek lahko povzroči napako \"datoteka ni najdena\" ali pa okvarjeno izhodno APK + Prilagojena barva + Šestnajstična barva + Navodila + Vnesi vrednost + Vnesi številko + Vnesi decimalno mesto + Izberi mapo + Ta vrednost že obstaja + Vrednost ne more biti prazna + Še nobene vrednosti ni dodano + %s: izpolni vsa zahtevana polja + Ustvari ikono prilagojeno + Izberite sliko ozadja, izberite barvo ozadja, nato uporabite drsnik ali povlecite s prstom za prilagoditev predogleda + Predogled ikone za prilagajanje + Za najboljši rezultat ohranite ikono v trdni notranji krog + Izberite sliko + Spremenite sliko + Barva ozadja + Ponastavi položaj in merilo + Varno območje (vedno vidno) + Maskirno območje (lahko se obreže) + Ikona prilagojena uspešno + Pri ustvarjanju prilagojene ikone je prišlo do napake + Izberi, kje ustvariti mapo \'%1$s/%2$s\' z ustvarjenimi datotekami ikone + Predogled ikone za obvestila + Uporabite drsnik za spreminjanje velikosti. Povlecite za prestavljanje + Ustvari prilagojeno glavo + Izberite slike za svetle in temne teme, nato pa jih prilagodite z gestami za zožanje in razširitev + Glava svetle teme + Glava temne teme + Ni izbrana nobena slika + Glava uspešno ustvarjena + Pri ustvarjanju glave je prišlo do napake + Izberi, kje ustvariti mapo \'%1$s/%2$s\' z ustvarjenimi datotekami glave + Ali ste prepričani, da želite odstraniti to aplikacijo? + To bo trajno izbrisalo: + Vnos baze podatkov + Popravljena datoteka APK + Izvirna datoteka APK + Vaše izbire in možnosti popravka bodo ohranjene za prihodnje popravke. + Popravki + Neimenovano + Napaka pri uvozu popravkov: %s + Izvirna datoteka APK ni najdena. Te aplikacije ni mogoče več popraviti. + Izvirno ime paketa + Uporabljeni popravki + Uporabljen vir popravka + Vrsta namestitve + Prilagojena namestitev + Sistemska namestitev + Popravljeno + Na voljo posodobitev popravkov + Na voljo je novejša različica popravkov. Popravite svojo aplikacijo, da pridobite najnovejše izboljšave in popravke. + Aplikacija je bila odstranjena + Ta aplikacija je bila odstranjena zunaj Morphe. Ponovno jo popravi, da obnoviš funkcionalnost + Velikost APK + Za način root je potrebno, da je izvirna datoteka APK nameščena na vaši napravi, preden jo popravite. + Popravek podpore GmsCore je izključen v načinu root + Izbira metode namestitve + Vaša naprava ima dostop do root. Izberite, kako želite namestiti posodobljeno aplikacijo + Root Mount + Namestite posodobljeno APK nad izvirno aplikacijo. GmsCore ni potreben + Standardna namestitev + Namestite kot ločeno aplikacijo s podporo GmsCore. Izberite namestitveni program (sistemski namestitveni program, Shizuku itd.) po namestitvi + Metoda namestitve določa, katere popravki so vključeni in je po namestitvi ni mogoče več spreminjati + Napaka je kopirana v odložišče + Dnevnik napak + Tehnične podrobnosti + Podatki o aplikaciji + Ta korak lahko traja nekaj časa. Prosim, počakaj… + Kmalu bo vse v redu! 🎸 + Končano! 🎉 + Vaša aplikacija je popravljena in nameščena! + Popravek končan! ✨ + Očitno! Nekaj se je zgodilo narobe + Preveri podrobnosti napake spodaj + Popravek je spodletel + Prišlo je do neznane napake + Konflikt paketov + Morate odstraniti obstoječo različico + Napaka pri namestitvi + Poskusite ponovno odstraniti in namestiti + Pritisnite gumb spodaj za namestitev + Pritisnite gumb spodaj za montažo + Ne morem dostopati do poti shrambe + Ena ali več možnosti popravka se sklicujejo na poti, do katerih Morphe ne more brati. Daj dovoljenje za shrambo in nato pritisni gumb, nato se bo popravljanje začelo samodejno + Ena ali več možnosti popravka se sklicujejo na poti, do katerih ni mogoče dostopiti. Posodobi poti v možnostih popravka in poskusite znova + Dovoli dostop do shrambe + Dostop do shrambe je bil zavrnjen. Daj dovoljenje, da lahko uporabite to pot, ali premakni datoteke v zasebno mapo aplikacije + Odpri nastavitve shrambe + Dovoljenje zavrnjeno + Ni bilo najdeno + Namig: Premakni datoteke v /Android/data/app.morphe.manager/files/ - dodatna dovoljenja niso potrebna + Pripravljanje + Pobira popravke + Združevanje razdeljenih APK + Preberi datoteko APK + Popravljanje + Shranjevanje + Shranjevanje popravljene datoteke APK + Podpisovanje popravljene datoteke APK + Popravki so v teku… + Dotaknite se, da se vrnete k popravilu + Ustavi popravilo + Ste prepričani, da želite ustaviti proces popravila? + Preobražam vašo aplikacijo v nekaj čudovitega… 🏆 + Učim stare aplikacije novim trikom… 🎩 + Dajam vaši aplikaciji digitalno osvežitev… 🔧 + Kuhanje digitalne magije… ☕ + Priporočljiva je kava… ☕ + Poglej ven, lepo je… 🌤️ + Prilagajam 1 in 0… 🤖 + Razrešujem digitalno spageto… 🍝 + Popravljanje napak v nepopravljeni aplikaciji 🪲 + Velika popravila vredijo počakanja… ⏳ + Poliranje pik do sijaja… 💎 + Prosim, ostanite na sedežu, dokler kapitan ne izklopi znaka \"Popravek je v teku\"… 💺✈️ + Kuhanje popravljene aplikacije, vroče in sveže… 🍳 + Poljubno pogajanje z bajtkodom… 🤝 + Vsi sistemi nominalni… pot trajektorije popravka je stabilna… 🚀 + Izpiranje dodatne moči, kot soka iz digitalne limone… 🍋 + Spodbujanje aplikacije, da sprejme svoj novi, boljši jaz… 🌱 + Spreminjanje aplikacije iz nepopravljene v popravljeno… 📦⚛️ + Vaša aplikacija ima vročino, edini recept pa je več popravkov… 🩺🔔 + Odstranjevanje nesmiselnosti. Prosim, počakajte… 🗑️ + Popravlja aplikacijo + Uporaba pomnilnika + Uporaba popravkov + Ni uspelo uporabiti %s + Popravljena aplikacija je shranjena za kasnejšo uporabo + Ni uspelo shraniti popravljene aplikacije + Za nadaljevanje s tem dodatkom je potrebno uporabniško vklopljenost. + Proces popravila se je končal z kodo %1$s + Uspelo je namestitev + Ni uspelo namestiti aplikacije: %s + APK shranjeno + Ni uspelo izvoziti popravljene aplikacije + Odstranjena je bila shranjena popravljena aplikacija + Pred odpiranjem namestite aplikacijo. + Ime paketa + Montiranje je potovalo: %s + Razmontiranje je potovalo: %s + Izgled +Nastavitve + Napredno + Sistem + Animacija ozadja + Krogi + Obroči + Mreža + Prostor + Oblike + Sneg + Mreža + Delci + Brez + Naključno + Ob zagonu + Dnevno + Vsakih 3 dni + Paralaksni učinek + Gladko premikanje ozadja med nagibanjem naprave. + Tema + Sistem + Svetlo + Temno + Material You + Barva akcenta + Črnina + Uporabi črne ozadja + Jezik aplikacije + Trenutni jezik + Prevodi za nekatera jezika so lahko manjkoči ali nepopolni + Za prevajanje novih jezikov ali izboljšanje obstoječih prevodov, obiščite %s + + Začetni zaslon + Pozdravljajoča sporočila + Prikaži pozdravljajoče sporočilo na začetnem zaslonu + Ikona aplikacije + Privzeta + Nebesa + Zaliv sonca + Ocean + Praznina + Indigov + Spremeniti ikono aplikacije? + Spremeniti na \'%1$s\' in znova zažeti? + Spremeni ikono + Posodobitve + Uporabi pred-izidno Morphe + Prejmi zgodnji dostop do novih funkcij Morphe. Za pridobitev pred-izidnih popravkov omogoči stikalo za pred-izid v vsakem viru popravkov ločeno + Dovoli popravkom in posodobitvam Morphe, da se prenesejo prek mobilnih podatkov + Dovoli Morphe in posodobitvam popravkov, da se prenesejo prek mobilnih podatkov + Obvestila o posodobitvah v ozadju + Občasno preverjajte prisotnost posodobitev v ozadju in vas obvestim, ko so na voljo + Prejemajte takojšnje push obvestila za nove izdaje, tudi ko je aplikacija zaprta + Preverjanje pogostosti + Preverjanje posodobitev + Na uro + Na dan + Na teden + Na mesec + Pogostost preverjanja v ozadju + Preverjanja v ozadju morda ne delujejo zanesljivo na nekaterih napravah. Proizvajalci, kot so Xiaomi, Huawei, Samsung in OnePlus, pogosto agresivno omejujejo procese v ozadju, da varčujejo z baterijo + Na voljo je posodobitev Morphe + Na voljo nove posodobitve + Različica %1$s je pripravljena za prenos + Dotakni se, da odpreš Morphe in posodobiš + Omogočiti obvestila + Morphe potrebuje dovoljenje za obvestila, da vas opozori, ko so posodobitve na voljo v ozadju + Želite biti obveščeni, ko so na voljo posodobitve? + Obvestila o posodobitvah + Obvestila o novih izdajah Morphe in popravkov + Popravljanje + Prikazano medtem ko Morphe popravlja aplikacijo v ozadju + Napredne nastavitve + Način strokovnjaka + Omogoči zapletene nastavitve popravkov Morphe in možnosti prilagajanja + Omogočiti način strokovnjaka? + Način strokovnjaka zagotavlja več nadzora nad tem, kako se uporabljajo popravki, vendar lahko napačna konfiguracija nastavitev v načinu strokovnjaka povzroči nehote delovanje aplikacije + Optimiziraj za arhitekturo naprave + "Preskoči modul APK, ki je razdeljen za neuporabne arhitekture CPU, lokalitete in gostote zaslona med združevanjem. Za navadne APK datoteke odstrani izvorne knjižnice za neuporabne arhitekture po popravljanju" + + Način obdelave bajtkoda + Nadzoruje, kako je bajtkod obdelan med popravljanjem. Vpliva na hitrost popravljanja, porabo pomnilnika in velikost izhodne APK datoteke + Hitro (Priporočeno) + Hitro + Hitrejše popravljanje in manjša poraba pomnilnika, na račun večje APK datoteke. Priporočeno za naprave z malo RAM ali starejše naprave + Celovit + Počasnejše popravljanje, uporablja se samo, če hitri način povzroča težave + GitHub PAT + Potrebno za vire povratnih zahtevkov + Token je konfiguriran + Kako pridobiti token + Nastavite GitHub Personal Access Token + Nastavi GitHub Personal Access Token, da lahko dodavaš povratne zahteve kot zunanje vire. Ustvari PAT s področjem public_repo github.com + Vključite v izvoz nastavitev + Dodaja to PAT v izvozne nastavitve Morphe + Ne delite svojega PAT. Če ga vključite v izvoz nastavitev, shranite to datoteko zasebno, saj vsebuje token + Možnosti popravkov + Tema, blagovna znamka in možnosti glave + Sprememba možnosti popravkov zahteva ponovno popravljanje aplikacije, da bi stopila veljavo + V načinu strokovnjaka bodo možnosti popravkov na voljo med procesom popravljanja + Barve teme + Spremenite barvo ozadja + Barva ozadja temne teme + Barva ozadja svetle teme + Lahko je barvna koda v hex (npr. #RRGGBB) ali referenca na barvno datoteko + Pehavanje možnosti popravkov… + Počakajte, da se vir popravkov posodobi + Ni uspešno naložiti možnosti popravka + Za to popravilo ni na voljo nobenih možnosti + Lastna blagovna znamka + Spremenite prikazno ime aplikacije + Ime aplikacije + Vnesite prilagojeno ime + Prilagojena ikona + "Mapa s slikami za uporabo kot prilagojene ikone. + +Mapa mora vsebovati eno ali več od naslednjih map, odvisno od ločljivosti naprave: +- mipmap-mdpi +- mipmap-hdpi +- mipmap-xhdpi +- mipmap-xxhdpi +- mipmap-xxxhdpi + +V vsaki od map mora biti vsebovana vsa naslednja datoteka: +morphe_adaptive_background_custom.png +morphe_adaptive_foreground_custom.png + +Dimenzije slik morajo biti naslednje: +- mipmap-mdpi: 108x108 px +- mipmap-hdpi: 162x162 px +- mipmap-xhdpi: 216x216 px +- mipmap-xxhdpi: 324x324 px +- mipmap-xxxhdpi: 432x432 px + +Neobvezno, pot vsebuje mapo 'drawable' z datoteko ikone v barvitih odtenkih: +morphe_adaptive_monochrome_custom.xml + +Neobvezno, pot vsebuje ikono obvestila v enem od naslednjih formatov: +- XML vektorna datoteka v 'drawable': + morphe_notification_icon_custom.xml +- PNG izbirna slika v mapah 'drawable-dpi': + drawable-mdpi/morphe_notification_icon_custom.png (24x24 px) + drawable-hdpi/morphe_notification_icon_custom.png (36x36 px) + drawable-xhdpi/morphe_notification_icon_custom.png (48x48 px) + drawable-xxhdpi/morphe_notification_icon_custom.png (72x72 px) + drawable-xxxhdpi/morphe_notification_icon_custom.png (96x96 px)" + Prilagojeni logotip glave + Spremenite logotip glave + "Mapa s slikami, ki se bodo uporabljale kot prilagojeni logotip glave. + +Mapa mora vsebovati eno ali več naslednjih map, odvisno od DPI naprave: +- drawable-hdpi +- drawable-xhdpi +- drawable-xxhdpi +- drawable-xxxhdpi + +Vsaka od map mora vsebovati vse naslednje datoteke: +morphe_header_custom_light.png +morphe_header_custom_dark.png + +Dimenzije slike morajo biti naslednje: +- drawable-hdpi: 194x72 px +- drawable-xhdpi: 258x96 px +- drawable-xxhdpi: 387x144 px +- drawable-xxxhdpi: 512x192 px" + Skrij funkcije Shorts + Skrij bližnjico aplikacije Shorts + Odstrani Shorts iz menija dolgega pritiska na omare + Skrij widget Shorts + Odstrani gumb Shorts iz widgeta na omari + Nameščen + Izberi namestitvenik + Sistemski nameščevalec + Uporabite nameščevalnik paketov Android + Nameščevalnik z ročnim dostopom + Namesti popravljen APK nad nameščeno aplikacijo + Namestite tiho z uporabo Shizuku ali Sui. + Brez nadomestitve + Ni konfiguriranega nameščevalnika brez nadomestitve + Potrebujete dostop prek root + Ni podprto za to dejanje. + Za uporabo te možnosti namestite Shizuku ali Sui. + Zaženite Shizuku ali Sui in poskusite znova. + Morphe podelite dovoljenje Shizuku iz aplikacije Shizuku. + Vaš preferirani namestitvenik je nastavljen na nameščeno montažo. Ta shranjena aplikacija ni bila nameščena z montažo. + Originalna različica aplikacije (%2$s) se ne ujema s popravljeno različico aplikacije (%1$s). Namestite ustrezno originalno različico, nato poskusite znova + Namestitvena različica Shizuku ni podprta. + Odprite Shizuku + Namestitev je zatažila. Poskusite znova ali preklopite na drugega namestitvenika. + Aplikacija z istim imenom paketa je že nameščena. Preden nadaljujete, jo odstranite. + Odpiranje %1$s… + %1$s ni potrdil namestitve. Preverite drugo aplikacijo in poskusite znova. + %1$s je poročal o uspešni namestitvi + %s nedostopna + Vaš preferirani namestitvenik (%s) je trenutno nedostopen. Popravite težavo in poskusite znova ali uporabite standardni namestitvenik. + Prepričajte se, da Shizuku deluje in da je tej aplikaciji podarjeno dovoljenje. + Uporabite standardno + Obvestite namestitelja ob namestitvi + Pokažite dialog za izbiro polnilnika vsakič, ko namestite popravljeno aplikacijo. + Izvedba + Čas izvajanja procesa + Izvajanje popravila v ločenem procesu za boljšo stabilnost + Omejitev pomnilnika + Višje omejitve pomnilnika lahko pospešijo popravilo, vendar na starejših napravah morda ne bodo delovale + Omogoči izvajanje procesa + Razpoljljivi pomnilnik za popravilo + Nizka omejitev pomnilnika lahko povzroči napake pri popravilu večjih aplikacij + %s MB omejitev + Kliknite za konfiguracijo + Ta funkcija zahteva Android 11 ali več + Uvozi & izvozi + Uvozi ključavnico + Uvozi prilagojeno ključavnico + Vnesite podatke ključavnice + Za uvoz boste morali vnesti podatke ključne datoteke. + Uporabniško ime (Alias) + Geslo + Uvozi + Format datoteke PKCS#12 + Nepravilni podatki ključavnice + Ključavnica uvožena + Uvoz ključavnice je spodletel + Izvozi ključavnico + Izvozi trenutno ključavnico + Ni na voljo ključavnica za izvoz + Ključavnica izvožena + Izvoz shranjevalnika ključev je spodletel + Pokaži geslo + Skrij geslo + Uvozi nastavitve Morphe + Obnovi nastavitve Morphe iz datoteke JSON + Ni bilo mogoče uvoziti nastavitev Morphe: %s + Uvožene so bile nastavitve Morphe + Izvozi nastavitve Morphe + Shrani nastavitve Morphe v datoteko JSON + Ni bilo mogoče izvoziti nastavitev Morphe: %s + Izvožene so bile nastavitve Morphe + Napajanje + Izvozi zaporne datoteke + Shrani sistemske beleške za odpravljanje težav + Ni bilo mogoče brati beležk (koda izhoda %s) + Izvoz datotek je spodletel + Datoteke izvožene + Upravljanje shrambe + Skupna velikost: %s + Popravljeni APK-ji + Upravljajte shranjenimi popravljenimi datotekami aplikacij + Ni shranjenih popravljenih APK-jev + Izbrišite popravljeni APK? + Ali želite izbrisati shranjeno popravljeno APK za %s? + Uspešno izbrisan popravljen APK + Izvirni APK-ji + Izvirne APK datoteke so shranjene za ponovno popravljanje. Za vsako aplikacijo se shrani samo najnovejša različica + Ni shranjenih izvirnih APK-jev + Uspešno izbrisan izvirni APK + Izbrisati izvirni APK? + Izbrisati izvirno datoteko APK za %s? Ne boste mogli več popraviti brez zagotovitve nove datoteke APK. + Izbire popravil + Upravljajte shranjene izbire in možnosti popravka. Te obstajajo tudi po odstranitvi aplikacije. + Ponastaviti vse izbire? + Ponastaviti izbire paketov? + Ponastaviti izbiro vira? + Ni shranjenih izbir popravkov + %1$s od %2$s + %1$s v %2$s + Vir #%s + To bo izbrisalo: + To bo trajno izbrisalo vse shranjene izbire popravkov za vse pakete in vire + To bo trajno izbrisalo vse shranjene izbire popravkov za %s na vseh virih + To bo trajno izbrisalo shranjene izbire popravkov za %1$s v viru #%2$s + Ni bilo mogoče izvoziti podatkov vira + Podatki vira so bili uspešno izvoženi + Ni bilo mogoče uvoziti podatkov vira + Podatki vira so bili uspešno uvoženi + Podrobnosti o popravkih + Izbrani popravki (%s) + Možnosti popravkov (%s) + Ni shranjenih popravkov ali možnosti + O aplikaciji + Delite spletno mesto + Delite uradno spletno mesto Morphe + "Odprtokodni projekt za sodobno in prilagojeno popravljanje priljubljenih aplikacij za Android, ki ga gonijo povratne informacije in prispevki skupnosti" + Zasluge + Trenutna razvojanost + Prejšnja razvojanost + Odprtokodna dovoljenja + Popravki + Domov + Nastavitve + Da + Ne + Vse + Filtriraj + Ponastavi + Ponastavi vse + Potrdite + Opozorilo + Napaka + Poskusite znova + Dodaj + Zapri + Počisti + Izbriši + Odstrani + Onemogoči + Omogoči + Pobira… + Ni nameščeno + Poglej + Poišči + Ni rezultatov najdenih + Katero koli + Omogočeno + Onemogočeno + Izbrano + Ni izbrano + Preimenuj + Uvozi + Izvozi + Prenesi + Manj + Razširi + Razširjeno + Zlož + Zloženo + Na voljo + Naprej + Različica + Popravi + Ponovno popravi + Odprti + Montiraj + Montirano + Razmontiraj + Razmontirano + Ponovno vstavite + Namesti + Nameščeno + Ponovna namesti + Odstrani + Raznameščeno + Shrani + Shranjeno + Posodobi + Omogoči + Podrobnosti + Skrij + Skrit + Pokaži + Beležke + Viri + Nameščanje… + Montiranje… + Odnamevam… + Uvažam… + Uvoženo uspešno + Poglejte dnevnike sprememb + Oglejte si najnovejše spremembe v tej posodobitvi + Ni mogoče prenesti dnevnika sprememb: %s + Poskusite znova + Ni na voljo popravkov + Ni na voljo internetne povezave + Na voljo je posodobitev + Pripravljeni na namestitev posodobitve + Posodobitev je bila uspešno nameščena + Ni uspelo namestiti posodobitve + Namestitev posodobitve… + Prenos posodobitve… + Ni uspelo prenesti posodobitve: %s + Ni uspelo preveriti posodobitev: %s + Ni uspelo prenesti posodobitve: %s + Ponavadi prenos + "Trenutno ste povezani na varčno povezavo in vam lahko zaračunajo stroški ponudnika storitev. Želite nadaljevati?" + Prenesti posodobitev? + Zdaj + %s nazaj + %s nazaj + %s nazaj + Neveljaven datum diff --git a/app/src/main/res/values-sr-rCS/plurals.xml b/app/src/main/res/values-sr-rCS/plurals.xml index 5aec274d1..607636718 100644 --- a/app/src/main/res/values-sr-rCS/plurals.xml +++ b/app/src/main/res/values-sr-rCS/plurals.xml @@ -1,29 +1,24 @@ - - %s patch - %s patch - %s patches - %s opcija %s opcije - %s opcija + Opcije zakrpe za %s %s izvor %s izvora - %s izvora + %s zakrpe %s paket %s paketa - %s paketa + %s paketi - Izvrši %s patch - Izvrši %s patches - Izvrši %s patches + Izvršen %s patch + Izvršena %s patcha + Izvršeno %s patch-eva %s APK datoteka @@ -45,4 +40,14 @@ %s patches odabrano %s patches odabrano + + Pokaži aplikaciju %s + Prikaži %s aplikacije + Prikaži %s aplikacije + + + %s sakrivena aplikacija + %s sakrivene aplikacije + %s sakrivene aplikacije + diff --git a/app/src/main/res/values-sr-rCS/strings.xml b/app/src/main/res/values-sr-rCS/strings.xml index 09af75d9c..17f1f1852 100644 --- a/app/src/main/res/values-sr-rCS/strings.xml +++ b/app/src/main/res/values-sr-rCS/strings.xml @@ -32,6 +32,8 @@ Nema dostupnih aplikacija Dodajte izvor aplikacije ili omogućite postojeći u %s Nema aplikacija koje odgovaraju \"%1$s\" + Sve su aplikacije sakrivene + Sve ste aplikacije sakrili sa početnog ekrana Koju aplikaciju želite da zakrpite? @@ -91,10 +93,14 @@ Da bi popravili <b>%s</b>, potrebno je nepokvarenog APK verzija: Preporučeno Neopterećeno + Nekompatibilan Da, pomozi mi da pronađem APK Ne, već imam APK Koristišć saćuvani APK (v%s) Nema sačuvanog APK-a. Krpljenje će zahtevati ponovni odabir APK datoteke + Zahteva Android %1$s+ + Nije podržano na ovom uređaju + <b>%1$s</b> nema podržanih verzija za Android %2$d (API %3$d). Sve deklarisane verzije zahtevaju višu verziju Androida Preuzmi originalnu APK Uputstva: @@ -221,6 +227,7 @@ Da li ste sigurni da želite nastaviti?" Nova verzija patchova je dostupna. Ponovno patchirajte svoju aplikaciju da bi dobili najnovije poboljšanja i ispravke Aplikacija je deinstalirana Ova aplikacija je deinstalirana izvan Morphe. Ponovo je instalirajte da bi obnovili funkcionalnost + Veličina APK-a Režim korena zahteva da je originalni APK instaliran na vašem uređaju pre patchanja GmsCore zakrpa za podršku je isključena u root modu @@ -308,16 +315,13 @@ Da li ste sigurni da želite nastaviti?" Došlo je do greške pri primeni %s Zakrpljena aplikacija sačuvana za kasnije Došlo je do greške pri čuvanju zakrpljene aplikacije - Preuzmi APK datoteku Potrebna je interakcija korisnika da bi se nastavilo sa ovim dodatkom Proces patchera završio se kodom %1$s Uspešno instalirano Nije uspelo instaliranje aplikacije: %s - Instalacija nije završena. Provjerite sistemski dijalog instalacije i pokušajte ponovo Sačuvana APK datoteka Nije uspelo izvoz zakrpljene aplikacije Uklonjena sačuvana zakrpljena aplikacija - Uklonjena sačuvana kopija Instalirajte aplikaciju prije otvaranja Naziv paketa Nije moguće montirati: %s @@ -337,6 +341,10 @@ Da li ste sigurni da želite nastaviti?" Rešetka Čestice Bez + Nasumično + Pokretanje + Dnevno + Svaki 3 dana Paralaksni efekat Glatko pomeranje pozadine pri naginjanju uređaja @@ -353,6 +361,10 @@ Da li ste sigurni da želite nastaviti?" Trenutni jezik Prevodi za neke jezike mogu biti nepotpuni ili nedostajati Da biste preveli novi jezike ili poboljšali postojeće prevode, posetite %s + + Početni ekran + Pozdravne poruke + Prikaži poruku dobrodošlice na početnom ekranu Ikonica aplikacije Podrazumevana @@ -399,8 +411,17 @@ Da li ste sigurni da želite nastaviti?" Omogući složeno podešavanje Morphe i opcije za prilagođavanje Omogućiti režim eksperta? Režim eksperta daje više kontrole nad primenom patchova, ali pogrešno podešavanje u režimu eksperta može da rezultuje u nefunkcionalnoj aplikaciji - Ukloni neiskoristane native biblioteka - Brisanje neiskoristane native biblioteka može usporedati patchiranje ali možda neće funkcionisati na stariji uređenicima + Optimizujte za arhitekturu uređaja + "Preskoči split APK module za neproverene CPU arhitekture, lokale i rezolucije ekrana tokom spajanja. +Za obične APK-ove, uklanja nativne biblioteke za neproverene arhitekture nakon zakrpavanja" + + Način obrade bajtkoda + Kontroliše kako se bajtkod obrađuje tokom popravki. Utiče na brzinu popravki, korišćenje memorije i veličinu izlaznog APK fajla + Brzo (Preporučeno) + Brzo + Brže ispravljanje i manja upotreba memorije, uz veći APK. Preporučeno za uređaje sa malo RAM-a ili starije uređaje + Puno + Sporije ispravljanje, replicira staro ponašanje. Koristiti samo ako brzi režim izaziva probleme GitHub PAT Potrebno za izvore zahtjeva za povlačenje @@ -494,13 +515,7 @@ Opciono, putanja sadrži ikonu obaveštenja u jednom od sledećih formata: Instalirana verzija Shizuku nije podržana Otvori Shizuku Instalacija nije uspjela. Pokušajte ponovo ili prebacite na različitog instalera - Instalacija je blokirana od strane Androida. Provjerite Play Protect ili postavke sigurnosti Druga aplikacija sa istim imenom paketa je već instalirana. Deinstalirajte je prije nastavka - APK nije kompatibilan sa ovim uređajem ili verzijom Androida - APK je nevažeći ili oštećen - Nema dovoljno prostora na disku za instalaciju - Vremensko ograničenje za instalaciju je isteklo. Pokušajte ponovo - Instalacija je prekinuta Otvaranje %1$s… %1$s nije potvrdio instalaciju. Provjerite drugu aplikaciju i pokušajte ponovo %1$s je izvijestio o uspješnoj instalaciji @@ -531,6 +546,7 @@ Opciono, putanja sadrži ikonu obaveštenja u jednom od sledećih formata: Pogrešni izvor Lozinka Uvoz + Format kejstora Pogrešni izvor Kejtore uvezeno Nijeleženo uvoz kejtora @@ -538,6 +554,7 @@ Opciono, putanja sadrži ikonu obaveštenja u jednom od sledećih formata: Izvezi trenutni kejtore Nema dostupnih kejtora za izvoz Kejtore izvezeno + Nije uspelo izvozivanje skladišta ključeva Prikaži lozinku Sakrij lozinku @@ -666,7 +683,8 @@ Opciono, putanja sadrži ikonu obaveštenja u jednom od sledećih formata: Dozvoli Detalji Sakrij - Podrazumevana + Skriven + Pokaži Dnevnici Izvori Instaliranje… diff --git a/app/src/main/res/values-sr-rSP/plurals.xml b/app/src/main/res/values-sr-rSP/plurals.xml index 9f740cbe8..4039ee9e5 100644 --- a/app/src/main/res/values-sr-rSP/plurals.xml +++ b/app/src/main/res/values-sr-rSP/plurals.xml @@ -2,28 +2,28 @@ %s патч - %s патича + %s исправка %s патича %s опција патича - %s опције патича + %s опција поправке %s опција патича %s извор - %s извора + %s извор %s извора %s пакет - %s пакета + %s пакет %s пакета - Изведи %s пакет - Изведи %s пакета - Изведи %s пакета + Извршена %s закрпа + Извршене %s закрпе + Извршене %s закрпе %s АПК датотека @@ -45,4 +45,14 @@ %s пакета изабрано %s пакета изабрано + + Покажите %s апликацију + Покажите %s апликацију + Покажите %s апликација + + + %s скривена апликација + %s скривене апликације + %s скривене апликације + diff --git a/app/src/main/res/values-sr-rSP/strings.xml b/app/src/main/res/values-sr-rSP/strings.xml index b9d3ac304..e29997eac 100644 --- a/app/src/main/res/values-sr-rSP/strings.xml +++ b/app/src/main/res/values-sr-rSP/strings.xml @@ -32,6 +32,8 @@ Нисe доступни апликације Додај извор пача или омогући постојећи у %s Нисe пронађени апликације које одговарају \"%1$s\" + Све апликације су скривене + Скрио/Скриле сте све апликације са почетног екрана Коју апликацију желите да печујете? @@ -91,10 +93,14 @@ Da bi popravili <b>%s</b>, potrebno je nepatchovani APK verzija: Preporučeno Необрађен + Некомпатибилан Да, помози ми да нађем АПК Ne, već imam APK Koristišć saćuvani APK (v%s) Nema saćuvanog APK-a. Patching zahtijeva ponovno odabir APK datoteke + Захтева Android %1$s+ + Није подржано на овом уређају + <b>%1$s</b> нема подржаних верзија за Android %2$d (API %3$d). Све декларисане верзије захтевају вишу верзију Android-а Преузмите оригинални APK Упутства: @@ -220,6 +226,7 @@ Доступна је нова верзија патчева. Репатујте своју апликацију да добијете најновије побољшања и исправке Aplikacija je deinstalirana Ova aplikacija je deinstalirana izvan Morphe. Ponovo je instalirajte da bi obnovili funkcionalnost + Величина APK Root режим захтева да оригинални APK буде инсталиран на вашем уређају пре печовања Печ подршке GmsCore је искључен у root режиму @@ -307,16 +314,13 @@ Није успело да се примени %s Крпељом потписана апликација сачувана за касније Није успело чување крпељом потписане апликације - Преузми APK фајл Неопходна је корисничка интеракција да бисте наставили са овом додатком. Процес пачера изашао са кодом %1$s Успешно инсталирано Није успело инсталирање апликације: %s - Инсталација није завршена. Проверите дијалог система за инсталацију и покушајте поново. АПК је сачуван Није успео извоз покрпљене апликације Уклоњена сачувана покрпљена апликација - Уклоњена сачувана копија Инсталирајте апликацију пре отварања. Име пакета Није успело монтирање: %s @@ -336,6 +340,10 @@ Мрежа Партикле Ништа + Насумично + При покретању + Дневно + Свака 3 дана Efekt paralaksa Umiranje pozadine pri kretanju ugao naprijed nazad @@ -352,6 +360,10 @@ Тренутна језика Недостаје превода за неке језике Да бисте додали нове језике или побољшали постоје преводе, посетите %s + + Почетни екран + Почетне фразе + Прикажи поруку на почетном екрану Иконица апликације Подразумеван @@ -398,8 +410,17 @@ Omogući kompleksno podešavanje Morphe i personalizacija opcija Omogući režim eksperta? Režim eksperta omogućava veću kontrolu nad primenom patchova, ali pogrešno podešavanje postavki u režimu eksperta može da rezultuje nefunkcionalnom aplikacijom - Уклонити ненужне оригиналне библиотеке - Брисање ненужних оригиналних библиотека може повећати патчирање, али може бити недовољно за старије уређаје + Optimizujte za arhitekturu uređaja + "Preskoči SPLIT APK module za nepoznate CPU arhitekture, lokale i rezolucije ekrana tokom objedinjavanja. +Za obične APK-ove, uklanja nativne biblioteke za nepoznate arhitekture nakon 'patch'ovanja" + + Начин рада обраде байткода + Контролише како се обрађује байткод током поправке. Утиче на брзину поправке, коришћење меморије и величину излазне APK датотеке + Брзо (Препоручено) + Брзо + Брже патчинг и мање коришћења меморије, на рачун веће APK величине. Препоручиво за уређаје са малом количином RAM или старије уређаје + Потпуно + Полако пачирање, реплицира понашање старије верзије. Користите само ако брзи режим изазове проблеме GitHub PAT Неопходно за изворе из GitHub pull request @@ -509,13 +530,7 @@ morphe_header_custom_dark.png Верзија Шизуку која је инсталирана није подржана. Отвори Схизуку Инсталација је неуспела. Покушајте поново или пребаците на други инсталатор. - Инсталација је блокирана од стране Андроида. Проверите Play Protect или подешавање безбедности. Друга апликација са истим именом пакета је већ инсталирана. Деинсталирајте је пре наставка. - АПК није компатибилан са овом уређајем или верзијом Андроида. - АПК је неважећи или оштећен. - Нема довољно места за инсталацију. - Време за инсталацију је истекло. Покушајте поново. - Инсталација је отказана. Отварање %1$s… %1$s није потврдио инсталацију. Проверите другу апликацију и покушајте поново. %1$s известио је о успешној инсталацији. @@ -546,6 +561,7 @@ morphe_header_custom_dark.png Корисник (Алиас) Лозинка Увоз + Формат кејстора Погрешно унете податке кластерског кључа Кластерски кључ успешно увезен Неуспешно увоз кластерског кључа @@ -553,6 +569,7 @@ morphe_header_custom_dark.png Извезите тренутни кластерски кључ Нема доступног кластерског кључа за извоз Кластерски кључ успешно извезен + Неуспешно извезена keystore Прикажите лозинку Сакрити лозинку @@ -681,7 +698,8 @@ morphe_header_custom_dark.png Дозволи Detalji Sakrij - Подразумеван + Сакривено + Покажи Дневници Извори Инсталирање… diff --git a/app/src/main/res/values-sv-rSE/plurals.xml b/app/src/main/res/values-sv-rSE/plurals.xml index 0309a4f3e..583ceca3b 100644 --- a/app/src/main/res/values-sv-rSE/plurals.xml +++ b/app/src/main/res/values-sv-rSE/plurals.xml @@ -1,12 +1,12 @@ - %s korrigering - %s korrigeringar + %s tillägg + %s tillägg - %s korrigeringsalternativ - %s korrigeringsalternativ + %s tilläggsalternativ + %s tilläggsalternativ %s källa @@ -17,23 +17,31 @@ %s paket - Kör %s korrigering - Kör %s korrigeringar + Körde %s tillägg + Körde %s tillägg %s APK-fil %s APK-filer - Tog bort %s inaktuell korrigering från det sparade urvalet - Tog bort %s inaktuella korrigeringar från det sparade urvalet + Tog bort %s inaktuellt tillägg från det sparade urvalet + Tog bort %s inaktuella tillägg från det sparade urvalet %s installerad källa %s installerade källor - %s vald korrigering - %s valda korrigeringar + %s valt tillägg + %s valda tillägg + + + Visa %s app + Visa %s appar + + + %s dold app + %s dolda appar diff --git a/app/src/main/res/values-sv-rSE/strings.xml b/app/src/main/res/values-sv-rSE/strings.xml index ee365b795..88a277dd2 100644 --- a/app/src/main/res/values-sv-rSE/strings.xml +++ b/app/src/main/res/values-sv-rSE/strings.xml @@ -2,8 +2,8 @@ Andra appar - Inte korrigerad än - Inga korrigeringar är tillgängliga + Inte modifierad än + Inga tillägg är tillgängliga Dölj den här appen? Den här appen döljs från startsidan. Du kan återställa den senare med knappen \"%1$s\" längst ned i listan Den här appen döljs @@ -14,12 +14,12 @@ Fel i Android 11 Behörigheten för appinstallation måste beviljas i förväg för att undvika ett fel i Android 11-systemet som negativt påverkar användarupplevelsen Lägg till källa - Vill du lägga till den här korrigeringskällan i Morphe? + Vill du lägga till den här tilläggskällan i Morphe? Lägg bara till källor från källor du litar på Uppdatering för Morphe är tillgänglig Ny version är redo att installeras - Uppdateringen misslyckades + Uppdatering misslyckades Kontrollera din internetanslutning och försök igen Uppdateringar hoppas över Mobildata är aktivt @@ -32,12 +32,14 @@ Inga appar är tillgängliga Lägg till en källa eller aktivera en befintlig i %s Inga appar matchar \"%1$s\" + Alla appar är dolda + Du har dolt alla appar från startsidan - Vilken app vill du korrigera? + Vilken app vill du modifiera? Redo att göra appar legendariska igen? - Spänn fast dig, det är dags för korrigeringar + Spänn fast dig, det är dags för modifiering En annan dag, ett annat mästerverk med annonsblockering @@ -45,55 +47,60 @@ Vi får dina appar att uppföra sig - Dagens prognos: 100 % risk för korrigeringar + Dagens prognos: 100 % risk för modifiering Redo att göra det medelmåttiga fantastiskt? Räddar appar från sig själva sedan i dag - Drivs av koffein och bra korrigeringar + Drivs av koffein och bra tillägg - Korrigeringskällor + Tilläggskällor Öppna i webbläsaren Det gick inte att öppna webbadressen - Använd förhandsversioner av korrigeringar - Få tidig åtkomst till nya experimentella korrigeringsversioner + Använd förhandsversioner av tillägg + Få tidig åtkomst till nya experimentella tilläggsversioner Använd experimentella appversioner - Korrigera experimentella appmål om sådana finns + Modifiera experimentella appmål om sådana finns Den här källan har redan lagts till Visningsnamn - Byt namn på korrigeringskällan - Det finns redan en korrigeringskälla med det här namnet - Det gick inte att uppdatera den här korrigeringskällan - Valfri version - Valfritt paket - Lägg till korrigeringskälla + Byt namn på tilläggskällan + Det finns redan en tilläggskälla med det här namnet + Det gick inte att uppdatera den här tilläggskällan + Alla versioner + Alla paket + Lägg till tilläggskälla Ta bort källan \"%s\"? Det går inte att ångra den här åtgärden Fjärrkälla Webbadress för källan Exempel: Lokal - Välj källfil för korrigeringar - Välj en källfil för korrigeringar i formatet .mpp från lagringen + Välj källfil för tillägg + Välj en källfil för tillägg i formatet .mpp från lagringen Byt fil Förinstallerad Uppdateringen lyckades Ingen uppdatering är tillgänglig Det gick inte att ladda ned källor: %s - Det gick inte att uppdatera \"%1$s\": JSON-filen för korrigeringarna hittades inte på den konfigurerade webbadressen + Det gick inte att ladda ned \"%1$s\": %2$s + Det gick inte att uppdatera \"%1$s\": JSON-filen för tillägg hittades inte på den konfigurerade webbadressen Expert - Aktivera Expertläge i inställningarna för att inkludera den här korrigeringen manuellt + Aktivera Expertläge i inställningarna för att inkludera det här tillägget manuellt - Behöver du hjälp att hitta en ursprunglig okorrigerad APK-fil? - Om du vill korrigera <b>%s</b> behöver du en okorrigerad APK-fil med följande version: - Om du vill korrigera <b>%s</b> behöver du en okorrigerad APK-fil med en av följande versioner: + Behöver du hjälp att hitta en ursprunglig omodifierad APK-fil? + Om du vill modifiera <b>%s</b> behöver du en omodifierad APK-fil med följande version: + Om du vill modifiera <b>%s</b> behöver du en omodifierad APK-fil med en av följande versioner: Rekommenderad - Okorrigerad + Omodifierad + Inkompatibel Ja, hjälp mig att hitta en APK-fil Nej, jag har redan en APK-fil Använd den sparade APK-filen (v%s) - Ingen sparad APK-fil. Appkorrigeringen kräver att du väljer APK-filen igen + Ingen sparad APK-fil. Modifieringen kräver att du väljer APK-filen igen + Kräver Android %1$s+ + Stöds inte på den här enheten + <b>%1$s</b> har inga versioner som stöds för Android %2$d (API %3$d). Alla deklarerade versioner kräver en senare Android-version Ladda ned den ursprungliga APK-filen Anvisningar: @@ -111,24 +118,24 @@ Öppna APK-fil Rekommenderad version Vald version - Välj en APK-fil - Tryck på knappen nedan för att välja en APK-fil för valfri app som du vill korrigera + Välj APK-fil + Tryck på knappen nedan för att välja en APK-fil för valfri app som du vill modifiera Det gick inte att läsa filen. Kontrollera att Extern lagring inte har batteribegränsningar Den valda filen är inte en giltig APK-fil Det gick inte att öppna filen. Försök igen Expertläge - Sök efter korrigeringar - Fortsätt till appkorrigering + Sök efter tillägg + Fortsätt till modifiering Inaktivera alla Aktivera alla - Aktivera rekommenderade korrigeringar + Aktivera rekommenderade tillägg Återställ sparat urval - Inga korrigeringar hittades - Universella korrigeringar + Inga tillägg hittades + Universella tillägg Ny - Flera korrigeringskällor har valts - "Du har valt korrigeringar från flera korrigeringskällor. Det kan orsaka kompatibilitetsproblem eller oväntat beteende. " + Flera tilläggskällor har valts + "Du har valt tillägg från flera tilläggskällor. Det kan orsaka kompatibilitetsproblem eller oväntat beteende. " Overifierad APK-fil Den APK-fil du har valt för <b>%s</b> matchar inte det förväntade signeringscertifikatet. Den kan ha modifierats eller vara från en opålitlig källa. @@ -137,27 +144,27 @@ "Den här filen är ett APK-paket (<b>APKM/APKS/XAPK</b>). -För bästa resultat rekommenderar den här appen att du korrigerar en <b>fullständig APK-fil</b>" +För bästa resultat rekommenderar den här appen att du modifierar en <b>fullständig APK-fil</b>" Välj en annan APK-fil Experimentell Den här versionen har experimentellt stöd och kan vara instabil eller ofullständig Vill du experimentera? 🧪 - Den här versionen av <b>%s</b> har tidigt experimentellt stöd<br/><br/>🔧 Räkna med underliga appbeteenden eller okända fel medan korrigeringarna finjusteras för den här appversionen + Den här versionen av <b>%s</b> har tidigt experimentellt stöd<br/><br/>🔧 Räkna med underligt appbeteende eller okända fel medan tilläggen finjusteras för den här appversionen Version som inte stöds Den här APK-filen är inte en rekommenderad appversion. Om du fortsätter kan det leda till att appen inte fungerar Fortsätt ändå Kompatibla versioner: Stöds inte Fel paket har valts - Den valda APK-filen är inte för den app du avsåg att korrigera + Den valda APK-filen är inte för den app du avsåg att modifiera Förväntat paket: Valt paket: - Korrigeringar kan vara inaktuella - Mobildata är aktivt och korrigeringsuppdateringar har inaktiverats - Om du korrigerar med inaktuella korrigeringar kan det leda till en trasig app - Uppdatera och korrigera + Tillägg kan vara inaktuella + Mobildata är aktivt och tilläggsuppdateringar har inaktiverats + Om du modifierar med inaktuella tillägg kan det leda till en trasig app + Uppdatera och modifiera Lågt diskutrymme - Endast %1$.2f GB ledigt utrymme är tillgängligt. Appkorrigeringen kräver minst %2$.2f GB ledigt utrymme för att fungera korrekt + Endast %1$.2f GB ledigt utrymme är tillgängligt. Modifieringen kräver minst %2$.2f GB ledigt utrymme för att fungera korrekt Om du fortsätter kan det leda till att du får ett felmeddelande om att filen inte hittades eller en skadad APK-fil Anpassad färg @@ -200,34 +207,35 @@ För bästa resultat rekommenderar den här appen att du korrigerar en <b> Är du säker på att du vill avinstallera den här appen? Detta tar bort följande permanent: Databaspost - Korrigerad APK-fil + Modifierad APK-fil Ursprunglig APK-fil - Ditt korrigeringsurval och dess inställningar sparas för framtida appkorrigering - Korrigeringar + Ditt tilläggsurval och dess inställningar sparas för framtida modifiering + Tillägg Namnlös - Det gick inte att importera korrigeringar: %s - Den ursprungliga APK-filen hittades inte. Det går inte att korrigera den här appen igen + Det gick inte att importera tillägg: %s + Den ursprungliga APK-filen hittades inte. Det går inte att modifiera den här appen igen Ursprungligt paketnamn - Tillämpade korrigeringar - Korrigeringskälla som används + Tillämpade tillägg + Tilläggskälla som används Installationstyp Anpassad installerare Systemets installerare - Korrigerades - Uppdatering för korrigeringar är tillgänglig - En nyare version av korrigeringarna är tillgänglig. Korrigera din app igen för att få de senaste förbättringarna och felkorrigeringarna + Modifierades + Uppdatering för tillägg är tillgänglig + En nyare version av tilläggen är tillgänglig. Modifiera din app igen för att få de senaste förbättringarna och felkorrigeringarna Appen har avinstallerats - Den här appen avinstallerades utanför Morphe. Korrigera den igen för att återställa funktionaliteten + Den här appen avinstallerades utanför Morphe. Modifiera den igen för att återställa funktionaliteten + Storlek på APK-fil - Rotläget kräver att den ursprungliga APK-filen är installerad på din enhet innan du korrigerar appen - Stödkorrigeringen för GmsCore exkluderas i rotläge + Rotläget kräver att den ursprungliga APK-filen är installerad på din enhet innan du modifierar appen + Tillägget för GmsCore-stöd exkluderas i rotläge Välj installationsmetod - Din enhet har rotåtkomst. Välj hur du vill installera den korrigerade appen + Din enhet har rotåtkomst. Välj hur du vill installera den modifierade appen Rotmontering - Montera den korrigerade APK-filen över standardappen. Inget GmsCore behövs + Montera den modifierade APK-filen över standardappen. Inget GmsCore behövs Standardinstallation - Installera som en separat app med stöd för GmsCore. Välj en installerare (Systemets installerare, Shizuku med flera) efter appkorrigeringen - Installationsmetoden bestämmer vilka korrigeringar som ingår och kan inte ändras efter appkorrigeringen + Installera som en separat app med stöd för GmsCore. Välj en installerare (Systemets installerare, Shizuku med flera) efter modifieringen + Installationsmetoden bestämmer vilka tillägg som ingår och kan inte ändras efter modifieringen Fel kopierat till urklipp Fellogg @@ -236,11 +244,11 @@ För bästa resultat rekommenderar den här appen att du korrigerar en <b> Det här steget kan ta en stund. Vänta … Nästan redo att rocka loss! 🎸 Klart! 🎉 - Din app är korrigerad och installerad! - Appkorrigeringen är klar! ✨ + Din app är modifierad och installerad! + Modifieringen är klar! ✨ Hoppsan! Något gick fel Kontrollera felinformationen nedan - Appkorrigeringen misslyckades + Modifieringen misslyckades Okänt fel uppstod Paketkonflikt Du måste avinstallera den befintliga versionen @@ -249,8 +257,8 @@ För bästa resultat rekommenderar den här appen att du korrigerar en <b> Tryck på knappen nedan för att installera Tryck på knappen nedan för att montera Det går inte att komma åt lagringssökväg - En eller fler korrigeringsalternativ pekar på sökvägar som Morphe inte kan läsa. Ge åtkomst till lagringen och tryck på knappen så startas appkorrigeringen automatiskt - En eller fler korrigeringsalternativ pekar på sökvägar som inte går att komma åt. Uppdatera sökvägarna i korrigeringsalternativen och försök igen + Ett eller fler tilläggsalternativ pekar på sökvägar som Morphe inte kan läsa. Ge åtkomst till lagringen och tryck på knappen så startas modifieringen automatiskt + Ett eller fler tilläggsalternativ pekar på sökvägar som inte går att komma åt. Uppdatera sökvägarna i tilläggsalternativen och försök igen Ge åtkomst till lagring Åtkomst till lagringen nekades. Ge behörighet att använda sökvägen eller flytta filerna till appens privata katalog Öppna lagringsinställningar @@ -259,18 +267,18 @@ För bästa resultat rekommenderar den här appen att du korrigerar en <b> Tips: Flytta filerna till /Android/data/app.morphe.manager/files/ – inga extra behörigheter behövs Förbereder - Läser in korrigeringar + Läser in tillägg Slår samman delad APK-fil Läser APK-fil - Korrigerar + Modifierar Sparar - Skriver korrigerad APK-fil - Signerar korrigerad APK-fil + Skriver modifierad APK-fil + Signerar modifierad APK-fil - Appkorrigering pågår … - Tryck för att återgå till appkorrigeringen - Stoppa appkorrigeringen - Är du säker på att du vill stoppa appkorrigeringsprocessen? + Modifiering pågår … + Tryck för att återgå till modifieringen + Stoppa modifieringen + Är du säker på att du vill stoppa modifieringsprocessen? Omvandlar din app till något fantastiskt … 🏆 Lär gamla appar att sitta … 🎩 @@ -281,40 +289,37 @@ För bästa resultat rekommenderar den här appen att du korrigerar en <b> Justerar ettor och nollor … 🤖 Reder ut digital spaghetti … 🍝 - Åtgärdar fel i den okorrigerade appen … 🪲 - Bra korrigeringar är värda att vänta på … ⏳ + Åtgärdar fel i den omodifierade appen … 🪲 + Bra tillägg är värda att vänta på … ⏳ Polerar pixlarna tills de glänser … 💎 - Vänligen sitt kvar tills kaptenen har släckt skylten Appkorrigering pågår … 💺✈️ - Tillreder din korrigerade app, varm och färsk … 🍳 + Vänligen sitt kvar tills kaptenen har släckt skylten Modifiering pågår … 💺✈️ + Tillreder din modifierade app, varm och färsk … 🍳 Förhandlar artigt med bytekoden … 🤝 - Systemet är normalt … korrigeringsbanan är stabil … 🚀 + Systemet är normalt … modifieringsbanan är stabil … 🚀 Pressar ut extra kraft, som saft ur en digital citron … 🍋 Uppmuntrar appen att acceptera sitt nya, bättre jag … 🌱 - Ändrar appens superposition från okorrigerad till korrigerad … 📦⚛️ + Ändrar appens superposition från omodifierad till modifierad … 📦⚛️ - Din app har feber, och det enda botemedlet är fler korrigeringar … 🩺🔔 + Din app har feber, och det enda botemedlet är fler tillägg … 🩺🔔 Tar bort nonsens. Vänligen vänta … 🗑️ - Korrigerar appen + Modifierar appen Minnesanvändning - Tillämpar korrigeringar + Tillämpar tillägg Det gick inte att tillämpa %s - Den korrigerade appen sparades till senare - Det gick inte att spara den korrigerade appen - Ladda ned APK-fil + Den modifierade appen sparades till senare + Det gick inte att spara den modifierade appen Användarinteraktion krävs för att fortsätta med det här plugin-programmet - Appkorrigeringsprocessen avslutades med koden %1$s + Modifieringsprocessen avslutades med koden %1$s Installationen lyckades Det gick inte att installera appen: %s - Installationen slutfördes inte. Kontrollera systemets installationsdialogruta och försök igen APK-fil sparad - Det gick inte att exportera den korrigerade appen - Tog bort den sparade korrigerade appen - Tog bort den sparade kopian + Det gick inte att exportera den modifierade appen + Tog bort den sparade modifierade appen Installera appen innan du öppnar den Paketnamn Det gick inte att montera: %s @@ -350,6 +355,10 @@ För bästa resultat rekommenderar den här appen att du korrigerar en <b> Aktuellt språk Översättningar kan saknas eller vara ofullständiga för vissa språk Om du vill översätta till nya språk eller förbättra de befintliga översättningarna, besök %s + + Startsida + Hälsningsfraser + Visa ett hälsningsmeddelande på startsidan Appikon Standard @@ -364,9 +373,9 @@ För bästa resultat rekommenderar den här appen att du korrigerar en <b> Uppdateringar Använd förhandsversioner av Morphe - Få tidig åtkomst till nya Morphe-funktioner. Om du vill få förhandsversioner av korrigeringar aktiverar du reglaget för förhandsversioner i varje enskild korrigeringskälla + Få tidig åtkomst till nya Morphe-funktioner. Om du vill få förhandsversioner av tillägg aktiverar du reglaget för förhandsversioner i varje enskild tilläggskälla Uppdatera via mobildata - Tillåt att uppdateringar för Morphe och korrigeringar laddas ned via mobildata + Tillåt att uppdateringar för Morphe och tillägg laddas ned via mobildata Aviseringar om uppdateringar i bakgrunden Sök regelbundet efter uppdateringar i bakgrunden och avisera när de är tillgängliga Få direkta pushmeddelanden om nya versioner, även när appen är stängd @@ -380,24 +389,33 @@ För bästa resultat rekommenderar den här appen att du korrigerar en <b> Sökningar i bakgrunden kanske inte fungerar tillförlitligt på vissa enheter. Tillverkare som Xiaomi, Huawei, Samsung och OnePlus begränsar ofta aggressivt bakgrundsprocesser för att spara batteri Uppdatering för Morphe är tillgänglig - Nya korrigeringar är tillgängliga + Nya tillägg är tillgängliga Version %1$s är redo att laddas ned Tryck för att öppna Morphe och uppdatera Tillåt aviseringar Morphe behöver behörighet att skicka aviseringar för att kunna meddela dig när uppdateringar är tillgängliga i bakgrunden Vill du få aviseringar när uppdateringar är tillgängliga? Aviseringar om uppdateringar - Aviseringar om nya versioner av Morphe och korrigeringar - Appkorrigering - Visas när Morphe korrigerar en app i bakgrunden + Aviseringar om nya versioner av Morphe och tillägg + Modifiering + Visas när Morphe modifierar en app i bakgrunden Expertinställningar Expertläge - Aktivera komplexa appkorrigeringsinställningar och anpassningsalternativ för Morphe + Aktivera komplexa modifieringsinställningar och anpassningsalternativ för Morphe Aktivera expertläge? - Expertläget ger mer kontroll över hur korrigeringarna tillämpas, men om du felaktigt konfigurerar inställningarna i expertläget kan det leda till att appen inte fungerar - Ta bort oanvända plattformsspecifika bibliotek - Ta bort plattformsspecifika bibliotek för CPU-arkitekturer som inte stöds från korrigerade appar. + Expertläget ger mer kontroll över hur tilläggen tillämpas, men om du felaktigt konfigurerar inställningarna i expertläget kan det leda till att appen inte fungerar + Optimera för enhetsarkitektur + "Hoppa över delade APK-moduler för CPU-arkitekturer, språk och skärmupplösningar som inte stöds vid sammanslagning. +Vanliga APK-filer rensas från inbyggda bibliotek för arkitekturer som inte stöds efter att appen har modifierats" + + Läge för bytekodbearbetning + Kontrollerar hur bytekod bearbetas när appar modifieras. Detta påverkar modifieringshastighet, minnesanvändning och den slutliga APK-filens storlek + Snabbt (Rekommenderas) + Snabbt + Snabbare modifiering och lägre minnesanvändning, på bekostnad av en större APK-fil. Rekommenderas för enheter med lågt RAM-minne eller äldre enheter + Fullt + Långsammare modifiering, replikerar gammalt beteende. Använd endast om läget Snabbt orsakar problem PAT för GitHub Krävs för att använda hämtningsbegäranden som källor @@ -409,20 +427,20 @@ För bästa resultat rekommenderar den här appen att du korrigerar en <b> Lägger till denna PAT i exporterade Morphe-inställningar Dela inte din PAT. Om du inkluderar den i en inställningsexport ska du hålla filen privat eftersom den innehåller din token - Korrigeringsalternativ + Tilläggsalternativ Alternativ för tema, varumärkesanpassning och sidhuvud - Om du ändrar korrigeringsalternativen måste du korrigera appen igen för att ändringarna ska träda i kraft - I expertläget blir korrigeringsalternativ tillgängliga under appkorrigeringsprocessen + Om du ändrar tilläggsalternativen måste du modifiera appen igen för att ändringarna ska träda i kraft + I expertläget blir tilläggsalternativ tillgängliga under modifieringsprocessen Temafärger för appen Ändra bakgrundsfärg Bakgrundsfärg för mörkt tema Bakgrundsfärg för ljust tema Kan vara en hexfärg (#RRGGBB) eller en färgresursreferens - Läser in korrigeringsalternativ … - Vänta tills korrigeringskällan har uppdaterats - Det gick inte att läsa in korrigeringsalternativ - Inga alternativ är tillgängliga för den här korrigeringen + Läser in tilläggsalternativ … + Vänta tills tilläggskällan har uppdaterats + Det gick inte att läsa in tilläggsalternativ + Inga alternativ är tillgängliga för det här tillägget Anpassat varumärke Ändra appens visningsnamn @@ -493,7 +511,7 @@ Bildernas mått ska vara följande: Systemets installerare Använd Androids pakethanterare Rootad monteringsinstallerare - Monterar den korrigerade APK-filen över den installerade appen + Monterar den modifierade APK-filen över den installerade appen Installera tyst med Shizuku eller Sui Ingen alternativ installerare Ingen alternativ installerare har konfigurerats @@ -503,17 +521,11 @@ Bildernas mått ska vara följande: Starta Shizuku eller Sui och försök igen Ge Shizuku behörighet till Morphe från Shizuku-appen Din primära installerare har angetts till den rootade monteringsinstalleraren. Den här sparade appen installerades inte med montering - Standardappens version (%2$s) matchar inte den korrigerade appens version (%1$s). Montera den matchande standardversionen och försök igen + Standardappens version (%2$s) matchar inte den modifierade appens version (%1$s). Montera den matchande standardversionen och försök igen Den installerade Shizuku-versionen stöds inte Öppna Shizuku Installationen misslyckades. Försök igen eller byt till en annan installerare - Installationen blockerades av Android. Kontrollera Play Protect eller säkerhetsinställningarna En annan app med det här paketnamnet är redan installerad. Avinstallera den innan du fortsätter - APK-filen är inte kompatibel med den här enheten eller Android-versionen - APK-filen är ogiltig eller skadad - Det finns inte tillräckligt med lagringsutrymme för installationen - Tidsgränsen överskreds för installationen. Försök igen - Installationen avbröts Öppnar %1$s … %1$s bekräftade inte installationen. Kontrollera den andra appen och försök igen %1$s meddelade om en lyckad installation @@ -522,16 +534,16 @@ Bildernas mått ska vara följande: Se till att Shizuku körs och att appen har tilldelats behörighet Använd standardinstalleraren Be om installerare vid installation - Visa en dialogruta för att välja en installerare varje gång du installerar en korrigerad app + Visa en dialogruta för att välja en installerare varje gång du installerar en modifierad app Prestanda Processexekvering - Kör appkorrigeringen i en separat process för bättre stabilitet + Kör modifieringen i en separat process för bättre stabilitet Minnesgräns - Högre minnesgränser kan göra appkorrigeringen snabbare, men det kan hända att det inte fungerar med äldre enheter + Högre minnesgränser kan göra modifieringen snabbare, men det kan hända att det inte fungerar med äldre enheter Aktivera processexekvering - Tillgängligt minne för appkorrigeringen - En låg minnesgräns kan leda till fel när du korrigerar större appar + Tillgängligt minne för modifieringen + En låg minnesgräns kan leda till fel när du modifierar större appar Gräns på %s MB Tryck för att konfigurera Den här funktionen kräver Android 11 eller senare @@ -551,6 +563,7 @@ Bildernas mått ska vara följande: Exportera det aktuella nyckellagret Inget nyckellager tillgängligt att exportera Nyckellager exporterat + Det gick inte att exportera nyckellager Visa lösenord Dölj lösenord @@ -573,52 +586,52 @@ Bildernas mått ska vara följande: Lagringshantering Total storlek: %s - Korrigerade APK-filer - Hantera sparade korrigerade appfiler - Inga korrigerade APK-filer har sparats än - Ta bort korrigerad APK-fil? - Är du säker på att du vill ta bort den sparade korrigerade APK-filen för %s? - Korrigerad APK-fil har tagits bort + Modifierade APK-filer + Hantera sparade modifierade appfiler + Inga modifierade APK-filer har sparats än + Ta bort modifierad APK-fil? + Är du säker på att du vill ta bort den sparade modifierade APK-filen för %s? + Modifierad APK-fil har tagits bort Ursprungliga APK-filer - Ursprungliga APK-filer sparas för att kunna korrigeras igen. Endast den senaste versionen sparas för varje app + Ursprungliga APK-filer sparas för att kunna modifieras igen. Endast den senaste versionen sparas för varje app Inga ursprungliga APK-filer lagras Ursprunglig APK-fil har tagits bort Ta bort ursprunglig APK-fil? - Ta bort den ursprungliga APK-filen för %s? Du kan inte korrigera appen igen utan att ange en ny APK-fil + Ta bort den ursprungliga APK-filen för %s? Du kan inte modifiera appen igen utan att ange en ny APK-fil - Korrigeringsurval - Hantera sparade korrigeringsurval och korrigeringsalternativ. Dessa blir kvar om appar avinstalleras + Tilläggsurval + Hantera sparade tilläggsurval och tilläggsalternativ. Dessa blir kvar om appar avinstalleras Återställ alla urval? Återställ paketurvalen? Återställ källurvalet? - Inga sparade korrigeringsurval + Inga sparade tilläggsurval %1$s i %2$s %1$s i %2$s Källa %s Detta tar bort: - Detta tar bort alla sparade korrigeringsurval för alla paket och källor permanent - Detta tar bort alla sparade korrigeringsurval för %s i alla källor permanent - Detta tar bort det sparade korrigeringsurvalet för %1$s i källa %2$s permanent + Detta tar bort alla sparade tilläggsurval för alla paket och källor permanent + Detta tar bort alla sparade tilläggsurval för %s i alla källor permanent + Detta tar bort det sparade tilläggsurvalet för %1$s i källa %2$s permanent Det gick inte att exportera källdata Källdata har exporterats Det gick inte att importera källdata Källdata har importerats - Korrigeringsinformation - Valda korrigeringar (%s) - Korrigeringsalternativ (%s) - Inga korrigeringar eller alternativ har sparats + Information om tillägg + Valda tillägg (%s) + Tilläggsalternativ (%s) + Inga tillägg eller alternativ har sparats Om Dela webbplatsen Dela Morphes officiella webbplats - "Ett projekt med öppen källkod för modern, smidig korrigering av populära Android-appar som drivs av feedback och bidrag från communityn" + "Ett projekt med öppen källkod för modern och smidig modifiering av populära Android-appar som drivs av feedback och bidrag från communityn" Medverkande Nuvarande utveckling Tidigare utveckling Licenser för öppen källkod - Korrigeringar + Tillägg Hem Inställningar Ja @@ -660,8 +673,8 @@ Bildernas mått ska vara följande: Tillgänglig Fortsätt Version - Korrigera - Korrigera igen + Modifiera + Modifiera igen Öppna Montera Monterad @@ -679,12 +692,15 @@ Bildernas mått ska vara följande: Tillåt Information Dölj - Standard + Dold + Visa Loggar + Källor Installerar … Monterar … Avmonterar … Importerar … + Importen lyckades Visa ändringsloggar Kolla in de senaste ändringarna i den här uppdateringen Det gick inte att ladda ned ändringsloggen: %s diff --git a/app/src/main/res/values-sw-rKE/strings.xml b/app/src/main/res/values-sw-rKE/strings.xml index 180ea1689..48f97ef60 100644 --- a/app/src/main/res/values-sw-rKE/strings.xml +++ b/app/src/main/res/values-sw-rKE/strings.xml @@ -39,10 +39,12 @@ + + diff --git a/app/src/main/res/values-ta-rIN/strings.xml b/app/src/main/res/values-ta-rIN/strings.xml index 8b03c94e5..44980d0e9 100644 --- a/app/src/main/res/values-ta-rIN/strings.xml +++ b/app/src/main/res/values-ta-rIN/strings.xml @@ -40,10 +40,12 @@ எதுவுமில்லை + + diff --git a/app/src/main/res/values-te-rIN/strings.xml b/app/src/main/res/values-te-rIN/strings.xml index 180ea1689..48f97ef60 100644 --- a/app/src/main/res/values-te-rIN/strings.xml +++ b/app/src/main/res/values-te-rIN/strings.xml @@ -39,10 +39,12 @@ + + diff --git a/app/src/main/res/values-th-rTH/plurals.xml b/app/src/main/res/values-th-rTH/plurals.xml index 9367b563d..cf0ade2d6 100644 --- a/app/src/main/res/values-th-rTH/plurals.xml +++ b/app/src/main/res/values-th-rTH/plurals.xml @@ -1,30 +1,36 @@ - %s ปัทช์ + %s แพตซ์ - %s ตัวเลือกปัทช์ + %s ตัวเลือกแพตซ์ - %s แหล่งข้อมูล + %s แหล่งที่มา %s แพ็คเกจ - ดำเนินการ %s แพตช์ + ดำเนินการแพตช์ %s รายการแล้ว %s ไฟล์ APK - ถอด %s แพตช์เก่าไปจากการเลือกที่ถูกบันทึกไว้ + ลบ %s แพตช์เก่าไปจากตัวเลือกที่ถูกบันทึกไว้ - %s ที่อยู่ที่ติดตั้งแล้ว + %s ที่มาที่ติดตั้งแล้ว - %s แพตช์ที่ถูกเลือก + %s แพตช์ถูกเลือก + + + แสดงแอป %s + + + แอปที่ซ่อนอยู่ %s diff --git a/app/src/main/res/values-th-rTH/strings.xml b/app/src/main/res/values-th-rTH/strings.xml index ad49b97ab..7be3fb989 100644 --- a/app/src/main/res/values-th-rTH/strings.xml +++ b/app/src/main/res/values-th-rTH/strings.xml @@ -21,19 +21,21 @@ เวอร์ชันใหม่พร้อมถูกติดตั้งแล้ว อัปเดตล้มเหลว ตรวจสอบการเชื่อมต่ออินเทอร์เน็ตของคุณแล้วลองอีกครั้ง - อัปเดตข้าม - ข้อมูลมือถือใช้งานอยู่ + อัปเดตถูกข้าม + กำลังใช้งานข้อมูลมือถือ กำลังอัพเดทข้อมูล… %1$s ถูกประมวลผลแล้ว - การอัพเดทแหล่งข้อมูลเสร็จสมบูรณ์แล้ว - แหล่งข้อมูลของคุณเป็นเวอร์ชันล่าสุดแล้ว - กำลังโหลดแหล่งข้อมูล กรุณารอสักครู่… + อัพเดทแหล่งที่มาเสร็จสมบูรณ์ + แหล่งทีมาของคุณเป็นเวอร์ชันล่าสุดแล้ว + กำลังโหลดแหล่งที่มา กรุณารอสักครู่… ค้นหาแอป - ไม่มีแอปให้ใช้งาน + ไม่มีแอปที่ใช้ได้ เพิ่มแหล่งที่มาของแพตช์หรือเปิดใช้งานแหล่งที่มาที่มีอยู่แล้วใน %s ไม่มีแอปที่ตรงกับ \"%1$s\" + แอปทั้งหมดถูกซ่อน + คุณซ่อนแอปทั้งหมดออกจากหน้าจอหลัก - คุณต้องการแพ็ตช์แอปใด + คุณต้องการแพตช์แอปอะไร? พร้อมหรือยังที่จะทำให้แอปของคุณกลับมาเป็นตำนานอีกครั้ง @@ -53,62 +55,66 @@ สนับสนุนโดยกาแฟหนึ่งแก้วกับแพตซ์ที่มีคุณภาพ - จัดการแหล่งข้อมูล + จัดการแหล่งที่มา เปิดในเบราว์เซอร์ ไม่สามารถเปิด URL ได้ ใช้แพทช์ก่อนปล่อยอย่างทางการ - รับสิทธิ์เข้าถึงเวอร์ชันใหม่ของแพทช์ทดลองก่อนอื่น - ใช้เวอร์ชันทดลองของแอป - แก้ไขเป้าหมายแอปเวอร์ชันทดลองหากมี + รับสิทธิ์เข้าถึงแพทช์ใหม่เวอร์ชันทดลองแต่เนิ่นๆ + ใช้แอปเวอร์ชันทดลอง + แพตซ์แอปเวอร์ชันทดลองของเป้าหมาย ถ้ามี แหล่งที่มานี้ได้ถูกเพิ่มไปแล้ว ชื่อที่แสดง - เปลี่ยนชื่อแหล่งข้อมูล - แหล่งข้อมูลที่มีชื่อนี้อยู่แล้ว + เปลี่ยนชื่อแหล่งที่มา + มีแหล่งที่มาที่ชื่อนี้อยู่แล้ว ไม่สามารถอัปเดตแหล่งที่มาแล้ว เวอร์ชันใด ๆ แพ็คเกจใด ๆ - เพิ่มแหล่งข้อมูล - ลบแหล่งข้อมูล “%s” ? นี้ ขั้นตอนนี้ไม่สามารถยกเลิกได้ + เพิ่มแหล่งที่มา + ลบแหล่งที่มา “%s”? ขั้นตอนนี้ไม่สามารถย้อนกลับได้ รีโมต - URL แหล่งข้อมูล + URL แหล่งที่มา ตัวอย่าง: อุปกรณ์ - เลือกไฟล์แหล่งข้อมูล + เลือกไฟล์แหล่งที่มา เลือกไฟล์ .mpp จากแหล่งข้อมูลอยู่บนที่เก็บข้อมูลภายใน เปลี่ยนไฟล์ ติดตั้งล่วงหน้าแล้ว อัปเดตเสร็จสมบูรณ์ ยังไม่มีการอัปเดต - ล้มข้อมูลแหล่งข้อมูลไม่สำเร็จ: %s + ดาวน์โหลดแหล่งที่มาไม่สำเร็จ: %s การดาวน์โหลด \'%1$s\' ไม่สำเร็จ: %2$s ไม่สามารถอัปเดต \'%1$s\': ไม่พบไฟล์ JSON ของแพทช์ที่ URL ที่กำหนด ผู้เชี่ยวชาญ - เปิดใช้งานโหมดผู้เชี่ยวชาญในการตั้งค่าเพื่อรวมแพตช์นี้ด้วยตนเอง + เปิดใช้งานโหมดผู้เชี่ยวชาญในการตั้งค่าเพื่อเลือกแพตช์นี้ด้วยตนเอง - ต้องการความช่วยเหลือในการค้นหา APK รุ่นต้นฉบับที่ยังไม่ถูกปรับปรุง? - เพื่อปรับปรุง <b>%s</b> คุณต้องมี APK รุ่นต้นฉบับ: - เพื่อปรับปรุง <b>%s</b> คุณต้องมี APK รุ่นต้นฉบับของเวอร์ชัน: + ต้องการความช่วยเหลือในการค้นหา APK เวอร์ชันที่ยังไม่ถูกแก้ไข? + เพื่อที่จะแพตซ์ <b>%s</b> คุณต้องมี APK ที่ยังไม่ถูกแก้ไขเวอร์ชัน: + เพื่อที่จะแพตซ์ <b>%s</b> คุณต้องมี APK ที่ยังไม่ถูกแก้ไขเวอร์ชัน: แนะนำ ไม่ได้แพตซ์ + ไม่รองรับ ใช่, ช่วยฉันค้นหา APK ไม่, ฉันมี APK แล้ว ใช้ APK ที่ถูกบันทึก (เวอร์ชัน %s) - ไม่มี APK ที่ถูกบันทึก. การปรับปรุงจะต้องเลือกไฟล์ APK อีกครั้ง + ไม่มี APK ที่ถูกบันทึก การแพตซ์จะต้องเลือกไฟล์ APK อีกครั้ง + ต้องใช้ Android %1$s+ + ไม่รองรับบนอุปกรณ์นี้ + <b>%1$s</b> ไม่มีเวอร์ชันที่รองรับสำหรับ Android %2$d (API %3$d) ทุกเวอร์ชันที่ประกาศต้องการ Android เวอร์ชันที่สูงกว่า - ดาวน์โหลด APK ดังเดิม + ดาวน์โหลด APK ต้นฉบับ คำแนะนำ: กดปุ่ม \'%1$s\' ด้านล่าง เลื่อนลงมาที่หน้าเว็บแล้วกดปุ่ม กดดาวน์โหลดบนเว็บไซต์ อย่ากดที่นี่ 😊 รอการดาวน์โหลดให้เรียบร้อย และ <b>อย่าไปติดตั้งไฟล์ </b> ไปล่วงหน้าก่อน รอให้การดาวน์โหลดเสร็จสิ้น แล้ว <b>ติดตั้ง APK</b> - หลังจากดาวน์โหลดเสร็จแล้ว คุณจะกลับไปยัง Morphe - หลังจากติดตั้ง APK รeturn to Morphe + หลังจากที่การดาวน์โหลดเสร็จสิ้น ให้กลับมาที่ Morphe + หลังจากติดตั้ง APK ต้นฉบับเสร็จสิ้น กลับมาที่ Morphe ไปที่ APKMirror.com - เลือก APK ที่ถูกดาวน์โหลด - เลือกไฟล์ APK %1$s ที่คุณดาวน์โหลดมาแล้ว + เลือก APK ที่ดาวน์โหลด + เลือกไฟล์ APK <b>%1$s</b> ที่คุณดาวน์โหลด เปิดไฟล์ APK เวอร์ชันแนะนำ เลือกเวอร์ชัน @@ -221,6 +227,7 @@ มีแพตซ์เวอร์ชันใหม่พร้อมใช้งานแล้ว ให้แพตซ์อีกครั้งเพื่อรับการปรับปรุงและการแก้ไขสำหรับเวอร์ชันล่าสุด แอปถูกลบออก แอปนี้ถูกลบออกนอก Morphe. แพตซ์อีกครั้งเพื่อฟื้นฟูฟังก์ชัน + ขนาด APK โหมดรากรูตต้องมี APK รุ่นต้นฉบับติดตั้งบนอุปกรณ์ของคุณก่อนปรับแก้ การรองรับ GmsCore ถูกยกเว้นในโหมด Root @@ -308,16 +315,13 @@ ไม่สามารถใช้ %s ได้ บันทึกแอปที่แก้ไขแล้วสำหรับภายหลัง ไม่สามารถบันทึกแอปที่แก้ไขได้ - ดาวน์โหลดไฟล์ APK ต้องมีการติดต่อผู้ใช้เพื่อดำเนินการต่อไปนี้ กระบวนการแพทช์ออกไปแล้วด้วยรหัส %1$s ติดตั้งสำเร็จแล้ว ไม่สามารถติดตั้งแอปได้: %s - การติดตั้งไม่สำเร็จ. ตรวจสอบการติดตั้งของตัวติดตั้งระบบและลองอีกครั้ง บันทึก APK สำเร็จ ส่งออกแอปที่แก้ไขแล้วไม่สำเร็จ ลบแอปที่บันทึกไว้แล้ว - ลบคู่มือที่บันทึกไว้ ติดตั้งแอปพลิเคชันก่อนเปิดใช้งาน ชื่อแพ็กเกจ ไม่สามารถติดตั้งใช้งานได้: %s @@ -337,6 +341,10 @@ ตาราง อนุภาค ธง + สุ่ม + เมื่อเปิดแอป + รายวัน + ทุกๆ 3 วัน เอฟเฟ็กต์พารัลแล็กซ์ การเปลี่ยนแปลงพื้นหลังที่เรียบร้อยเมื่อลดทอนอุปกรณ์ @@ -353,6 +361,10 @@ ภาษาที่ใช้อยู่ ข้อมูลภาษาบางค์บางค์อาจไม่ครบถูกต้องหรือไม่ครบ เพื่อนำให้มีภาษาใหม่หรือปรับปรุงภาษาที่มีอยู่แล้ว ให้ไปที่ %s + + หน้าจอหลัก + วลีทักทาย + แสดงข้อความทักทายบนหน้าจอหลัก ไอคอนแอป ค่าเริ่มต้น @@ -399,8 +411,17 @@ เปิดใช้งานการปรับแต่งตัวเลือกและการตั้งค่าความละเอียดสูงสำหรับการปรับปรุง Morphe เปิดใช้งานโหมดเชี่ยวชาญ? โหมดเชี่ยวชาญให้ควบคุมการนำเข้าแปลงมากขึ้น แต่การตั้งค่าค่าไม่ถูกต้องในโหมดเชี่ยวชาญอาจทำให้แอปพลิเคชันไม่สามารถใช้งานได้ - ลบเนทิฟไลบรารีที่ไม่ได้ใช้ - ลบเนทิฟไลบรารีที่ไม่รองรับสถาปัตยกรรมของ CPU บนอุปกรณ์ของคุณ จากแอปที่ถูกแพตซ์ + ปรับประสิทธิภาพสำหรับสถาปัตยกรรมของอุปกรณ์ + "ข้ามโมดูล APK แบบแยกส่วนสำหรับสถาปัตยกรรม, ภาษา และความหนาแน่นของหน้าจอที่ไม่รองรับในระหว่างการรวม +สำหรับ APK แบบปกติ ให้ลบไลบรารีเนทีฟสำหรับสถาปัตยกรรมที่ไม่รองรับหลังจากแก้ไข" + + โหมดการประมวลผลไบต์โค้ด + ควบคุมวิธีการประมวลผลไบต์โค้ดในระหว่างการแก้ไขแพตช์ มีผลต่อความเร็วในการแก้ไข การใช้หน่วยความจำ และขนาด APK ที่ได้ + รวดเร็ว (แนะนำ) + เร็ว + การปรับปรุงแพตช์ที่เร็วขึ้นและการใช้หน่วยความจำน้อยลง แต่มีขนาด APK ที่ใหญ่ขึ้น แนะนำสำหรับอุปกรณ์ที่มี RAM ต่ำหรือเก่า + เต็ม + การปรับแพตช์ที่ช้าลง ทำซ้ำพฤติกรรมเดิม ใช้เฉพาะเมื่อโหมดที่เร็วทำให้เกิดปัญหา โทเคน GitHub PAT จำเป็นสำหรับแหล่งขอร้องข้อมูล @@ -510,13 +531,7 @@ morphe_header_custom_dark.png รุ่น Shizuku ที่ติดตั้งไม่ได้รับการรองรับ เปิด Shizuku การติดตั้งล้มเหลว. ลองอีกครั้งหรือเปลี่ยนไปใช้ตัวติดตั้งอื่น - การติดตั้งถูกบล็อกโดย Android. ตรวจสอบ Play Protect หรือการตั้งค่าความปลอดภัย มีแอปที่มีชื่อแพ็คเกจเดียวกันติดตั้งอยู่แล้ว. ลบแอปนั้นก่อนดำเนินการต่อไป - APK ไม่เข้ากับอุปกรณ์หรือเวอร์ชัน Android นี้ - APK ไม่ถูกต้องหรือเสียหาย - มีพื้นที่เก็บข้อมูลไม่เพียงพอสำหรับการติดตั้ง - การติดตั้งหมดเวลา. ลองอีกครั้ง - การติดตั้งถูกยกเลิก กำลังเปิด %1$s… %1$s ไม่ยืนยันการติดตั้ง. ตรวจสอบแอปอื่นและลองอีกครั้ง %1$s รายงานการติดตั้งสำเร็จ @@ -547,6 +562,7 @@ morphe_header_custom_dark.png รหัส รหัสไฟล์ตัวเลือกไม่ถูกต้อง นำเข้า + รูปแบบคีย์สโตร์ ล้มการนำเข้าไฟล์ตัวเลือก ส่งออกไฟล์ตัวเลือก ส่งออกไฟล์ตัวเลือก @@ -554,6 +570,7 @@ morphe_header_custom_dark.png ส่งออกไฟล์ตัวเลือกสำเร็จ แสดงรหัส ซ่อนรหัส + ไม่สามารถส่งออกคีย์สโตร์ได้ นำเข้าและส่งออกการตั้งค่า นำเข้าและส่งออกการตั้งค่า @@ -683,7 +700,8 @@ morphe_header_custom_dark.png อนุญาต รายละเอียด ซ่อน - ค่าเริ่มต้น + ซ่อน + แสดง บันทึก แหล่งที่มา กำลังติดตั้ง… diff --git a/app/src/main/res/values-tr-rTR/plurals.xml b/app/src/main/res/values-tr-rTR/plurals.xml index a7e614032..1f954219b 100644 --- a/app/src/main/res/values-tr-rTR/plurals.xml +++ b/app/src/main/res/values-tr-rTR/plurals.xml @@ -17,8 +17,8 @@ %s paket - %s yama uygula - %s yama uygula + %s yama uygulandı + %s yama uygulandı %s APK dosyası @@ -36,4 +36,12 @@ %s yama seçildi %s yama seçildi + + %s uygulamayı göster + %s uygulamayı göster + + + %s gizli uygulama + %s gizli uygulama + diff --git a/app/src/main/res/values-tr-rTR/strings.xml b/app/src/main/res/values-tr-rTR/strings.xml index 6b2667ecd..f7d865135 100644 --- a/app/src/main/res/values-tr-rTR/strings.xml +++ b/app/src/main/res/values-tr-rTR/strings.xml @@ -32,6 +32,8 @@ Uygulama bulunamadı Bir yama kaynağı ekleyin veya %s içinde mevcut olanı etkinleştirin \"%1$s\" ile eşleşen uygulama bulunamadı + Tüm uygulamalar gizlendi + Ana ekrandaki tüm uygulamaları gizlediniz Hangi uygulamayı yamalamak istiyorsunuz? @@ -91,10 +93,14 @@ <b>%s</b>\'i yamalamak için bu yamalanmamış APK sürümlerinden birine ihtiyacınız var: Önerilen Yamalanmamış + Uyumsuz Evet, APK bulmama yardım et Hayır, bende APK var Kaydedilmiş APK (v%s) kullan Kayıtlı APK yok. Yamayı uygulayabilmek için tekrar APK dosyasını seçmeniz gerekiyor + Android %1$s+ gerektiriyor + Bu cihazda desteklenmiyor + <b>%1$s</b> Android %2$d (API %3$d) için desteklenen sürümlere sahip değil. Bildirilen tüm sürümler daha yüksek bir Android sürümü gerektiriyor Orijinal APK\'i indir Talimatlar: @@ -128,7 +134,7 @@ Yama bulunamadı Evrensel yamalar Yeni - Çoklu yama kaynağı seçildi + Birden fazla yama kaynağı seçildi "Birden fazla yama kaynağından yama seçtiniz. Bu, uyumluluk sorunlarına veya beklenmedik davranışlara neden olabilir. Devam etmek istediğinizden emin misiniz?" @@ -221,6 +227,7 @@ En iyi sonuçlar için bu uygulama, <b>tam APK</b> dosyasını yamal Daha yeni sürüm yamalar mevcut. En son iyileştirmeleri ve düzeltmeleri almak için uygulamayı yeniden yamalayın Uygulama kaldırıldı Bu uygulama Morphe dışından kaldırıldı. İşlevselliği geri getirmek için yeniden yamalayın + APK boyutu Root modu, yamalama yapmadan önce orijinal APK\'in cihazınızda yüklü olmasını gerektirir GmsCore destek yaması root modunda hariç tutulmuştur @@ -255,7 +262,7 @@ En iyi sonuçlar için bu uygulama, <b>tam APK</b> dosyasını yamal Bir veya daha fazla yama seçeneği, Morphe\'nin okuyamadığı yollara işaret ediyor. Depolama erişimini verin ve düğmeye dokunun, ardından yamalama otomatik olarak başlayacaktır Bir veya daha fazla yama seçeneği, erişilemeyen yollara işaret ediyor. Yama seçeneklerindeki yolları güncelleyin ve tekrar deneyin Depolama izni verin - Depolama izni reddedildi. Bu yolu kullanmak için izni verin veya dosyaları uygulamaların özel dizinine taşıyın + Depolama izni reddedildi. Bu yolu kullanmak için izni verin veya dosyaları uygulamanın özel dizinine taşıyın Depolama ayarlarını aç İzin reddedildi Bulunamadı @@ -270,9 +277,9 @@ En iyi sonuçlar için bu uygulama, <b>tam APK</b> dosyasını yamal Yamalanmış APK dosyası yazılıyor Yamalanmış APK dosyası imzalanıyor - Yama işlemi devam ediyor… - Yama işlemcisine dönmek için dokunun - Yama işlemcisini durdur + Yamalama devam ediyor… + Patcher\'a dönmek için dokunun + Patcher\'ı durdur Yama işlemini durdurmak istediğinizden emin misiniz? Uygulamanızı harika bir şeye dönüştürüyoruz… 🏆 @@ -287,7 +294,7 @@ En iyi sonuçlar için bu uygulama, <b>tam APK</b> dosyasını yamal Uygulamadaki hataları düzeltiyoruz… 🪲 Harika yamalar beklemeye değer… ⏳ Pikselleri parlayana kadar cilalıyoruz… 💎 - Lütfen pilot \'Yamalama işlemi devam ediyor\' ışığını söndürene kadar yerinizde kalın... 💺✈️ + Lütfen pilot \'Yamalama devam ediyor\' ışığını söndürene kadar yerinizde kalın... 💺✈️ Yamalanmış uygulamanız pişiyor, sıcak ve taze… 🍳 Bytecode ile nazikçe pazarlık yapıyoruz... 🤝 @@ -308,16 +315,13 @@ En iyi sonuçlar için bu uygulama, <b>tam APK</b> dosyasını yamal %s uygulanamadı Yamalanmış uygulama daha sonrası için kaydedildi Yamalanmış uygulama kaydedilemedi - APK dosyasını indir Bu eklentinin çalışmaya devam edebilmesi için kullanıcı etkileşimi gereklidir - Yamalama işlemi %1$s kodu ile sonlandı + Patcher işlemi %1$s kodu ile sonlandı Başarıyla yüklendi Uygulama yüklenemedi: %s - Yükleme tamamlanmadı. Sistem yükleyici iletişim kutusunu kontrol edin ve tekrar deneyin APK Kaydedildi Yamalanmış uygulama dışa aktarılamadı Kayıtlı yamalanmış uygulama kaldırıldı - Kaydedilen kopya kaldırıldı Uygulamayı açmadan önce yükleyin Paket adı Bağlanamadı: %s @@ -337,6 +341,10 @@ En iyi sonuçlar için bu uygulama, <b>tam APK</b> dosyasını yamal Izgara Parçacıklar Yok + Rastgele + Başlangıçta + Günlük + Her 3 günde bir Paralaks etkisi Cihaz eğildiğinde arka planın yumuşak bir şekilde kayması @@ -353,6 +361,10 @@ En iyi sonuçlar için bu uygulama, <b>tam APK</b> dosyasını yamal Mevcut dil Çeviriler bazı diller için eksik veya tamamlanmamış olabilir Yeni diller eklemek veya mevcut çevirileri iyileştirmek için %s adresini ziyaret edin + + Ana ekran + Selamlama cümleleri + Ana ekranda bir selamlama mesajı göster Uygulama simgesi Varsayılan @@ -399,8 +411,17 @@ En iyi sonuçlar için bu uygulama, <b>tam APK</b> dosyasını yamal Gelişmiş Morphe yama ayarlarını ve özelleştirme seçeneklerini etkinleştirin Uzman modu etkinleştirilsin mi? Uzman modu yamaların nasıl uygulanacağı konusunda daha fazla kontrol sağlar, ancak uzman modunda ayarların yanlış yapılandırılması işlevsiz bir uygulamayla sonuçlanabilir - Kullanılmayan yerel kütüphaneleri kaldır - APK\'yi yamalarken desteklenmeyen CPU mimarileri için kullanılan yerel kütüphaneleri silin + Cihaz mimarisi için optimize et + "Birleştirme sırasında desteklenmeyen CPU mimarileri, diller ve ekran yoğunluklarının bölünmüş APK modüllerini atlar. +Basit APK'ler için, yamalamadan sonra desteklenmeyen mimarilere ait yerel kütüphaneleri temizler" + + Bayt kodu işleme modu + Yamalama sırasında bayt kodunun nasıl işleneceğini kontrol eder. Yama hızını, bellek kullanımını ve çıktı APK boyutunu etkiler + Hızlı (Önerilen) + Hızlı + Daha büyük bir APK pahasına daha hızlı yamalama ve daha düşük bellek kullanımı. Düşük RAM\'li veya eski cihazlar için önerilir + Tam + Daha yavaş yamalama, eski davranışı tekrarlar. Yalnızca hızlı mod sorunlara neden olursa kullanın GitHub PAT Çekme isteği kaynakları için gereklidir @@ -425,7 +446,7 @@ En iyi sonuçlar için bu uygulama, <b>tam APK</b> dosyasını yamal Yama seçenekleri yükleniyor... Yama kaynağı güncellenene kadar bekleyin Yama seçenekleri yüklenemedi - Bu yamaya ait seçenekler bulunmuyor + Bu yamaya ait hiçbir seçenek yok Özel marka Görüntülenen uygulama adını değiştir @@ -510,13 +531,7 @@ Resim boyutları şu şekilde olmalıdır: Yüklü Shizuku sürümü desteklenmiyor Shizuku\'yu Aç Yükleme başarısız oldu. Tekrar deneyin veya farklı bir yükleyiciye geçin - Yükleme Android tarafından engellendi. Play Protect veya güvenlik ayarlarını kontrol edin Bu paket adıyla başka bir uygulama zaten yüklü. Devam etmeden önce onu kaldırın - APK, bu cihaz veya Android sürümüyle uyumlu değil - APK, geçersiz veya bozuk - Yükleme için yeterli depolama alanı yok - Yükleme zaman aşımına uğradı. Tekrar deneyin - Yükleme iptal edildi %1$s açılıyor… %1$s yüklemeyi onaylamadı. Diğer uygulamayı kontrol edin ve tekrar deneyin %1$s başarılı bir şekilde yüklediğini bildirdi @@ -547,6 +562,7 @@ Resim boyutları şu şekilde olmalıdır: Kullanıcı Adı (Takma Ad) Parola İçe aktar + Anahtar deposu formatı Yanlış anahtar deposu kimlik bilgileri Anahtar deposu içe aktarıldı Anahtar deposu içe aktarılamadı @@ -554,6 +570,7 @@ Resim boyutları şu şekilde olmalıdır: Geçerli anahtar deposunu dışa aktar Dışa aktarılacak anahtar deposu yok Anahtar deposu dışa aktarıldı + Anahtar deposu dışa aktarılamadı Parolayı göster Parolayı gizle @@ -682,7 +699,8 @@ Resim boyutları şu şekilde olmalıdır: İzin ver Detaylar Gizle - Varsayılan + Gizli + Gizliliği kaldır Günlükler Kaynaklar Yükleniyor… diff --git a/app/src/main/res/values-uk-rUA/plurals.xml b/app/src/main/res/values-uk-rUA/plurals.xml index 64738da7a..fe5c6f7f7 100644 --- a/app/src/main/res/values-uk-rUA/plurals.xml +++ b/app/src/main/res/values-uk-rUA/plurals.xml @@ -25,10 +25,10 @@ %s пакетів - Виконати %s патч - Виконати %s патча - Виконати %s патчів - Виконати %s патчів + Застосовано %s патч + Застосовано %s патчі + Застосовано %s патчів + Застосовано %s патчів %s APK файл @@ -54,4 +54,16 @@ %s патчів вибрано %s патчів вибрано + + Показати %s застосунок + Показати %s застосунки + Показати %s застосунків + Показати %s застосунків + + + %s прихований застосунок + %s приховані застосунки + %s прихованих застосунків + %s прихованих застосунків + diff --git a/app/src/main/res/values-uk-rUA/strings.xml b/app/src/main/res/values-uk-rUA/strings.xml index 4ae93a04d..e0113a177 100644 --- a/app/src/main/res/values-uk-rUA/strings.xml +++ b/app/src/main/res/values-uk-rUA/strings.xml @@ -32,6 +32,8 @@ Немає доступних застосунків Додайте джерело патчів або ввімкніть існуюче в %s Не знайдено застосунків за запитом \"%1$s\" + Усі застосунки приховані + Ви приховали всі застосунки з головного екрану Який застосунок ви хочете пропатчити? @@ -91,10 +93,14 @@ Щоб патчити <b>%s</b>, вам потрібні непатчені APK файли з версіями: Рекомендована Непатчена + Несумісна Так, допоможіть мені знайти APK Ні, я вже маю APK Збережений APK (v%s) Немає збереженого APK файлу. Для патчінгу потрібно буде знову вибрати APK файл + Потрібен Android %1$s+ + Не підтримується на цьому пристрої + <b>%1$s</b> не має підтримуваних версій для Android %2$d (API %3$d). Усі заявлені версії вимагають вищу версію Android Завантажити оригінальний APK файл Інструкції: @@ -221,6 +227,7 @@ Доступна новіша версія патчів. Перепатчіть застосунок, щоб отримати останні покращення та виправлення Застосунок було видалено Це застосунок було видалено поза менеджером Morphe. Перепатчіть його, щоб відновити функціональність + Розмір APK Режим root вимагає, щоб оригінальний APK файл був встановлений на вашому пристрої перед патчінгом Патч підтримки GmsCore виключено у режимі root @@ -308,16 +315,13 @@ Не вдалося застосувати %s Патчений застосунок збережено для подальшого використання Не вдалося зберегти патчений застосунок - Завантажити APK файл Для продовження потрібна взаємодія користувача з цим плагіном Процес патчера завершився з кодом %1$s Встановлено успішно Неможливо встановити застосунок: %s - Встановлення не завершено. Перевірте діалог системного інсталятора та спробуйте ще раз APK файл збережено Не вдалося експортувати патчений застосунок Видалено збережений патчений застосунок - Видалено збережену копію Встановіть застосунок, перш ніж відкрити його Назва пакета Не вдалося змонтувати: %s @@ -337,6 +341,10 @@ Сітка Частинки Немає + Випадково + При запуску + Щодня + Кожні 3 дні Ефект паралакса Плавне зміщення фону при нахилі пристрою @@ -353,6 +361,10 @@ Поточна мова Переклади деяких мов можуть бути відсутніми або неповними Щоб перекласти нові мови або покращити наявні переклади, відвідайте %s + + Головний екран + Вітальні фрази + Показувати вітальне повідомлення на головному екрані Іконка застосунку Стандартна @@ -399,8 +411,17 @@ Увімкнути розширенні налаштування патчінгу Morphe та параметри кастомізації Увімкнути режим експерта? Експертний режим надає більше контролю над застосуванням патчів, але неправильне налаштування параметрів у експертному режимі може призвести до непрацездатності застосунку - Видалити невикористані нативні бібліотеки - Видаляти нативні бібліотеки для непідтримуваних архітектур процесора з патчених застосунків + Оптимізація для архітектури пристрою + "Пропускати split модулі APK для непідтримуваних архітектур процесора, локалізацій та щільностей екрана під час об'єднання. +Для звичайних APK видаляти нативні бібліотеки для непідтримуваних архітектур після патчінгу" + + Режим обробки байт-коду + Керує процесом обробки байт-коду під час встановлення патчів. Впливає на швидкість встановлення патчів, використання пам\'яті та розмір вихідного APK файлу + Швидкий (Рекомендовано) + Швидкий + Швидший патчінг та менше використання пам\'яті, але розмір APK файлу буде більшим. Рекомендовано для пристроїв з малим обсягом оперативної пам\'яті або старих пристроїв + Повний + Повільніший патчінг, відтворює застарілу поведінку. Використовуйте лише якщо швидкий режим викликає проблеми GitHub PAT Потрібно для використання pull request як джерела @@ -487,7 +508,7 @@ morphe_header_custom_dark.png Приховати функціонал Shorts Приховати ярлик Shorts - Прибрати Shorts з меню довгого натискання у лаунчері + Прибрати Shorts з меню тривалого натискання у лаунчері Приховати віджет Shorts Прибрати кнопку Shorts з віджета лаунчера @@ -510,13 +531,7 @@ morphe_header_custom_dark.png Версія встановленого Shizuku не підтримується Відкрити Shizuku Не вдалося встановити. Спробуйте ще раз або перемкніть на інший інсталятор - Інсталяція була заблокована Android. Перевірте Play Protect чи налаштування безпеки Інший застосунок із цим іменем пакета вже встановлений. Видаліть його, перш ніж продовжити - APK файл не сумісний з цим пристроєм або версією Android - APK файл недійсний або пошкоджений - Недостатньо місця для встановлення - Встановлення зайняло багато часу. Спробуйте ще раз - Встановлення скасовано Відкривається %1$s… %1$s не підтвердив встановлення. Перевірте інший застосунок і спробуйте знову %1$s повідомив про успішне встановлення @@ -547,6 +562,7 @@ morphe_header_custom_dark.png Ім\'я користувача (псевдонім) Пароль Імпортувати + Формат сховища ключів Невірні облікові дані сховища ключів Сховище ключів імпортовано Не вдалося імпортувати сховище ключів @@ -554,6 +570,7 @@ morphe_header_custom_dark.png Експортувати поточне сховище ключів Немає доступного сховища ключів для експорту Сховище ключів експортовано + Не вдалося експортувати сховище ключів Показати пароль Приховати пароль @@ -682,7 +699,8 @@ morphe_header_custom_dark.png Дозволити Деталі Приховати - За замовчуванням + Приховані + Показати Журнали Джерела Інсталяція… diff --git a/app/src/main/res/values-ur-rIN/strings.xml b/app/src/main/res/values-ur-rIN/strings.xml index 180ea1689..48f97ef60 100644 --- a/app/src/main/res/values-ur-rIN/strings.xml +++ b/app/src/main/res/values-ur-rIN/strings.xml @@ -39,10 +39,12 @@ + + diff --git a/app/src/main/res/values-uz-rUZ/strings.xml b/app/src/main/res/values-uz-rUZ/strings.xml index 180ea1689..48f97ef60 100644 --- a/app/src/main/res/values-uz-rUZ/strings.xml +++ b/app/src/main/res/values-uz-rUZ/strings.xml @@ -39,10 +39,12 @@ + + diff --git a/app/src/main/res/values-vi-rVN/plurals.xml b/app/src/main/res/values-vi-rVN/plurals.xml index f3350c7aa..434abe7a0 100644 --- a/app/src/main/res/values-vi-rVN/plurals.xml +++ b/app/src/main/res/values-vi-rVN/plurals.xml @@ -27,4 +27,10 @@ Đã chọn %s bản vá + + Hiện %s ứng dụng + + + %s ứng dụng đang ẩn + diff --git a/app/src/main/res/values-vi-rVN/strings.xml b/app/src/main/res/values-vi-rVN/strings.xml index a2e7d90a4..47926539f 100644 --- a/app/src/main/res/values-vi-rVN/strings.xml +++ b/app/src/main/res/values-vi-rVN/strings.xml @@ -5,12 +5,12 @@ Chưa vá Không có bản vá nào Ẩn ứng dụng này? - Ứng dụng này sẽ bị ẩn khỏi màn hình chính. Bạn có thể khôi phục lại bằng nút \'%1$s\' ở cuối danh sách + Ứng dụng này sẽ bị ẩn khỏi trang chủ. Bạn có thể khôi phục lại bằng nút \"%1$s\" ở cuối danh sách Ứng dụng này sẽ bị ẩn Hiện ứng dụng đã ẩn Không có ứng dụng nào đang ẩn Ứng dụng đã ẩn - Nhấn để hiện lại + Bấm để hiện lại Lỗi trên Android 11 Vui lòng cấp quyền cài đặt ứng dụng không rõ nguồn gốc để tránh lỗi trên Android 11 có thể ảnh hưởng đến trải nghiệm Thêm nguồn @@ -19,7 +19,7 @@ Đã có bản cập nhật mới của Morphe Phiên bản mới đã sẵn sàng để được cài đặt - Cập nhật thất bại + Không thể cập nhật Kiểm tra kết nối mạng và thử lại Đã bỏ qua bản cập nhật Đang bật dữ liệu di động @@ -32,6 +32,8 @@ Không có ứng dụng nào có sẵn Thêm nguồn vá mới hoặc bật nguồn có sẵn trong %s Không có ứng dụng nào khớp với \"%1$s\" + Tất cả ứng dụng đã bị ẩn + Bạn đã ẩn tất cả ứng dụng khỏi trang chủ Bạn muốn vá ứng dụng nào? @@ -82,7 +84,7 @@ Hiện không có bản cập nhật nào Không thể tải nguồn vá: %s Không tải được \"%1$s\": %2$s - Không thể cập nhật \'%1$s\': không tìm thấy tệp JSON bản vá tại URL đã cấu hình + Không thể cập nhật \"%1$s\": không tìm thấy tệp JSON bản vá tại URL đã cấu hình Chuyên gia Bật chế độ Chuyên gia trong Cài đặt để có thể tự thêm bản vá này @@ -91,14 +93,18 @@ Để vá <b>%s</b>, bạn cần APK chưa vá thuộc một trong các phiên bản: Khuyên dùng Chưa vá - Có, giúp tôi với + Không tương thích + Có, giúp tôi vs Không, tôi có APK rồi Dùng APK đã lưu (v%s) Chưa lưu APK nào. Bạn sẽ phải chọn lại APK khi vá + Yêu cầu Android %1$s+ + Không hỗ trợ trên thiết bị này + <b>%1$s</b> không có phiên bản nào hỗ trợ cho Android %2$d (API %3$d). Tất cả các phiên bản đã khai báo đều yêu cầu phiên bản Android cao hơn Tải APK gốc Hướng dẫn: - Bấm nút \'%1$s\' ở bên dưới + Bấm nút \"%1$s\" ở bên dưới Cuộn xuống trang web và bấm Bấm tải xuống trên trang web ấy, chứ không phải ở đây 😊 Đợi tải xong, nhưng <b>không cài</b> APK @@ -114,7 +120,7 @@ Phiên bản đã chọn Chọn APK Bấm nút bên dưới để chọn tệp APK của bất kỳ ứng dụng nào để vá - Không thể đọc tệp. Hãy kiểm tra xem \'Bộ nhớ ngoài\' có bị hạn chế pin hay không + Không thể đọc tệp. Hãy kiểm tra xem \"Bộ nhớ ngoài\" có bị hạn chế pin hay không Tệp đã chọn không phải là tệp APK hợp lệ Không thể mở tệp. Vui lòng thử lại @@ -145,7 +151,7 @@ Tốt nhất bạn nên chuyển sang <b>APK đầy đủ</b> để Thử nghiệm Phiên bản này vẫn đang thử nghiệm và có thể không ổn định hoặc chưa hoàn thiện Muốn thử không? 🧪 - Phiên bản <b>%s</b> này hiện mới được hỗ trợ thử nghiệm<br/><br/>🔧 Ứng dụng có thể hoạt động chưa ổn định hoặc còn lỗi trong quá trình hoàn thiện bản vá + Phiên bản <b>%s</b> hiện mới được hỗ trợ thử nghiệm<br/><br/>🔧 Ứng dụng có thể hoạt động chưa ổn định hoặc còn lỗi trong quá trình hoàn thiện bản vá Phiên bản không hỗ trợ Phiên bản APK này không được đề xuất. Tiếp tục có thể dẫn đến ứng dụng không hoạt động Vẫn tiếp tục @@ -179,15 +185,15 @@ Tốt nhất bạn nên chuyển sang <b>APK đầy đủ</b> để Chọn ảnh tiền cảnh, chọn màu nền, sau đó kéo thanh trượt hoặc chụm hai ngón để xem trước biểu tượng Xem trước biểu tượng thích ứng Để biểu tượng trông đẹp hơn, hãy giữ nó trong vòng tròn nét liền nhé - Chọn hình ảnh - Đổi hình ảnh + Chọn ảnh + Đổi ảnh Màu nền Đặt lại vị trí và tỷ lệ Vùng an toàn (luôn hiển thị) Vùng mặt nạ (có thể bị cắt) Đã tạo biểu tượng thích ứng thành công Không thể tạo biểu tượng thích ứng - Chọn vị trí để tạo thư mục \'%1$s/%2$s\' chứa các tệp biểu tượng đã tạo + Chọn vị trí để tạo thư mục \"%1$s/%2$s\" chứa các tệp biểu tượng đã tạo Xem trước biểu tượng trên thông báo Dùng thanh trượt để thay đổi kích thước. Kéo để điều chỉnh vị trí @@ -198,7 +204,7 @@ Tốt nhất bạn nên chuyển sang <b>APK đầy đủ</b> để Không có hình ảnh nào được chọn Đã tạo logo thành công Không thể tạo logo - Chọn vị trí để tạo thư mục \'%1$s/%2$s\' chứa các tệp logo đã tạo + Chọn vị trí để tạo thư mục \"%1$s/%2$s\" chứa các tệp logo đã tạo Bạn có chắc muốn gỡ ứng dụng này không? Thao tác này sẽ xóa vĩnh viễn: @@ -221,9 +227,10 @@ Tốt nhất bạn nên chuyển sang <b>APK đầy đủ</b> để Đã có bản vá mới. Hãy vá lại ứng dụng để áp dụng các cải tiến và bản sửa lỗi mới nhất Ứng dụng đã bị gỡ cài đặt Ứng dụng này đã bị gỡ bên ngoài Morphe. Hãy vá lại để khôi phục chức năng + Kích thước APK Chế độ root yêu cầu APK gốc phải được cài đặt trên thiết bị trước khi vá - Chế độ root không cần bản vá \'GmsCore support\' + Chế độ root không cần bản vá \"GmsCore support\" Chọn phương thức cài đặt Thiết bị của bạn đã có quyền root. Hãy chọn cách bạn muốn cài đặt ứng dụng đã vá Gắn kết bằng root @@ -271,7 +278,7 @@ Tốt nhất bạn nên chuyển sang <b>APK đầy đủ</b> để Đang ký APK đã vá Đang vá… - Chạm để quay trở lại trình vá + Bấm để quay trở lại trình vá Dừng vá Bạn có chắc chắn muốn dừng tiến trình vá không? @@ -308,16 +315,13 @@ Tốt nhất bạn nên chuyển sang <b>APK đầy đủ</b> để Không thể áp dụng %s Đã lưu ứng dụng đã vá Không thể lưu ứng dụng đã vá - Tải tệp APK Bạn vẫn cần tự thao tác để tải ứng dụng Tiến trình vá đã dừng với mã lỗi %1$s Đã cài đặt thành công Không thể cài đặt ứng dụng: %s - Cài đặt không thành công. Hãy kiểm tra hộp thoại cài đặt của hệ thống và thử lại Đã lưu APK Không thể xuất ứng dụng đã vá Đã xoá bản lưu ứng dụng đã vá - Đã xoá bản sao đã lưu Cài ứng dụng trước khi mở Tên gói Không thể gắn: %s @@ -337,6 +341,10 @@ Tốt nhất bạn nên chuyển sang <b>APK đầy đủ</b> để Lưới ô Hạt Không + Ngẫu nhiên + Khi khởi động + Hàng ngày + Cứ 3 ngày một lần Hiệu ứng thị sai Nền chuyển động nhẹ nhàng khi nghiêng thiết bị @@ -353,6 +361,10 @@ Tốt nhất bạn nên chuyển sang <b>APK đầy đủ</b> để Ngôn ngữ hiện tại Một số ngôn ngữ có thể bị thiếu hoặc chưa hoàn thiện Bạn có thể yêu cầu thêm ngôn ngữ hoặc cải thiện bản dịch hiện tại bằng cách truy cập %s + + Trang chủ + Thông điệp + Hiện thông điệp trên trang chủ Biểu tượng Mặc định @@ -362,7 +374,7 @@ Tốt nhất bạn nên chuyển sang <b>APK đầy đủ</b> để Hư không Màu chàm Đổi biểu tượng? - Bạn có muốn đổi sang \'%1$s\' và khởi động lại? + Bạn có muốn đổi sang \"%1$s\" và khởi động lại? Chốt Cập nhật @@ -375,10 +387,10 @@ Tốt nhất bạn nên chuyển sang <b>APK đầy đủ</b> để Nhận thông báo ngay khi có phiên bản mới, kể cả khi ứng dụng đang tắt Tần suất kiểm tra Kiểm tra cập nhật - Mỗi giờ - Mỗi ngày - Mỗi tuần - Mỗi tháng + 1 giờ/lần + 1 ngày/lần + 1 tuần/lần + 1 tháng/lần Tần suất kiểm tra trong nền Tính năng này có thể không hoạt động trên một số thiết bị như Xiaomi, Huawei, Samsung và OnePlus, vì chúng thường giới hạn ứng dụng chạy nền để tiết kiệm pin @@ -399,8 +411,17 @@ Tốt nhất bạn nên chuyển sang <b>APK đầy đủ</b> để Mở khoá các tuỳ chỉnh vá nâng cao của Morphe Bật chế độ Chuyên gia? Chế độ chuyên gia cho phép bạn tùy chỉnh sâu hơn về cách các bản vá được áp dụng, nhưng nếu bạn chỉnh không đúng có thể khiến ứng dụng không hoạt động - Xóa thư viện gốc không cần - Xóa thư viện gốc cho kiến trúc CPU không được hỗ trợ từ ứng dụng đã vá + Tối ưu theo kiến trúc thiết bị + "Khi gộp, sẽ bỏ qua các mô-đun APK chia nhỏ không phù hợp với kiến trúc CPU, ngôn ngữ hoặc mật độ màn hình. +Với APK thường, các thư viện gốc của kiến trúc không được hỗ trợ sẽ bị loại bỏ sau khi vá." + + Chế độ xử lý bytecode + Quy định cách xử lý bytecode trong quá trình vá. Ảnh hưởng đến tốc độ vá, mức dùng bộ nhớ và kích thước APK đầu ra + Nhanh (khuyên dùng) + Nhanh + Vá nhanh hơn và dùng ít bộ nhớ hơn, đổi lại APK sẽ lớn hơn. Khuyến nghị cho thiết bị RAM thấp hoặc đời cũ + Đầy đủ + Vá chậm hơn, mô phỏng lại cơ chế cũ. Chỉ dùng nếu chế độ nhanh bị lỗi Mã PAT GitHub Bắt buộc đối với các nguồn pull request @@ -511,13 +532,7 @@ Kích thước hình ảnh yêu cầu: Phiên bản Shizuku đã cài không được hỗ trợ Mở Shizuku Không thể cài đặt. Thử lại hoặc đổi sang trình cài đặt khác - Cài đặt bị chặn bởi Android. Kiểm tra Play Protect hoặc cài đặt bảo mật Đã cài đặt ứng dụng khác với cùng tên gói. Hãy gỡ cài ứng dụng đó trước khi tiếp tục - APK không tương thích với thiết bị hoặc phiên bản Android - APK không hợp lệ hoặc bị hỏng - Không đủ bộ nhớ trống để cài đặt - Quá thời gian chờ cài đặt. Hãy thử lại - Đã hủy cài đặt Đang mở %1$s… %1$s không xác nhận cài đặt. Kiểm tra ứng dụng khác và thử lại %1$s báo đã cài đặt thành công @@ -548,6 +563,7 @@ Kích thước hình ảnh yêu cầu: Tên người dùng (Alias) Mật khẩu Nhập + Định dạng Keystore Thông tin xác thực kho khóa không đúng Đã nhập kho khóa Khổng thể nhập kho khóa @@ -555,6 +571,7 @@ Kích thước hình ảnh yêu cầu: Xuất kho khoá hiện tại Không có kho khóa nào để xuất Đã xuất kho khóa + Không thể xuất kho khóa Hiện mật khẩu Ẩn mật khẩu @@ -622,7 +639,7 @@ Kích thước hình ảnh yêu cầu: Trước đây Giấy phép mã nguồn mở - Bản vá + Số bản vá Trang chủ Cài đặt @@ -657,9 +674,9 @@ Kích thước hình ảnh yêu cầu: Xuất Tải về Thu gọn - Thêm + Mở rộng Đã mở rộng - Bớt + Thu gọn Đã thu gọn Khả dụng Tiếp @@ -683,7 +700,8 @@ Kích thước hình ảnh yêu cầu: Cho phép Chi tiết Ẩn - Mặc định + Ẩn + Hiện Nhật ký Nguồn Đang cài đặt… diff --git a/app/src/main/res/values-zh-rCN/plurals.xml b/app/src/main/res/values-zh-rCN/plurals.xml index 84a87cee8..e32bb8511 100644 --- a/app/src/main/res/values-zh-rCN/plurals.xml +++ b/app/src/main/res/values-zh-rCN/plurals.xml @@ -27,4 +27,10 @@ 已选择 %s 个补丁 + + 显示 %s 个应用 + + + %s 个隐藏的应用 + diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index b4d49d7be..879dd2cc6 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -32,6 +32,8 @@ 没有可用的应用 添加一个补丁源或在 %s 中启用现有补丁源 没有应用匹配 \"%1$s\" + 所有应用已隐藏 + 您已将所有应用从主屏幕隐藏 您想要修补哪个应用程序? @@ -91,10 +93,14 @@ 补丁<b>%s</b>需要未补丁的APK版本 推荐 未修补 + 不兼容 是的,帮我找个 APK 不,我已经有APK了 使用保存的APK(v%s) 没有保存的APK。修补需要重新选择APK文件 + 需要 Android %1$s+ + 不支持此设备 + <b>%1$s</b> 没有适用于 Android %2$d (API %3$d) 的支持版本。所有声明的版本都需要更高版本的 Android 下载原始 APK 说明: @@ -221,6 +227,7 @@ 有新的修补版本可用。重新修补您的应用以获取最新改进和修复 应用已卸载 此应用已在 Morphe 外卸载。重新安装以恢复功能 + APK大小 根模式需要在修补前将原始 APK 安装在设备上 Root模式下已排除GmsCore支持补丁 @@ -308,16 +315,13 @@ 执行 %s 失败 修补后的应用已保存以备后用 保存修补后的应用失败 - 下载 APK 文件 需要用户交互才能继续使用此插件 修補程式程序結束,傳回代碼 %1$s 安装成功 应用安装失败:%s - 安装未完成。请检查系统安装程序对话框并重试 APK 已保存 导出修补的应用失败 已移除保存的修补应用 - 已移除保存的副本 请在打开应用前安装它 包名 挂载失败:%s @@ -337,6 +341,10 @@ 网格 粒子 + 随机 + 启动时 + 每日 + 每3天 视差效果 倾斜设备时平滑背景过渡 @@ -353,6 +361,10 @@ 当前语言 某些语言的翻译可能缺失或不完整 访问 %s 以翻译新语言或改进现有翻译。 + + 主屏幕 + 问候语 + 在主屏幕上显示问候消息 应用图标 默认 @@ -367,7 +379,7 @@ 更新 使用预发布版Morphe - Receive early access to new Morpheph features. To get pre-release patches, enable the pre-release toggle in each patch source separately + 抢先体验 Morphe 的新功能。若要获取预发布补丁,请分别在每个补丁源中启用预发布开关 在移动数据上更新 Allow Morphe and patch updates to download over mobile data 后台更新通知 @@ -399,8 +411,17 @@ 启用复杂的 Morphe 补丁设置和自定义选项 启用专家模式? 专家模式提供了更多控制如何应用补丁的选项,但如果错误配置专家模式设置,可能会导致应用程序无法使用 - 移除未使用的原生库 - 从修补的应用中删除不支持 CPU 架构的原生库 + 为设备架构优化 + "在合并过程中,跳过不支持的 CPU 架构、语言环境和屏幕密度的拆分 APK 模块。 +对于纯 APK,在打补丁后会删除不受支持的架构的本机库" + + 字节码处理模式 + 控制打补丁期间字节码的处理方式。影响打补丁的速度、内存使用情况和输出 APK 大小 + 快速(推荐) + 快速 + 更快地进行补丁,且内存占用更低,但 APK 文件会更大。 推荐用于内存较低或较旧的设备 + 完整 + 补丁速度较慢,复制了旧行为。 仅当快速模式出现问题时才使用 GitHub PAT 用于拉取请求源所需 @@ -510,13 +531,7 @@ morphe_header_custom_dark.png 已安装的 Shizuku 版本不受支持 打开 Shizuku 安装失败。请重试或切换到其他安装程序 - 安装被 Android 阻止。请检查 Play Protect 或安全设置 已安装具有相同包名的其他应用。请在继续之前卸载它 - APK与此设备或 Android 版本不兼容 - APK 无效或已损坏 - 存储空间不足,无法安装 - 安装超时。请重试 - 安装已取消 正在打开 %1$s… %1$s 未确认安装。请检查其他应用并重试 %1$s 报告安装成功 @@ -547,6 +562,7 @@ morphe_header_custom_dark.png 用户名 (别名) 密码 导入 + 密钥库格式 密钥库凭据错误 密钥库已导入 导入密钥库失败 @@ -554,6 +570,7 @@ morphe_header_custom_dark.png 导出当前密钥库 无可用密钥库可导出 密钥库已导出 + 密钥库导出失败 显示密码 隐藏密码 @@ -682,7 +699,8 @@ morphe_header_custom_dark.png 允许 详细信息 隐藏 - 默认 + 隐藏 + 显示 日志 来源 正在安装… diff --git a/app/src/main/res/values-zh-rTW/plurals.xml b/app/src/main/res/values-zh-rTW/plurals.xml index 10170c71e..0e9990c94 100644 --- a/app/src/main/res/values-zh-rTW/plurals.xml +++ b/app/src/main/res/values-zh-rTW/plurals.xml @@ -13,7 +13,7 @@ %s 個套件 - 執行 %s 項修補 + 已執行 %s 項修補程式 %s 個 APK 檔案 @@ -27,4 +27,10 @@ 已選擇 %s 個修補 + + 顯示 %s 個應用程序 + + + %s 個隱藏的應用程序 + diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml index fcb790847..75d5774c0 100644 --- a/app/src/main/res/values-zh-rTW/strings.xml +++ b/app/src/main/res/values-zh-rTW/strings.xml @@ -32,6 +32,8 @@ 沒有應用程式可用 新增程式來源或啟用現有的來源於 %s 找不到與「%1$s」相符的應用程式 + 所有應用程式已隱藏 + 您已將所有應用程式從主畫面隱藏 你想修補哪個應用程式? @@ -91,10 +93,14 @@ 修補 <b>%s</b> 需要未修補的 APK,版本為: 推薦 未修補 + 不相容 是,請幫我尋找 APK 否,我已經有 APK 了 使用已儲存的 APK (v%s) 沒有已儲存的 APK。修補將需要再次選擇 APK 檔案 + 需要 Android %1$s+ + 此裝置不支援 + <b>%1$s</b> 沒有適用於 Android %2$d (API %3$d) 的版本。所有宣告的版本都需要較高的 Android 版本 下載原始 APK 步驟如下: @@ -221,6 +227,7 @@ 有更新的修補版本。重新修補你的應用程式以獲得最新的改進和修正 應用程式已卸載 這個應用程式已在 Morphe 外移除。重新修補以復原功能 + APK 檔案大小 Root 模式需要在修補之前將原始 APK 安裝在您的裝置上 GmsCore 支援性修補在 root 模式中已排除 @@ -308,16 +315,13 @@ 無法套用 %s 已儲存修補後的應用程式以供日後使用 無法儲存修補後的應用程式 - 下載 APK 檔案 需要使用者互動以繼續使用此插件 修補程式程序結束,傳回代碼 %1$s 安裝成功 無法安裝應用程式:%s - 安裝未完成。請檢查系統安裝對話方塊,然後再試一次 APK 已儲存 無法匯出已修補的應用程式 已移除儲存的修補應用程式 - 已移除儲存的複本 在開啟應用程式之前安裝應用程式 套件名稱 無法安裝:%s @@ -337,6 +341,10 @@ 格狀 粒子 + 隨機 + 開機時 + 每日 + 每隔3天 視差效果 傾斜裝置時平滑背景移動 @@ -353,6 +361,10 @@ 目前語言 某些語言的翻譯可能缺少或不完整 若要翻譯新語言或改進現有翻譯,請前往 %s + + 首頁 + 問候語 + 在首頁顯示問候語 應用程式圖示 預設 @@ -399,8 +411,17 @@ 啟用複雜 Morphe 修補設定和自訂選項 要啟用專家模式嗎? 專家模式可以更精細地控制修補的套用方式,但如果專家模式下的設定設定錯誤,可能會造成應用程式無法正常運作 - 刪除未使用的原生函式庫 - 刪除不支援的 CPU 架構原生函式庫修補的應用程式 + 針對裝置架構進行優化 + "在合併時,跳過對不受支援的 CPU 架構、語言環境和螢幕密度分割 APK 模組。 +對於純 APK,在修補後會移除不受支援架構的原始函式庫" + + 位元組程式碼處理模式 + 控制修補程式處理位元組程式碼的方式。影響修補速度、記憶體使用量和輸出 APK 檔案大小 + 快速(建議) + 快速 + 更快且佔用更少的記憶體,但 APK 檔案會更大。建議用於低記憶體或舊裝置 + 完整 + 修補速度較慢,呈現遺留行為。僅當快速模式出現問題時才使用 GitHub 權杖 用於合併請求捆綁包 @@ -509,13 +530,7 @@ morphe_header_custom_dark.png 安裝的 Shizuku 版本不受支援 開啟 Shizuku 安裝失敗。再試一次或切換到不同的安裝程式 - Android 已禁止安裝。請檢查 Play 安全防護或安全設定 已安裝具有相同套件名稱的其他應用程式。請先將其解除安裝後再繼續 - APK 與這部裝置或 Android 版本不相容 - APK 無效或損毀 - 沒有足夠的儲存空間進行安裝 - 安裝逾時。請再試一次 - 已取消安裝 正在開啟 %1$s…… %1$s 未確認安裝。請檢查其他應用程式,然後再試一次 %1$s 報告成功安裝 @@ -546,6 +561,7 @@ morphe_header_custom_dark.png 使用者名稱(別名) 密碼 匯入 + 金鑰庫格式 金鑰庫憑證資訊錯誤 已匯入金鑰庫 無法匯入金鑰庫 @@ -553,6 +569,7 @@ morphe_header_custom_dark.png 匯出目前金鑰庫 沒有可匯出的金鑰庫 已匯出金鑰庫 + 匯出金鑰庫失敗 顯示密碼 隱藏密碼 @@ -597,7 +614,7 @@ morphe_header_custom_dark.png 沒有已儲存的修補選擇 %1$s 至 %2$s %1$s 至 %2$s - 修補來源格式 + 修補來源 %s 這將刪除: 這將永久刪除所有儲存的修補選項,包括所有套件和來源 這將永久刪除 %s 的所有修補選項,跨越所有來源 @@ -681,9 +698,10 @@ morphe_header_custom_dark.png 允許 詳細資訊 隱藏 - 預設 + 隱藏 + 取消隱藏 日誌 - 個修補來源 + 修補來源 安裝中…… 掛載中…… 正在解除掛載…… diff --git a/app/src/main/res/values-zu-rZA/strings.xml b/app/src/main/res/values-zu-rZA/strings.xml index 180ea1689..48f97ef60 100644 --- a/app/src/main/res/values-zu-rZA/strings.xml +++ b/app/src/main/res/values-zu-rZA/strings.xml @@ -39,10 +39,12 @@ + + diff --git a/app/src/main/res/values/plurals.xml b/app/src/main/res/values/plurals.xml index f94d74e0f..104c46fc3 100644 --- a/app/src/main/res/values/plurals.xml +++ b/app/src/main/res/values/plurals.xml @@ -17,8 +17,8 @@ %s packages - Execute %s patch - Execute %s patches + Executed %s patch + Executed %s patches %s APK file @@ -36,4 +36,12 @@ %s patch selected %s patches selected + + Show %s app + Show %s apps + + + %s hidden app + %s hidden apps + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 02fed7931..2394e976a 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -15,7 +15,7 @@ Add source Do you want to add this patch source to Morphe? - Only add source from sources you trust + Only add sources you trust Morphe update available @@ -34,6 +34,8 @@ No apps available Add a patch source or enable an existing one in %s No apps match \"%1$s\" + All apps are hidden + You\'ve hidden all apps from the home screen What app do you want to patch? @@ -101,10 +103,14 @@ To patch <b>%s</b>, you need an unpatched APK of versions: Recommended Unpatched + Incompatible Yes, help me find an APK No, I already have an APK Use saved APK (v%s) No saved APK. Patching will require selecting the APK file again + Requires Android %1$s+ + Not supported on this device + <b>%1$s</b> has no supported versions for Android %2$d (API %3$d). All declared versions require a higher Android version Download the original APK @@ -240,6 +246,7 @@ For the best results, this app recommends patching a <b>full APK</b> A newer version of patches is available. Repatch your app to get the latest improvements and fixes App was uninstalled This app was uninstalled outside the Morphe. Repatch it to restore functionality + APK size Root mode requires the original APK to be installed on your device before patching @@ -336,17 +343,14 @@ For the best results, this app recommends patching a <b>full APK</b> Failed to apply %s Patched app saved for later Failed to save patched app - Download APK file User interaction is required in order to proceed with this plugin Patcher process exited with code %1$s Installed successfully Failed to install app: %s - Installation did not finish. Check the system installer dialog and try again APK Saved Failed to export patched app Removed saved patched app - Removed saved copy Install the app before opening it Package name @@ -369,6 +373,10 @@ For the best results, this app recommends patching a <b>full APK</b> Grid Particles None + Random + On launch + Daily + Every 3 days Parallax effect Smooth background shifting when tilting the device @@ -389,6 +397,11 @@ For the best results, this app recommends patching a <b>full APK</b> To translate new languages or improve the existing translations, visit %s morphe.software/translate + + Home screen + Greeting phrases + Show a greeting message on the home screen + App icon Default @@ -438,8 +451,18 @@ For the best results, this app recommends patching a <b>full APK</b> Enable complex Morphe patching settings and customization options Enable Expert mode? Expert mode gives more control over how patches are applied, but misconfiguring settings in expert mode can result in a non-functional app - Remove unused native libraries - Delete native libraries for unsupported CPU architectures from patched apps + Optimize for device architecture + "Skip split APK modules for unsupported CPU architectures, locales and screen densities during merge. +For plain APKs, strips native libraries for unsupported architectures after patching" + + + Bytecode processing mode + Controls how bytecode is processed during patching. Affects patching speed, memory usage, and output APK size + Fast (Recommended) + Fast + Faster patching and lower memory usage, at the cost of a larger APK. Recommended for low-RAM or older devices + Full + Slower patching, replicates legacy behavior. Use only if fast mode causes issues GitHub PAT @@ -557,16 +580,7 @@ The image dimensions must be as follows: The installed Shizuku version is not supported Open Shizuku The installation failed. Try again or switch to a different installer - The installation was blocked by Android. Check Play Protect or security settings Another app with this package name is already installed. Uninstall it before continuing - The APK is not compatible with this device or Android version - The APK is invalid or corrupted - There is not enough storage space for the installation - The installation timed out. Try again - The installation was cancelled - "%1$s - -Details: %2$s" Opening %1$s… %1$s did not confirm the installation. Check the other app and try again %1$s reported a successful installation @@ -599,6 +613,7 @@ Details: %2$s" Username (Alias) Password Import + Keystore format Incorrect keystore credentials Keystore imported Failed to import keystore @@ -606,6 +621,7 @@ Details: %2$s" Export the current keystore No keystore available to export Keystore exported + Failed to export keystore Show password Hide password @@ -744,7 +760,8 @@ Details: %2$s" Allow Details Hide - Default + Hidden + Unhide Logs Sources Installing… diff --git a/app/src/main/res/xml/locales_config.xml b/app/src/main/res/xml/locales_config.xml index 2df260b3c..e6c93df06 100644 --- a/app/src/main/res/xml/locales_config.xml +++ b/app/src/main/res/xml/locales_config.xml @@ -11,6 +11,7 @@ + @@ -48,6 +49,7 @@ + diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 43e33457d..d319a98ea 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,47 +1,48 @@ [versions] -about-libraries = "14.0.0-b03" +ackpine = "0.22.8" # Before upgrading, verify that the 'android-tools' version matches the patcher +about-libraries = "14.0.1" accompanist = "0.37.3" activity = "1.13.0" -android-gradle-plugin = "9.1.0" +android-gradle-plugin = "9.1.1" app-icon-loader-coil = "1.5.0" appcompat = "1.7.1" arsclib = "9696ffecda" coil = "2.7.0" collection = "0.4.0" -compose-bom = "2026.03.01" +compose-bom = "2026.04.01" compose-icons = "1.2.4" datetime = "0.7.1" dev-tools-gradle-plugin = "2.3.6" documentfile = "1.1.0" enumutil = "1.1.1" fading-edges = "1.0.4" -firebase-bom = "34.11.0" +firebase-bom = "34.12.0" google-services = "4.4.4" hidden-api-stub = "4.4.0" hiddenapibypass = "6.1" -koin = "4.2.0" -kotlin = "2.3.20" +koin = "4.2.1" +kotlin = "2.3.21" kotlin-process = "1.5.1" -ktor = "3.4.2" +ktor = "3.4.3" ktx = "1.18.0" libsu = "6.0.0" -markdown-renderer = "0.39.2" +markdown-renderer = "0.40.2" # Remove when released stable 1.5.0 in Compose BOM. Fix for https://github.com/MorpheApp/morphe-manager/issues/74 -material3 = "1.5.0-alpha16" # 1.5.0-alpha09 API Changes: Remove deprecated experimental ModalBottomSheet +material3 = "1.5.0-alpha18" # 1.5.0-alpha09 API Changes: Remove deprecated experimental ModalBottomSheet morphe-library = "1.3.0" -morphe-patcher = "1.3.3" -navigation = "2.9.7" +morphe-patcher = "1.5.0" +navigation = "2.9.8" placeholder = "2.0.0" play-services-base = "18.10.0" preferences-datastore = "1.2.1" -reorderable = "3.0.0" +reorderable = "3.1.0" room-version = "2.8.4" scrollbars = "1.0.4" semver-parser = "3.0.0" -serialization = "1.10.0" +serialization = "1.11.0" shizuku = "13.1.5" splash-screen = "1.2.0" -ui-tooling = "1.10.6" +ui-tooling = "1.11.0" viewmodel-lifecycle = "2.10.0" work-runtime = "2.11.2" @@ -99,6 +100,12 @@ koin-compose = { group = "io.insert-koin", name = "koin-androidx-compose", versi koin-compose-navigation = { group = "io.insert-koin", name = "koin-androidx-compose-navigation", version.ref = "koin" } koin-workmanager = { group = "io.insert-koin", name = "koin-androidx-workmanager", version.ref = "koin" } +# Ackpine +ackpine-core = { module = "ru.solrudev.ackpine:ackpine-core", version.ref = "ackpine" } +ackpine-ktx = { module = "ru.solrudev.ackpine:ackpine-ktx", version.ref = "ackpine" } +ackpine-shizuku = { module = "ru.solrudev.ackpine:ackpine-shizuku", version.ref = "ackpine" } +ackpine-shizuku-ktx = { module = "ru.solrudev.ackpine:ackpine-shizuku-ktx", version.ref = "ackpine" } + # About Libraries about-libraries-core = { group = "com.mikepenz", name = "aboutlibraries-compose-core", version.ref = "about-libraries" } about-libraries-m3 = { group = "com.mikepenz", name = "aboutlibraries-compose-m3", version.ref = "about-libraries" } diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index d997cfc60..b1b8ef56b 100644 Binary files a/gradle/wrapper/gradle-wrapper.jar and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 8e61ef125..4b271a646 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,8 +1,10 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionSha256Sum=2ab2958f2a1e51120c326cad6f385153bb11ee93b3c216c5fccebfdfbb7ec6cb -distributionUrl=https\://services.gradle.org/distributions/gradle-9.4.1-bin.zip +distributionSha256Sum=553c78f50dafcd54d65b9a444649057857469edf836431389695608536d6b746 +distributionUrl=https\://services.gradle.org/distributions/gradle-9.5.0-bin.zip networkTimeout=10000 +retries=0 +retryBackOffMs=500 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew index 739907dfd..b9bb139f7 100755 --- a/gradlew +++ b/gradlew @@ -57,7 +57,7 @@ # Darwin, MinGW, and NonStop. # # (3) This script is generated from the Groovy template -# https://github.com/gradle/gradle/blob/2d6327017519d23b96af35865dc997fcb544fb40/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# https://github.com/gradle/gradle/blob/3d91ce3b8caaf77ad09f381f43615b715b53f72c/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt # within the Gradle project. # # You can find Gradle at https://github.com/gradle/gradle/. diff --git a/gradlew.bat b/gradlew.bat index c4bdd3ab8..24c62d56f 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -23,8 +23,8 @@ @rem @rem ########################################################################## -@rem Set local scope for the variables with windows NT shell -if "%OS%"=="Windows_NT" setlocal +@rem Set local scope for the variables, and ensure extensions are enabled +setlocal EnableExtensions set DIRNAME=%~dp0 if "%DIRNAME%"=="" set DIRNAME=. @@ -51,7 +51,7 @@ echo. 1>&2 echo Please set the JAVA_HOME variable in your environment to match the 1>&2 echo location of your Java installation. 1>&2 -goto fail +"%COMSPEC%" /c exit 1 :findJavaFromJavaHome set JAVA_HOME=%JAVA_HOME:"=% @@ -65,7 +65,7 @@ echo. 1>&2 echo Please set the JAVA_HOME variable in your environment to match the 1>&2 echo location of your Java installation. 1>&2 -goto fail +"%COMSPEC%" /c exit 1 :execute @rem Setup the command line @@ -73,21 +73,10 @@ goto fail @rem Execute Gradle -"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* +@rem endlocal doesn't take effect until after the line is parsed and variables are expanded +@rem which allows us to clear the local environment before executing the java command +endlocal & "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* & call :exitWithErrorLevel -:end -@rem End local scope for the variables with windows NT shell -if %ERRORLEVEL% equ 0 goto mainEnd - -:fail -rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of -rem the _cmd.exe /c_ return code! -set EXIT_CODE=%ERRORLEVEL% -if %EXIT_CODE% equ 0 set EXIT_CODE=1 -if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% -exit /b %EXIT_CODE% - -:mainEnd -if "%OS%"=="Windows_NT" endlocal - -:omega +:exitWithErrorLevel +@rem Use "%COMSPEC%" /c exit to allow operators to work properly in scripts +"%COMSPEC%" /c exit %ERRORLEVEL% diff --git a/settings.gradle.kts b/settings.gradle.kts index f2c292c57..1aa543fa4 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -1,3 +1,5 @@ +@file:Suppress("UnstableApiUsage") + import java.util.Properties val localProps = Properties().apply {