diff --git a/.github/workflows/gradle_build.yml b/.github/workflows/gradle_build.yml index 1a220dc4..b03af921 100644 --- a/.github/workflows/gradle_build.yml +++ b/.github/workflows/gradle_build.yml @@ -1,30 +1,52 @@ -name: Build Jar +name: Build JavaFX GUI Jar on: push: - branches: [ "master" ] + branches: [ "master", "UI" ] pull_request: - branches: [ "master" ] + branches: [ "master", "UI" ] + workflow_dispatch: permissions: contents: read jobs: build: - runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - - name: Set up JDK 8 + + - name: Set up JDK 21 uses: actions/setup-java@v4 with: - java-version: '8' + java-version: '21' distribution: 'temurin' + - uses: gradle/actions/setup-gradle@v4 - - name: Build with Gradle - run: ./gradlew fatJar - - uses: actions/upload-artifact@v4 + + - name: Build JavaFX GUI jar + run: | + chmod +x gradlew + ./gradlew clean guiFatJar --no-daemon + ls -lh build/libs build/javafx + + - name: Install Xvfb + run: sudo apt-get update && sudo apt-get install -y xvfb + + - name: JavaFX smoke test + run: xvfb-run -a ./gradlew javafxSmoke --no-daemon + + - name: Package GUI artifact + run: | + mkdir -p dist/gui/javafx + cp build/libs/*-gui.jar dist/gui/ + cp build/javafx/*.jar dist/gui/javafx/ + ls -lh dist/gui dist/gui/javafx + + - name: Upload GUI artifact + uses: actions/upload-artifact@v4 with: - name: APKEditor - path: build/libs + name: APKEDITOR-GUI + path: dist/gui + if-no-files-found: error diff --git a/.github/workflows/gradle_build_beta.yml b/.github/workflows/gradle_build_beta.yml index d8fecad6..815d567a 100644 --- a/.github/workflows/gradle_build_beta.yml +++ b/.github/workflows/gradle_build_beta.yml @@ -1,4 +1,4 @@ -name: Build Beta Jar +name: Build Beta Jars on: push: @@ -11,20 +11,43 @@ permissions: jobs: build: - runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - - name: Set up JDK 8 + + - name: Set up JDK 21 uses: actions/setup-java@v4 with: - java-version: '8' + java-version: '21' distribution: 'temurin' + - uses: gradle/actions/setup-gradle@v4 +<<<<<<< HEAD + with: + gradle-version: '8.5' - name: Build with Gradle - run: ./gradlew betaJar + run: gradle fatJar --no-daemon - uses: actions/upload-artifact@v4 +======= + + - name: Build CLI and JavaFX jars + run: | + chmod +x gradlew + ./gradlew fatJar guiFatJar --no-daemon + ls -lh build/libs build/javafx + + - name: Package beta jars + run: | + mkdir -p dist/APKEditor/javafx + cp build/libs/*.jar dist/APKEditor/ + cp build/javafx/*.jar dist/APKEditor/javafx/ + ls -lh dist/APKEditor dist/APKEditor/javafx + + - name: Upload beta artifacts + uses: actions/upload-artifact@v4 +>>>>>>> job-work with: name: APKEditor - path: build/libs + path: dist/APKEditor + if-no-files-found: error diff --git a/.gitignore b/.gitignore index 0afd022d..23da9433 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,10 @@ # Project exclude paths +/README.md/ /.gradle/ /build/ -/build/classes/java/main/ -/.github/ \ No newline at end of file +/.github/ + +# JavaFX/proot local test artifacts +/debian-trixie/ +hs_err_pid*.log +/core.* diff --git a/.img/ui.jpg b/.img/ui.jpg new file mode 100755 index 00000000..87283970 Binary files /dev/null and b/.img/ui.jpg differ diff --git a/.img/ui_last.png b/.img/ui_last.png new file mode 100755 index 00000000..bf9c93a0 Binary files /dev/null and b/.img/ui_last.png differ diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 00000000..20f4db6f --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,68 @@ +# AGENTS.md + +APKEditor Futuristic: a native JavaFX GUI wrapping the upstream APKEditor engine, plus the upstream CLI. Single Gradle module (`rootProject.name = 'APKEditor'`), Java 21. The old Swing/Java2D UI was removed. + +## Build & run + +```bash +sh gradlew compileJava --offline # CLI + shared model/services +sh gradlew javafxClasses # JavaFX UI (downloads OpenJFX on first run) +sh gradlew guiFatJar # build/libs/APKEditor-1.4.9-gui.jar +sh gradlew fatJar --offline # build/libs/APKEditor-1.4.9-all.jar (CLI) +sh gradlew build -x test --offline # no tests exist +``` + +- JavaFX 21.0.13 is a Maven Central dependency with a platform classifier + (`linux`, `linux-aarch64`, `win`, `mac`, `mac-aarch64`). The first build needs + network; later builds can use `--offline` once the artifacts are cached. + +- On Termux `./gradlew` fails with permission denied; always use `sh gradlew`. +- `sh gradlew run` launches the **CLI** (`com.reandroid.apkeditor.Main`), not the GUI. +- `guiFatJar`/`javafxRuntime` copy the downloaded JavaFX modules to + `build/javafx`. Run the jar with: + `java --module-path build/javafx --add-modules javafx.controls -jar build/libs/APKEditor-1.4.9-gui.jar` +- `guiFatJar` includes the JavaFX app classes plus the engine/shared classes, but + not the JavaFX runtime (it stays in `build/javafx`). +- Jar names are derived from `version` in `build.gradle` (currently `1.4.9`); CI globs `build/libs/*-gui.jar`. + +## JavaFX UI + +- Sources live in a separate `javafx` source set (`src/javafx/java`, `src/javafx/resources`). `compileJava`/`fatJar` do not depend on JavaFX; `javafxClasses` and `guiFatJar` do. +- Entrypoint: `com.reandroid.apkeditor.javafx.JavaFXApplication` -> `javafx.MainView`. Views live under `src/javafx/java/com/reandroid/apkeditor/javafx/view/`. +- The UI reuses `gui.model.AppState`, `gui.service.ApkService`, `gui.service.OperationService`, `gui.service.LogService`, `gui.i18n.I18n`, `gui.navigation.Route`, and `operations.OperationCatalog`. The remaining `gui` packages are shared model/service code, not a Swing UI. +- Services still publish callbacks on the AWT EDT (`SwingUtilities.invokeLater`). Wrap JavaFX updates with `com.reandroid.apkeditor.javafx.Fx.run` (`Platform.runLater`). +- Theme palettes live in `FxTheme`. `src/javafx/resources/styles/app.css` is a template whose `-fx-apx-*` tokens `FxTheme.renderCss(...)` replaces with concrete palette colors; `MainView` loads the rendered CSS as a `data:text/css` stylesheet. Use `derive(, )` for glows/translucent variants so all themes stay coherent. +- Icons are SVG paths in `com.reandroid.apkeditor.javafx.Icons` (no image assets). The workspace editor is `com.reandroid.apkeditor.javafx.CodeEditor` (editable `TextArea` + read-only gutter + `Ln/Col` status); it has no syntax highlighting. +- Run on glibc Linux: `sh gradlew javafxRun`. +- Smoke test: `sh gradlew javafxSmoke`. +- Termux tests use **only** `bash proot-run.sh` or `bash proot-run.sh --smoke-test` (Debian proot + glibc JavaFX). It runs `javafxRuntime`, binds `build/javafx` as `/jfx-modules`, and uses `--module-path /jfx-modules`. Defaults: `JFX_BASE=$HOME/javaFX`, `ROOTFS=$JFX_BASE/debian-trixie`, `START_XVFB=1`; override with env vars. + +## Dependencies & offline builds + +- Engine deps are checked-in jars in `libs/` (`ARSCLib.jar`, `smali.jar`, `JCommand.jar`), loaded via `files(...)`, not Maven coordinates. Do not replace with remote deps. +- `--offline` is expected after the first build; Gradle wrapper is 8.5. JavaFX is the one dependency that needs an initial online resolution from Maven Central. + +## Entrypoints & architecture + +- CLI: `src/main/java/com/reandroid/apkeditor/Main.java`. +- GUI: `com.reandroid.apkeditor.javafx.JavaFXApplication` -> `MainView`. +- Operations are registered once in `operations/OperationCatalog.java` and run via `core/OperationRegistry` -> `LegacyOptionsOperation`. +- Views must not call `Decompiler`/`Builder`/`Protector` directly. Route work through `gui/service/OperationService`. +- Shared UI state lives in `gui/model/AppState` (PropertyChangeSupport); it must not contain engine logic. +- `docs/dev-ui.md` is the authoritative deep reference for UI structure, routes, themes, shortcuts, and limitations. Update it when UI behavior changes. + +## Conventions + +- GUI source comments (and `build.gradle` comments) are in Spanish; upstream engine code is English. Match the file you edit. +- GUI strings live in `src/main/resources/i18n/messages.properties` (English base) and `messages_es.properties`. Add every new key to both; `I18n` returns the raw key on a miss. Default UI language is Spanish. +- Engine CLI strings use `src/main/resources/strings/strings*.properties` (separate from GUI i18n). +- `jar` task uses `duplicatesStrategy = FAIL`: duplicate resources break the regular jar (fat jars use EXCLUDE). +- No test framework is configured; do not invent unit-test commands. Verify with `compileJava`/`javafxClasses`/`guiFatJar`, and use `javafxSmoke` or `bash proot-run.sh --smoke-test` only for the JavaFX entrypoint. + +## Known limitations (do not "fix" silently) + +- Editor edits are in-memory only; saving back to APK/project is not implemented. +- JavaFX editor has line numbers and status but no syntax highlighting. +- Theme, language, and session are not persisted across restarts. +- ZIP entries read are capped at 2 MB; tree is capped at 6000 entries; hex dump shows 4096 bytes. +- AAB loading is offered by the file selector but actual support depends on the engine. diff --git a/CHANGES.md b/CHANGES.md new file mode 100644 index 00000000..aabe43c6 --- /dev/null +++ b/CHANGES.md @@ -0,0 +1,6 @@ +## Beta + +- Actualisar a javaFX para un mejor estilo +- second +- third + diff --git a/LICENSE b/LICENSE old mode 100755 new mode 100644 diff --git a/README.md b/README.md old mode 100755 new mode 100644 index 717f3f06..9643875b --- a/README.md +++ b/README.md @@ -1,558 +1,110 @@ -
👈 Click arrows to expand/collapse details on this page
- -# APKEditor -### Powerful android apk resources editor -This tool uses [ARSCLib](https://github.com/REAndroid/ARSCLib) to edit any apk resources and has six main features - -
java -jar APKEditor.jar -h - -```ShellSession -$ java -jar APKEditor.jar -h -APKEditor - x.x.x -https://github.com/REAndroid/APKEditor -Android binary resource files editor -Commands: - d | decode Decodes android resources binary to readable json/xml/raw. - b | build Builds android binary from json/xml/raw. - m | merge Merges split apk files from directory or compressed apk files - like XAPK, APKM, APKS ... - x | refactor Refactors obfuscated resource names - p | protect Protects/Obfuscates apk resource files. Using unique - obfuscation techniques. - info Prints information of apk. -Other options: - -h | -help Displays this help and exit - -v | -version Displays version information and exit - -To get help about each command run with: - -h +

+ ꧁𝔊 𝔄𝔓𝔎𝔈𝔡𝔦𝔱𝔬𝔯𝔉𝔲𝔱𝔲𝔯𝔦𝔰𝔱𝔦𝔠 𝔊꧂ - Interface +

+

+ Español | English +

+ + +## Description + +APKEditor Futuristic is a native JavaFX interface for the original [APKEditor](https://github.com/REAndroid/APKEditor) engine. The interface reuses the engine and its operations, but no longer ships a Swing UI. + +## Final goal + +![ui_last](./.img/ui_last.png) + +> [!WARNING] +> Work in progress. + +## Build + +Native JavaFX GUI jar (Gradle downloads OpenJFX 21.0.13 on the first build): + +```bash +sh gradlew guiFatJar ``` -
- -#### 1- Decompile -* Decompiles resources of apk to human readable json string. -* Decompiles resources of apk to XML source code (for un-obfuscated apk only). Use ``` -t xml ``` -
java -jar APKEditor.jar d -i path/to/your-file.apk - -```ShellSession -$ java -jar APKEditor.jar d -i test.apk -o test_json -00.000 I: [DECOMPILE] Using: APKEditor version 1.4.3, ARSCLib version 1.3.6 - -t = xml - -load-dex = 3 - -dex-lib = jf - -i = test.apk - -o = test_json - _________________________ -00.006 I: [DECOMPILE] Loading ... -00.101 I: [DECOMPILE] Decompiling to xml ... -00.264 I: [DECOMPILE] Initializing android framework ... -00.264 I: [DECOMPILE] Loading android framework for version: 34 -00.304 I: [DECOMPILE] Initialized framework: android-34 (14) -00.320 I: [DECOMPILE] [SANITIZE]: Sanitizing paths ... -00.341 I: [DECOMPILE] Validating resource names ... -00.396 I: [DECOMPILE] All resource names are valid -00.396 I: [DECOMPILE] Decode: archive-info.json -00.398 I: [DECOMPILE] Decode: uncompressed-files.json -00.412 I: [DECOMPILE] Decoding: AndroidManifest.xml -00.450 I: [DECOMPILE] public.xml: com.test.name -> package_1 -00.508 I: [DECOMPILE] Res files: resources -02.483 I: [DECOMPILE] Baksmali: classes.dex -05.041 I: [DECOMPILE] Baksmali: classes2.dex -12.643 I: [DECOMPILE] Extracting root files ... -12.766 I: [DECOMPILE] Dumping signatures ... -12.766 I: [DECOMPILE] Saved to: test_json -``` -
more info - -```ShellSession -Decodes android resources binary to readable json/xml/raw. -Usage: - d [Options, flags] -Options: - -framework Path of framework file (can be multiple). - -framework-version Preferred framework version number - -i Input path. - -load-dex Number of dex files to load at a time. - If the apk dex files count greater than this value, - then the decoder loads one dex at a time. - *Applies only when -dex-lib set to internal. - *Default = 3 - *See below. - -o Output path. Optional, if not provided then a new file - will be generated on same directory as input - -remove-annotation [Baksmali] Type name of annotation to remove from dex - e.g: -remove-annotation Lkotlin/Metadata; - *Can be multiple. - -res-dir Sets resource files root dir name. e.g. for obfuscation - to move files from 'res/*' to 'r/*' or vice versa. - -sig Signatures directory path. - -t Decode types: - [xml, json, raw, sig] - -dex-lib Dex library to use: - 1) internal : Use internal library, supports dex - versions up to 042. - 2) jf : Use library by JesusFreke/smali, supports dex - versions 035 and below. - *Default = internal - *See below. - [internal, jf] - -comment-level [Baksmali] Sets the comment level. - 1) off : No comment will be printed. - 2) basic : Prints basic and resource id comments. - 3) detail : Including , Prints class & method - relation comments. - 4) detail2 : Including , Prints encoded hex - strings with literal as comment. - 5) full : Prints all comments. - **Default = detail - [off, basic, detail, detail2, full] -Flags: - -dex Copy raw dex files / skip smali. - -dex-markers Dumps dex markers (applies only when smali mode). - -dex-profile Decodes dex profile binary files under assets/dexopt to - readable json files. - -f Force delete output path. - -h | -help | --help Displays this help and exit. - -keep-res-path Keeps original res/* file paths: - *Applies only when decoding to xml - *All res/* files will be placed on dir - *The relative paths will be linked to values/*xml - -no-cache Do not create dex .cache files. - -no-dex-debug Drops all debug info from smali/dex. - -smali-registers [Baksmali] Use ".registers" directive instead of the - default ".locals". - -split-json Splits resources.arsc into multiple parts as per type - entries (use this for large files) - -vrd Validate resources dir name - (eg. if a drawable resource file path is 'res/abc.png' - then it will be moved to 'res/drawable/abc.png)' -Examples: - 1) [Basic] - java -jar APKEditor.jar d -i path/input.apk - 2) [Specify output] - java -jar APKEditor.jar d -i path/input.apk -o path/output.apk - 3) [Specify decode type] - java -jar APKEditor.jar d -t xml -i path/input.apk - 4) [Specify framework file(s)] - java -jar APKEditor.jar d -i path/input.apk -framework framework-res.apk - -framework platforms/android-32/android.jar - 5) [Decode apk signature block] - java -jar APKEditor.jar d -t sig -i path/input.apk -sig path/signatures_dir -Notes: - 1) [internal] Dex builder: - * Fully supports dex files up to 042. - * Highest dex file compression. - * Builds with similar dex-section order as r8/dx. - * Convenient dex markers editing, see file smali/classes/dex-file.json - * Additional helpful smali comments: e.g class/method hierarchy. - * Supports whitespaces on class simple name as introduced on dex 040+ - 2) [-load-dex] To print correct class/method hierarchy, it is necessary to - load all dex files at once. This may result high memory consumption and - could fail with "OutOfMemoryError" thus you are required to limit the - number of dex files to load at a time. You can overcome this problem with - -Xmx memory arg e.g java -Xmx8g -jar APKEditor.jar ... -``` -
-
- -#### 2- Build -Builds back to apk from decompiled json/XML files -
java -jar APKEditor.jar b -i path/to/decompiled-directory - -```ShellSession -$ java -jar APKEditor.jar b -i test_json -o test_edited.apk -00.000 I: [BUILD] Using: APKEditor version 1.4.3, ARSCLib version 1.3.6 - -t = xml - -dex-lib = jf - -i = test_json - -o = test_edited.apk - ______________________________ -00.005 I: [BUILD] Scanning XML directory ... -00.024 I: [BUILD] Scanning: test_json -00.168 I: [BUILD] Initializing android framework ... -00.168 I: [BUILD] Loading android framework for version: 34 -00.254 I: [BUILD] Initialized framework: android-34 (14) -00.254 I: [BUILD] Set main package id from manifest: @mipmap/ic_launcher -00.256 I: [BUILD] Main package id initialized: id = 0x7f, from: @mipmap/ic_launcher -00.256 I: [BUILD] Encoding attrs ... -00.307 I: [BUILD] Encoding values ... -00.560 I: [BUILD] Scan: package_1/res -00.661 I: [BUILD] Scanned 5718 files: package_1/res -00.852 I: [BUILD] Add manifest: AndroidManifest.xml -00.852 I: [BUILD] Building dex ... -00.960 I: [BUILD] (1/2) Cached: classes.dex -00.973 I: [BUILD] (2/2) Cached: classes2.dex -01.033 I: [BUILD] Scanning root directory ... -01.036 I: [BUILD] Restoring original file paths ... -01.078 I: [BUILD] Loading signatures ... -01.082 I: [BUILD] Sorting files ... -01.104 I: [BUILD] Refreshing resource table ... -01.163 I: [BUILD] TableBlock: packages = 1, size = 3067996 bytes -01.164 I: [BUILD] Applying: extractNativeLibs=false -01.164 I: [BUILD] Writing apk... -01.185 I: [BUILD] Buffering compress changed files ... -06.083 I: [BUILD] Writing files: 6637 -06.201 I: [BUILD] Writing signature block ... -06.241 I: [BUILD] Saved to: test_edited.apk -``` -
more info - -```ShellSession -Builds android binary from json/xml/raw. -Options: - -framework Path of framework file (can be multiple). - -framework-version Preferred framework version number - -i Input path. - -o Output path. Optional, if not provided then a new file - will be generated on same directory as input - -res-dir Sets resource files root dir name. e.g. for obfuscation - to move files from 'res/*' to 'r/*' or vice versa. - -sig Signatures directory path. - -t Build types, By default build types determined by quick - scanning of input directory files. Values are: - [xml, json, raw, sig] - -extractNativeLibs Sets extractNativeLibs attribute on manifest and - applies compression of library files (*.so). - *Default = manifest - 1) manifest: read and apply from manifest. - 2) none: remove attribute from manifest and store - libraries compressed. - 3) true: set manifest attribute 'true' and store - libraries compressed. - 4) false: set manifest attribute 'false' and store - libraries un-compressed with 4096 alignment. - [manifest, none, false, true] - -dex-lib Dex library to use: - 1) internal : Use internal library, supports dex - versions up to 042. - 2) jf : Use library by JesusFreke/smali, supports dex - versions 035 and below. - *Default = internal - *See below. - [internal, jf] -Flags: - -dex-profile Encodes decoded dex profile json files to binary - assets/dexopt files. - -f Force delete output path. - -h | -help | --help Displays this help and exit. - -no-cache Ignore built cached .dex files and re-build smali files. - -vrd Validate resources dir name - (eg. if a drawable resource file path is 'res/abc.png' - then it will be moved to 'res/drawable/abc.png)' -Examples: - 1) [Basic] - java -jar APKEditor.jar b -i path/input_directory - 2) [Specify output] - java -jar APKEditor.jar b -i path/input_directory -o path/output.apk - 3) [Restore signatures] - java -jar APKEditor.jar b -t sig -i path/input.apk -sig - path/signatures_dir - 4) [Specify framework] - java -jar APKEditor.jar b -i path/input_directory -framework - framework-res.apk -framework platforms/android-32/android.jar + +The GUI jar is generated at: + +```text +build/libs/APKEditor-1.4.9-gui.jar ``` -
-
- -#### 3- Merge -Merges multiple splitted apk files (directory, xapk, apkm, apks ...) to standalone apk -
java -jar APKEditor.jar m -i path/to/input - - ```ShellSession -$ java -jar APKEditor.jar m -i apk_files -00.000 I: [MERGE] Using: APKEditor version 1.4.3, ARSCLib version 1.3.6 - -i = apk_files - -o = apk_files_merged.apk - _____________________________ -00.009 I: [MERGE] Searching apk files ... -00.011 I: [MERGE] Found apk files: 3 -00.214 I: [MERGE] Found modules: 3 -00.329 I: [MERGE] Merging: base -00.331 I: [MERGE] Added [base] classes.dex -> classes.dex -00.331 I: [MERGE] Added [base] classes2.dex -> classes2.dex -01.289 I: [MERGE] Merging: config.arm64_v8a -01.293 I: [MERGE] Merging: config.xxhdpi -01.634 I: [MERGE] Sanitizing manifest ... -01.637 I: [MERGE] Removed-attribute : splitTypes -01.637 I: [MERGE] Removed-attribute : requiredSplitTypes -01.640 I: [MERGE] Attributes on removed: 0x01010591 (isSplitRequired) -01.641 I: [MERGE] Removed-element : name="com.android.vending.splits.required" -01.641 I: [MERGE] Removed-element : name="com.android.stamp.source" -01.641 I: [MERGE] Removed-element : name="com.android.stamp.type" -01.645 I: [MERGE] Removed-table-entry : res/xml/splits0.xml -01.646 I: [MERGE] Removed-element : name="com.android.vending.splits" -01.646 I: [MERGE] Removed-element : name="com.android.vending.derived.apk.id" -01.779 I: [MERGE] Applying: extractNativeLibs=false -01.779 I: [MERGE] Writing apk ... -01.806 I: [MERGE] Buffering compress changed files ... -04.766 I: [MERGE] Writing files: 6637 -04.981 I: [MERGE] Writing signature block ... -05.035 I: [MERGE] Saved to: apk_files_merged.apk -``` -![apkmerger](/.github/apkmerger.png) - -
more info - -```ShellSession -Merges split apk files from directory or compressed apk files like XAPK, -APKM, APKS ... -Options: - -i Input path. - -o Output path. Optional, if not provided then a new file - will be generated on same directory as input - -res-dir Sets resource files root dir name. e.g. for obfuscation - to move files from 'res/*' to 'r/*' or vice versa. - -extractNativeLibs Sets extractNativeLibs attribute on manifest and - applies compression of library files (*.so). - *Default = manifest - 1) manifest: read and apply from manifest. - 2) none: remove attribute from manifest and store - libraries compressed. - 3) true: set manifest attribute 'true' and store - libraries compressed. - 4) false: set manifest attribute 'false' and store - libraries un-compressed with 4096 alignment. - [manifest, none, false, true] -Flags: - -clean-meta Cleans META-INF directory along with signature block. - -f Force delete output path. - -h | -help | --help Displays this help and exit. - -validate-modules Validates for same versionNumber of base.apk with split - apk files. - -vrd Validate resources dir name - (eg. if a drawable resource file path is 'res/abc.png' - then it will be moved to 'res/drawable/abc.png)' -Examples: - 1) [Basic] - java -jar APKEditor.jar m -i path/input -o path/output.apk + +It includes the JavaFX app and the shared/engine classes, but not the JavaFX +runtime: `guiFatJar` copies the downloaded modules to `build/javafx`. Launch it +with: + +```bash +java --module-path build/javafx --add-modules javafx.controls \ + -jar build/libs/APKEditor-1.4.9-gui.jar ``` -
-
- -#### 4- Refactor -Refactors obfuscated resource entry names -
java -jar APKEditor.jar x -i path/to/input.apk - - ```ShellSession -$ java -jar APKEditor.jar x -i input.apk -00.000 I: [REFACTOR] Using: APKEditor version 1.4.3, ARSCLib version 1.3.6 - -i = input.apk - -o = input_refactored.apk - _____________________________ -00.006 I: [REFACTOR] Loading apk: input.apk -00.105 I: [REFACTOR] Auto refactoring ... -00.105 I: [REFACTOR] Validating resource names ... -00.279 I: [REFACTOR] Initializing android framework ... -00.279 I: [REFACTOR] Loading android framework for version: 34 -00.320 I: [REFACTOR] Initialized framework: android-34 (14) -00.364 I: [REFACTOR] All resource names are valid -00.364 I: [REFACTOR] Validating file paths ... -00.455 I: [REFACTOR] Auto renamed entries -00.644 I: [REFACTOR] Removed unused table strings -Table size changed = 3067996, 3282324 -00.644 I: [REFACTOR] Writing apk ... -00.663 I: [REFACTOR] Buffering compress changed files ... -01.587 I: [REFACTOR] Writing files: 6637 -01.729 I: [REFACTOR] Writing signature block ... -01.783 I: [REFACTOR] Saved to: input_refactored.apk -``` -
more info - -```ShellSession -Refactors obfuscated resource names -Options: - -i Input path. - -o Output path. Optional, if not provided then a new file - will be generated on same directory as input - -public-xml Path of resource ids xml file (public.xml) - Loads names and applies to resources from 'public.xml' - file -Flags: - -clean-meta Cleans META-INF directory along with signature block. - -f Force delete output path. - -fix-types Corrects resource type names based on usages and values - -h | -help | --help Displays this help and exit. -Examples: - 1) [Basic] - java -jar APKEditor.jar x -i path/input.apk -o path/output.apk + +CLI-only fat jar: + +```bash +sh gradlew fatJar --offline ``` -
-
- -#### 5- Protect -Protects apk resources against almost all known decompile/modify tools. -
java -jar APKEditor.jar p -i path/to/input.apk - - ```ShellSession -00.000 I: [PROTECT] Using: APKEditor version 1.4.3, ARSCLib version 1.3.6 - -keep-type = font - -i = test.apk - -o = test_protected.apk - ___________________________________ -00.178 I: [PROTECT] DirectoryConfuser: Confusing ... -00.519 I: [PROTECT] FileNameConfuser: Confusing ... -00.602 I: [PROTECT] TableConfuser: Confusing ... -00.758 I: [PROTECT] TableConfuser: Type names ... -00.831 I: [PROTECT] Writing apk ... -00.858 I: [PROTECT] Buffering compress changed files ... -01.794 I: [PROTECT] Writing files: 6637 -01.931 I: [PROTECT] Writing signature block ... -01.987 I: [PROTECT] Saved to: test_protected.apk -``` -
more info - -```ShellSession -Protects/Obfuscates apk resource files. Using unique obfuscation techniques. -Options: - -dex-level Dex protection level (0-5). - 0 - Off - 1 - Basic - 2 - Normal (Not implemented yet) - 3 - High (Not implemented yet) - 4 - Max (Not implemented yet) - * Default 0 - -dic-dir-names Path to a text file containing a list of directory - names separated by new line. - -dic-file-names Path to a text file containing a list of file names - separated by new line. - -i Input path. - -keep-type Keep specific resource type names (e.g drawable), By - default keeps only resource type. - *Can be multiple - -o Output path. Optional, if not provided then a new file - will be generated on same directory as input -Flags: - -confuse-zip Confuse zip structure. When this turned on: - * Apps might crash if accessing apk files directly e.g - Class.getResourceAsStream(). - * Some apk scanners might flag it as "Malformed zip" - -f Force delete output path. - -h | -help | --help Displays this help and exit. - -skip-manifest Do not protect manifest. -Examples: - 1) [Basic] - java -jar APKEditor.jar p -i path/input.apk -o path/output.apk + +```text +build/libs/APKEditor-1.4.9-all.jar ``` -
-
- -#### 6- Info (⭐NEW⭐) -Prints/dumps from basic up to detailed information of apk. -
java -jar APKEditor.jar info -v -resources -i input.apk - - ```ShellSession -Package name=com.mypackage id=0x7f - type string id=1 entryCount=1 - resource 0x7f010000 string/app_name - () My Application - (-de) Meine Bewerbung - (-ru-rRU) Мое заявление - type mipmap id=2 entryCount=1 - resource 0x7f020000 mipmap/ic_launcher_round - () res/mipmap/ic_launcher_round.png - type drawable id=3 entryCount=1 - resource 0x7f030000 drawable/ic_launcher - () #006400 - -``` -
more info - -```ShellSession -Prints information of apk. -Options: - -filter-type Prints only the specified resource type names - *This applies only when flag '-resources' used. - *Can be multiple. - -framework Path of framework file (can be multiple). - -framework-version Preferred framework version number - -i Input path. - -o Output path. Optional, if not provided then a new file - will be generated on same directory as input - -res Prints resource entries specified by either of: - 1) Hex or decimal resource id. - 2) Full resource name e.g @string/app_name. - *Can be multiple. - -xmlstrings Print the strings of the given compiled xml assets. - *Can be multiple - -xmltree Prints the compiled xmls in the given assets. - *Can be multiple - -t Print types/formats: - [text, json, xml] -Flags: - -activities Prints main activity class name. If verbose mode, - prints all declared activities including - . - -app-class Application class name. - -app-icon App icon path/value. If verbose mode, prints all - configurations. - -app-name App name. If verbose mode, prints all configurations. - -app-round-icon App round icon path/value. If verbose mode, prints all - configurations. - -configurations Print the configurations in the APK. - -dex Prints dex information. - -f Force delete output path. - -h | -help | --help Displays this help and exit. - -languages Print the languages in the APK. - -list-files List files inside apk. - -list-xml-files List compiled xml files inside apk. - -locales Print the locales in the APK. - -min-sdk-version Minimum SDK version. - -package Package name(application id) from manifest and if - verbose mode, prints resource table packages. - -permissions Permissions. - -resources Prints all resources - -signatures Prints signature information. - -signatures-base64 Prints signature information with base64 certificates. - -strings Print the contents of the resource table string pool in - the APK. - -target-sdk-version Target SDK version. - -v Verbose mode. - -version-code App version code. - -version-name App version name. -Examples: - 1) [Basic] - java -jar APKEditor.jar info -i file.apk - 2) [Specify output and type] - java -jar APKEditor.jar info -i path/input.apk -t json -v -o - info_file.json - 3) [Print only specific type] - java -jar APKEditor.jar info -i path/input.apk -resources -filter-type - drawable + +> Use `sh gradlew` if `./gradlew` fails with `permission denied`. + +## Launch the graphical interface + +### Development run + +```bash +sh gradlew javafxRun ``` -
-
+### Smoke test + +```bash +sh gradlew javafxSmoke +``` ---- +The first JavaFX build needs network to download the Maven artifacts; later +builds can use `--offline` once they are cached. -***Build executable jar*** -
./gradlew fatJar +## Termux (proot) - ```ShellSession - -# NB: Due to my lazyness , the dependency ARSCLib.jar is pre-built and placed under APKEditor/libs/ARSCLib.jar or you can build yourself and replace it. -git clone https://github.com/REAndroid/APKEditor -cd APKEditor -./gradlew fatJar -# Executable jar will be placed ./build/libs/APKEditor-x.x.x.jar +The OpenJFX binaries are linked against glibc. On Termux, run the app inside the +Debian `proot` environment: - ``` -
+```bash +bash proot-run.sh +bash proot-run.sh --smoke-test +``` -***Downloads*** -* [Latest release with pre-built executable jar](https://github.com/REAndroid/APKEditor/releases/latest) +`proot-run.sh` uses `~/javaFX/debian-trixie` as the Debian rootfs, runs Gradle to +download JavaFX and copy it to `build/javafx`, and starts Xvfb unless +`START_XVFB=0`. Override with `JFX_BASE`, `ROOTFS`, `DISPLAY_NUM`, or +`SKIP_BUILD=1`. -***Contribute*** -* Everyone is so welcome in this project, if you have some code improvements please make a pull request -* Please share your ideas / thoughts in [discussions](https://github.com/REAndroid/APKEditor/discussions) -* Please create issue you faced while using this tool along with your apk +For an existing VNC/Termux:X11 display: +```bash +START_XVFB=0 DISPLAY_NUM=:0 bash proot-run.sh +``` +## Quick start -
Contact +1. Open the GUI. +2. Click the APK selector or press `Ctrl+O`. +3. Choose an APK/ZIP file. +4. Browse the tree, open files in the editor, and check the logs. +5. Press `F5` or click **Decompile** to generate the project. +6. Use the **Build**, **Merge**, **Refactor**, and **Protect** views to run the engine operations. +7. Switch between Spanish and English from the top bar or **Settings**. -* Telegram: [@kikfox](https://t.me/kikfox) -* Email: [thekikfox@gmail.com](mailto:thekikfox@gmail.com) +## Status -
+The only graphical interface is the native JavaFX UI. It includes live theme +switching, Spanish/English language switching, SVG icons, a code editor with +line numbers, live logs, progress, and the same operation catalog as the engine. +Details about routes, themes, shortcuts, and limitations are available in +[dev-ui](./docs/dev-ui.md). diff --git a/build.gradle b/build.gradle old mode 100755 new mode 100644 index cae105fb..3aee57dd --- a/build.gradle +++ b/build.gradle @@ -1,58 +1,147 @@ +plugins { + id 'application' + id 'java' +} -apply plugin: 'java' +group = 'com.reandroid.apkeditor' +version = '1.4.9' -group 'com.reandroid.apkeditor' -version '1.4.9' +application { + mainClass = 'com.reandroid.apkeditor.Main' +} java { - sourceCompatibility JavaVersion.VERSION_1_8 - targetCompatibility JavaVersion.VERSION_1_8 + // Definir la compatibilidad con Java 21 + sourceCompatibility = JavaVersion.VERSION_21 + targetCompatibility = JavaVersion.VERSION_21 } -if (JavaVersion.current().isJava8Compatible()) { - allprojects { - tasks.compileJava { - //options.addStringOption('-Xlint:unchecked', '-quiet') - } - } +// JavaFX se descarga como dependencia de Maven Central. El artefacto lleva +// un classifier por plataforma (linux, linux-aarch64, win, mac, mac-aarch64). +def javafxVersion = '21.0.13' +def osName = System.getProperty('os.name', '').toLowerCase() +def osArch = System.getProperty('os.arch', '').toLowerCase() +def javafxPlatform = osName.contains('win') + ? 'win' + : osName.contains('mac') + ? (osArch.contains('aarch64') ? 'mac-aarch64' : 'mac') + : (osArch.contains('aarch64') ? 'linux-aarch64' : 'linux') + +configurations { + javafx } + repositories { mavenCentral() } dependencies { - //implementation("io.github.reandroid:ARSCLib:+") - compile(files("$rootProject.projectDir/libs/ARSCLib.jar")) - - // built from: https://github.com/REAndroid/smali-lib - compile(files("$rootProject.projectDir/libs/smali.jar")) + implementation(files("$rootProject.projectDir/libs/ARSCLib.jar")) + implementation(files("$rootProject.projectDir/libs/smali.jar")) + implementation(files("$rootProject.projectDir/libs/JCommand.jar")) + javafx("org.openjfx:javafx-base:${javafxVersion}:${javafxPlatform}") { transitive = false } + javafx("org.openjfx:javafx-graphics:${javafxVersion}:${javafxPlatform}") { transitive = false } + javafx("org.openjfx:javafx-controls:${javafxVersion}:${javafxPlatform}") { transitive = false } +} - // built from: https://github.com/REAndroid/JCommand - compile(files("$rootProject.projectDir/libs/JCommand.jar")) +// La interfaz JavaFX vive en un source set separado para que el CLI no +// dependa de las dependencias de JavaFX. +sourceSets { + javafx { + // Las rutas por defecto son src/javafx/java y src/javafx/resources. + compileClasspath += sourceSets.main.output + } } +sourceSets.javafx.compileClasspath += configurations.javafx + processResources { filesMatching('apkeditor.properties') { - expand('version': version) + expand('version': project.version) } } javadoc { exclude('com/reandroid/test/**') } + jar { exclude('com/reandroid/test/**') duplicatesStrategy = DuplicatesStrategy.FAIL + manifest { + attributes 'Main-Class': application.mainClass.get() + } } +// Fat jar del CLI (incluye el motor y las clases compartidas). task fatJar(type: Jar) { duplicatesStrategy = DuplicatesStrategy.EXCLUDE + archiveClassifier = 'all' + manifest { - attributes( - 'Main-Class': 'com.reandroid.apkeditor.Main' - ) + attributes 'Main-Class': application.mainClass.get() + } + + from { + configurations.runtimeClasspath.collect { it.isDirectory() ? it : zipTree(it) } } - from { configurations.compile.collect { it.isDirectory() ? it : zipTree(it) } } with jar } +// Copia los jars modulares descargados a build/javafx para poder ejecutar el +// jar de la GUI sin depender de un SDK local. +tasks.register('javafxRuntime', Sync) { + group = 'application' + description = 'Copia el runtime de JavaFX descargado a build/javafx' + from configurations.javafx + into layout.buildDirectory.dir('javafx') +} + +task guiFatJar(type: Jar) { + dependsOn tasks.named('javafxClasses') + dependsOn tasks.named('javafxRuntime') + duplicatesStrategy = DuplicatesStrategy.EXCLUDE + archiveClassifier = 'gui' + + manifest { + attributes 'Main-Class': 'com.reandroid.apkeditor.javafx.JavaFXApplication' + } + + // La app JavaFX y las clases compartidas; el runtime descargado se copia a + // build/javafx y se aporta con --module-path al ejecutar el jar. + from sourceSets.javafx.output + from sourceSets.main.output + from { + configurations.runtimeClasspath.collect { it.isDirectory() ? it : zipTree(it) } + } +} + +// Tareas de la interfaz JavaFX nativa. El runtime se carga por module-path. +def configurarJavaFxExec = { JavaExec task -> + task.dependsOn tasks.named('javafxClasses') + task.classpath = files(sourceSets.javafx.output, sourceSets.main.runtimeClasspath) + task.mainClass = 'com.reandroid.apkeditor.javafx.JavaFXApplication' + task.jvmArgs = [ + '--module-path', configurations.javafx.asPath, + '--add-modules', 'javafx.controls' + ] +} + +tasks.register('javafxRun', JavaExec) { + group = 'application' + description = 'Ejecuta la interfaz JavaFX nativa (requiere un display)' + configurarJavaFxExec(it) +} + +tasks.register('javafxSmoke', JavaExec) { + group = 'verification' + description = 'Prueba de humo de JavaFX; cierra sola al cargar' + args '--smoke-test' + configurarJavaFxExec(it) + jvmArgs = [ + '-Dprism.order=sw', + '--module-path', configurations.javafx.asPath, + '--add-modules', 'javafx.controls' + ] +} + diff --git a/docs/.language/README.es.md b/docs/.language/README.es.md new file mode 100644 index 00000000..23e2f2f3 --- /dev/null +++ b/docs/.language/README.es.md @@ -0,0 +1,126 @@ +

+<<<<<<< HEAD + Español | English +

+ +

+======= +>>>>>>> job-work + APKEditor Futuristic - Interfaz +

+ +

+ Español | English +

+ + +## Descripción + +<<<<<<< HEAD +APKEditor Futuristic es una interfaz gráfica moderna que envuelve el motor original de [APKEditor](https://github.com/REAndroid/APKEditor). + +## Objetivo final + +![ui_last](https://github.com/VictorH028/APKEditorFuturistic/blob/master/.img/ui_last.png) +======= +APKEditor Futuristic es una interfaz nativa JavaFX para el motor original +[APKEditor](https://github.com/REAndroid/APKEditor). La interfaz reutiliza el +motor y sus operaciones; ya no incluye una UI Swing. + +## Objetivo final + +![ui_last](../../.img/ui_last.png) +>>>>>>> job-work + +> [!WARNING] +> Trabajo en progreso. + +## Compilación + +Jar de la interfaz JavaFX (Gradle descarga OpenJFX 21.0.13 en la primera compilación): + +```bash +sh gradlew guiFatJar +``` + +El jar se genera en: + +```text +build/libs/APKEditor-1.4.9-gui.jar +``` + +Incluye la app JavaFX y las clases compartidas/del motor, pero no el runtime de +JavaFX: `guiFatJar` copia los módulos descargados a `build/javafx`. Ejecútalo con: + +```bash +java --module-path build/javafx --add-modules javafx.controls \ + -jar build/libs/APKEditor-1.4.9-gui.jar +``` + +Fat jar solo para el CLI: + +```bash +sh gradlew fatJar --offline +``` + +```text +build/libs/APKEditor-1.4.9-all.jar +``` + +> Usa `sh gradlew` si `./gradlew` falla con `permission denied`. + +## Ejecutar la interfaz gráfica + +### Ejecución de desarrollo + +```bash +sh gradlew javafxRun +``` + +### Prueba de humo + +```bash +sh gradlew javafxSmoke +``` + +La primera compilación de JavaFX necesita red para descargar los artefactos de +Maven; después puedes usar `--offline` cuando estén en caché. + +## Termux (proot) + +Los binarios de OpenJFX están enlazados contra glibc. En Termux, ejecuta la app +dentro del entorno Debian `proot`: + +```bash +bash proot-run.sh +bash proot-run.sh --smoke-test +``` + +`proot-run.sh` usa `~/javaFX/debian-trixie` como rootfs de Debian, ejecuta Gradle +para descargar JavaFX y copiarlo a `build/javafx`, y arranca Xvfb salvo +`START_XVFB=0`. Puedes sobrescribir con `JFX_BASE`, `ROOTFS`, `DISPLAY_NUM` o +`SKIP_BUILD=1`. + +Para un display VNC/Termux:X11 existente: + +```bash +START_XVFB=0 DISPLAY_NUM=:0 bash proot-run.sh +``` + +## Inicio rápido + +1. Abre la interfaz. +2. Pulsa el selector de APK o `Ctrl+O`. +3. Elige un APK/ZIP. +4. Explora el árbol, abre archivos en el editor y revisa el log. +5. Pulsa `F5` o **Descompilar** para generar el proyecto. +6. Usa **Construir**, **Fusionar**, **Refactorizar** y **Proteger** para ejecutar las operaciones del motor. +7. Cambia entre español e inglés desde la barra superior o **Configuración**. + +## Estado + +La única interfaz gráfica es la UI nativa JavaFX. Incluye cambio de tema en +caliente, idioma español/inglés, iconos SVG, editor con números de línea, log en +vivo, progreso y el mismo catálogo de operaciones del motor. El detalle de +rutas, temas, atajos y limitaciones está en +[dev-ui](./docs/.language/dev-ui.es.md). diff --git a/docs/.language/dev-ui.es.md b/docs/.language/dev-ui.es.md new file mode 100644 index 00000000..1b769427 --- /dev/null +++ b/docs/.language/dev-ui.es.md @@ -0,0 +1,334 @@ +# APKEDITOR UI + +

+<<<<<<< HEAD + Español | English +======= + English | Español +>>>>>>> job-work +

+ +Documentación de la interfaz gráfica de APKEditor. + +> Estado actual: la interfaz gráfica es **JavaFX nativa** (`src/javafx`). La antigua UI Swing/Java2D fue eliminada. El CLI y el modelo/servicios compartidos permanecen en `src/main`. + +## 1. Resumen + +APKEDITOR UI es una interfaz de escritorio para: + +- Abrir y analizar archivos APK/ZIP. +- Explorar el árbol de archivos del paquete. +- Leer código Smali, XML y recursos. +- Ver imágenes binarias como volcado hexadecimal. +- Ejecutar las operaciones existentes del motor: decodificar, construir, fusionar, refactorizar y proteger. +- Consultar logs, progreso y estadísticas del APK. + +La interfaz no reimplementa el motor. Las vistas sólo preparan los datos y +`gui/service/OperationService` ejecuta las operaciones registradas en +`operations/OperationCatalog`. + +## 2. Requisitos + +- **JDK 21**. +- Gradle Wrapper incluido en el repositorio (`gradlew`). +- Acceso a red en la primera compilación: Gradle descarga OpenJFX 21.0.13 desde Maven Central. +- Los archivos de `libs/`: + - `ARSCLib.jar` + - `smali.jar` + - `JCommand.jar` +- Un servidor gráfico X11: + - En escritorio Linux: Xorg/Wayland con XWayland. + - En Termux: VNC (`Xvnc`) o `Termux:X11`, o el Xvfb que arranca `proot-run.sh`. + +No hace falta instalar Android SDK para ejecutar la interfaz. + +## 3. Compilación + +Desde la raíz del proyecto: + +```bash +sh gradlew compileJava --offline +sh gradlew javafxClasses +``` + +Build completo sin tests: + +```bash +sh gradlew build -x test --offline +``` + +Jar ejecutable de la interfaz JavaFX (Gradle descarga OpenJFX en la primera compilación): + +```bash +sh gradlew guiFatJar +``` + +Tras la primera resolución, puedes añadir `--offline` para reutilizar los +artefactos de Maven en caché. + +Fat jar sólo para el CLI: + +```bash +sh gradlew fatJar --offline +``` + +Artefactos generados: + +```text +build/classes/java/main/ clases CLI + compartidas +build/classes/java/javafx/ clases de la UI JavaFX +build/resources/main/ recursos compartidos +build/resources/javafx/ styles/app.css +build/javafx/ módulos de JavaFX descargados +build/libs/APKEditor-1.4.9.jar jar regular (CLI) +build/libs/APKEditor-1.4.9-all.jar fat jar del CLI +build/libs/APKEditor-1.4.9-gui.jar jar de la GUI JavaFX +``` + +> Usa `sh gradlew` si `./gradlew` falla con `permission denied`. + +## 4. Ejecución + +### 4.1 JAR + +El jar de la GUI incluye la app y las clases compartidas/del motor, pero **no** el +runtime de JavaFX. `guiFatJar` copia los módulos descargados a `build/javafx`; +añádelos con `--module-path`: + +```bash +java --module-path build/javafx --add-modules javafx.controls \ + -jar build/libs/APKEditor-1.4.9-gui.jar +``` + +### 4.2 Ejecución de desarrollo (glibc Linux) + +```bash +sh gradlew javafxRun +``` + +`javafxRun` resuelve los módulos JavaFX de Maven, compila el source set y lanza +`com.reandroid.apkeditor.javafx.JavaFXApplication` con el module path de JavaFX. + +### 4.3 Termux (proot) + +En Termux los binarios de OpenJFX necesitan glibc, así que ejecuta dentro de +Debian `proot`: + +```bash +bash proot-run.sh +``` + +Prueba de humo (arranca Xvfb, imprime `JAVAFX_UI_READY` y cierra): + +```bash +bash proot-run.sh --smoke-test +``` + +Valores por defecto: `JFX_BASE=$HOME/javaFX`, `ROOTFS=$JFX_BASE/debian-trixie`, +`START_XVFB=1`. Ejecuta `javafxClasses` + `javafxRuntime` (descarga y copia a +`build/javafx`), monta ese directorio como `/jfx-modules` y usa +`--module-path /jfx-modules`. Sobrescríbelos con variables de entorno y usa +`SKIP_BUILD=1` para omitir la compilación de Gradle. + +Para un display ya existente (VNC/Termux:X11): + +```bash +START_XVFB=0 DISPLAY_NUM=:0 bash proot-run.sh +``` + +### 4.4 Prueba de humo en glibc + +```bash +sh gradlew javafxSmoke +``` + +## 5. Punto de entrada y arquitectura + +```text +JavaFXApplication + -> MainView (shell, rutas, tema, idioma) + -> DashboardFxView + -> WorkspaceFxView (estructura/código/manifiesto/recursos/análisis) + -> OperationFxView (build/merge/refactor/protect) + -> InformationFxView + -> SettingsFxView +``` + +- `MainView` construye la barra superior, la sidebar, el área de contenido y la barra de estado. Cachea las vistas por ruta y las reconstruye al cambiar de idioma. +- La UI reutiliza código compartido de `src/main`: + - `gui.model.AppState`: APK actual, análisis, árbol, tema, idioma, ruta y estado ocupado. + - `gui.service.ApkService`: analiza APKs fuera del hilo de UI. + - `gui.service.OperationService`: ejecuta operaciones del motor en segundo plano. + - `gui.service.LogService`: entradas y niveles de log. + - `gui.i18n.I18n`: cadenas de la UI. + - `gui.navigation.Route`: identificadores de ruta. + - `operations.OperationCatalog` / `core.OperationRegistry`: operaciones. +- Los servicios siguen publicando callbacks mediante `SwingUtilities.invokeLater` (EDT de AWT). Envuelve cada actualización JavaFX con `Fx.run(...)` (`Platform.runLater`). + +Las vistas no deben llamar a `Decompiler`, `Builder`, `Protector` ni a otras +clases del motor directamente. Todo el trabajo pasa por `OperationService`. + +## 6. Distribución de la ventana + +### Barra superior + +- Marca: logo con degradado y subtítulo `FUTURISTIC`. +- Chip del APK: nombre del archivo, tooltip con la ruta y acción para cambiar de archivo. Abre el selector. +- Selector de tema y de idioma. +- Controles de ventana (minimizar/maximizar/cerrar). + +### Sidebar + +- **WORKSPACE**: Dashboard, Decompile, Analyze, Smali Viewer. +- **OPERATIONS**: Build, Merge, Refactor, Protect. +- **SYSTEM**: Information, Settings. +- Tarjeta de resumen del motor al pie. +- La ruta activa recibe la clase de estilo `active`. + +### Contenido + +Región desplazable con la vista seleccionada. Aquí viven las tarjetas, las +cabeceras de sección y los layouts de dos columnas (explorador + editor, +información + estadísticas). + +### Barra de estado + +- Uso de memoria y texto de estado del motor. +- Las operaciones en curso cambian el estado a `Processing...` (color warning). + +## 7. Rutas + +| Ruta | Vista | Descripción | +| --- | --- | --- | +| `DASHBOARD` | `DashboardFxView` | Tarjetas de resumen, resumen del APK y accesos rápidos. | +| `DECOMPILE` (`DECODE`) | `WorkspaceFxView` | Workspace principal con cabecera de descompilación y árbol. | +| `ANALYZE` | `WorkspaceFxView` | Workspace con la pestaña de análisis activa. | +| `SMALI_VIEWER` | `WorkspaceFxView` | Workspace con la pestaña de código activa. | +| `BUILD` | `OperationFxView` | Formulario y ejecución de build. | +| `MERGE` | `OperationFxView` | Formulario y ejecución de merge. | +| `REFACTOR` | `OperationFxView` | Formulario y ejecución de refactor. | +| `PROTECT` | `OperationFxView` | Formulario y ejecución de protect. | +| `INFORMATION` | `InformationFxView` | Información de la aplicación y estadísticas. | +| `SETTINGS` | `SettingsFxView` | Tema, idioma e información del runtime. | + +`LOGS` existe en el enum compartido pero no es una ruta de la sidebar; el log está embebido en el workspace. + +## 8. Workspace + +`WorkspaceFxView` contiene: + +1. **Cabecera**: título, subtítulo, búsqueda, reset y acción **Decompile**. +2. **Tarjeta explorador**: campo de filtro y árbol perezoso. Los iconos y colores dependen del tipo (Smali, XML, recurso, binario). +3. **Tarjeta editor**: editor con pestañas (`TabPane`), buscar siguiente, copiar y ocultar/mostrar gutter. +4. **Panel de log**: hora, icono de nivel y mensaje. +5. **Panel de progreso**: porcentaje, barra y lista de pasos. +6. **Tarjetas inferiores**: `ApplicationInfoCard` y `StatisticsCard`. + +Pestañas del workspace: + +- **Structure**: árbol de paquetes. +- **Code**: editor con el Smali/XML/texto seleccionado. +- **Manifest**: contenido de AndroidManifest. +- **Resources**: explorador de recursos. +- **Analysis**: resumen de aplicación/estadísticas. + +Límites (no los subas en silencio): + +- Las entradas ZIP leídas están limitadas a **2 MB**. +- El árbol está limitado a **6000** entradas. +- El volcado hexadecimal muestra **4096** bytes. +- Los AAB se pueden seleccionar, pero el soporte real depende del motor. + +## 9. Editor + +`com.reandroid.apkeditor.javafx.CodeEditor` es un `TextArea` editable más un +gutter de números de línea de sólo lectura. + +- El gutter se desplaza con el texto mediante un listener (no un binding). +- Línea de estado: `Ln/Col`, espacios, codificación (`UTF-8`) e idioma. +- `toggleGutter()` oculta/muestra el gutter. +- Sin resaltado de sintaxis. +- Las ediciones son **sólo en memoria**; guardar de vuelta al APK/proyecto no está implementado. + +## 10. Temas + +`FxTheme.NAMES`: + +| Tema | Carácter | +| --- | --- | +| `Futuristic` | Fondo azul profundo, primario cian, acentos púrpura (por defecto). | +| `Cyberpunk` | Fondo púrpura, primario magenta, acentos cian. | +| `Dark` | Gris oscuro neutro, primario azul cielo. | +| `Midnight` | Azul muy oscuro, acentos azul suave. | + +`src/javafx/resources/styles/app.css` es una **plantilla**. Usa tokens como +`-fx-apx-bg`, `-fx-apx-surface`, `-fx-apx-primary`, `-fx-apx-border`, +`-fx-apx-text`, etc. `FxTheme.renderCss(name, template)` los sustituye por la +paleta y `MainView` inyecta el resultado como hoja de estilos +`data:text/css;base64,...`. + +Usa `derive(, )` en la plantilla para glows y variantes translúcidas, +de modo que los cuatro temas sigan siendo coherentes. + +## 11. Idioma + +- Idioma por defecto: **español**. +- Cadenas: `src/main/resources/i18n/messages.properties` (inglés) y + `messages_es.properties` (español). +- Cada clave debe existir en ambos archivos; `I18n` devuelve la clave cruda si falta. +- El selector de la barra superior actualiza `AppState`; `MainView` limpia cachés y reconstruye. + +## 12. Iconos + +`com.reandroid.apkeditor.javafx.Icons` guarda trazos SVG (sin assets de imagen): + +- `Icons.node(name, size, styleClass)` para iconos normales. +- `Icons.holder(...)` para iconos contenidos/apilados. +- `Icons.logo(...)` para la marca con degradado. +- Se usan en la sidebar, pestañas, tarjetas, árbol, niveles de log y pasos de progreso. + +## 13. Atajos + +| Atajo | Acción | +| --- | --- | +| `Ctrl+O` | Abrir APK/ZIP. | +| `F5` | Ir a Decompile y ejecutar decode. | +| `Ctrl+1` | Ir a Decompile. | +| `Ctrl+,` | Abrir Settings. | + +## 14. Operaciones + +Las operaciones se registran una vez en `operations/OperationCatalog.java`: + +| ID | Nombre | Opciones | +| --- | --- | --- | +| `decode` | Decode APK | `DecompileOptions` | +| `build` | Build APK | `BuildOptions` | +| `merge` | Merge APK | `MergerOptions` | +| `refactor` | Refactor | `RefactorOptions` | +| `protect` | Protect | `ProtectorOptions` | +| `info` | APK Info | `InfoOptions` | + +`OperationFxView` construye el formulario para el ID seleccionado, resuelve las +rutas de entrada/salida y delega la ejecución en `OperationService`. Los pasos de +progreso son `Preparing -> Running -> Finishing`; los logs y errores se añaden al +`LogService` compartido. + +## 15. Limitaciones + +- Las ediciones del editor son sólo en memoria. +- El tema, el idioma y la sesión no persisten entre reinicios. +- Sin resaltado de sintaxis. +- Límites de ZIP/árbol/hex indicados en la sección 8. +- El soporte de AAB depende del motor. +- No hay framework de tests; verifica con `javafxClasses`, `guiFatJar`, + `javafxSmoke` o `bash proot-run.sh --smoke-test`. + +## 16. Solución de problemas + +- **Falla la resolución de JavaFX**: ejecuta `sh gradlew javafxRuntime` con red + una vez; después puedes usar `--offline`. +- **`Permission denied` en `./gradlew`**: usa `sh gradlew`. +- **`Unable to open DISPLAY`**: arranca Xvfb/VNC o usa `bash proot-run.sh`. +- **`guiFatJar` compila pero el jar no arranca**: añade + `--module-path build/javafx --add-modules javafx.controls`. diff --git a/docs/dev-ui.md b/docs/dev-ui.md new file mode 100644 index 00000000..5fde4d2c --- /dev/null +++ b/docs/dev-ui.md @@ -0,0 +1,324 @@ +# APKEDITOR UI + +

+ Español | English +

+ +Documentation for the APKEditor graphical interface. + +> Current status: the graphical interface is **native JavaFX** (`src/javafx`). The old Swing/Java2D UI was removed. The CLI and the shared model/services remain in `src/main`. + +## 1. Overview + +APKEDITOR UI is a desktop interface for: + +- Opening and analyzing APK/ZIP files. +- Browsing the package file tree. +- Reading Smali code, XML, and resources. +- Viewing binary images as hexadecimal dumps. +- Running the existing engine operations: decode, build, merge, refactor, and protect. +- Checking logs, progress, and APK statistics. + +The interface does not reimplement the engine. Views only prepare data, and `gui/service/OperationService` runs the operations registered in `operations/OperationCatalog`. + +## 2. Requirements + +- **JDK 21**. +- Gradle Wrapper included in the repository (`gradlew`). +- Network access on the first build: Gradle downloads OpenJFX 21.0.13 from Maven Central. +- The files in `libs/`: + - `ARSCLib.jar` + - `smali.jar` + - `JCommand.jar` +- An X11 graphical server: + - On a Linux desktop: Xorg/Wayland with XWayland. + - On Termux: VNC (`Xvnc`) or `Termux:X11`, or the Xvfb started by `proot-run.sh`. + +The Android SDK is not required to run the interface. + +## 3. Build + +From the project root: + +```bash +sh gradlew compileJava --offline +sh gradlew javafxClasses +``` + +Full build without tests: + +```bash +sh gradlew build -x test --offline +``` + +Executable JavaFX GUI jar (Gradle downloads OpenJFX on the first build): + +```bash +sh gradlew guiFatJar +``` + +After the first resolution, add `--offline` to reuse the cached Maven artifacts. + +CLI-only fat jar: + +```bash +sh gradlew fatJar --offline +``` + +Generated artifacts: + +```text +build/classes/java/main/ CLI + shared classes +build/classes/java/javafx/ JavaFX UI classes +build/resources/main/ shared resources +build/resources/javafx/ styles/app.css +build/javafx/ downloaded JavaFX modules +build/libs/APKEditor-1.4.9.jar regular jar (CLI) +build/libs/APKEditor-1.4.9-all.jar CLI fat jar +build/libs/APKEditor-1.4.9-gui.jar JavaFX GUI jar +``` + +> Use `sh gradlew` if `./gradlew` fails with `permission denied`. + +## 4. Running + +### 4.1 JAR + +The GUI jar contains the app and the shared/engine classes, but **not** the +JavaFX runtime. `guiFatJar` copies the downloaded modules to `build/javafx`; +add them with `--module-path`: + +```bash +java --module-path build/javafx --add-modules javafx.controls \ + -jar build/libs/APKEditor-1.4.9-gui.jar +``` + +### 4.2 Development run (glibc Linux) + +```bash +sh gradlew javafxRun +``` + +`javafxRun` resolves the Maven JavaFX modules, compiles the JavaFX source set, and launches +`com.reandroid.apkeditor.javafx.JavaFXApplication` with the JavaFX module path. + +### 4.3 Termux (proot) + +On Termux the OpenJFX binaries need glibc, so run inside Debian `proot`: + +```bash +bash proot-run.sh +``` + +Smoke test (starts Xvfb, prints `JAVAFX_UI_READY`, closes): + +```bash +bash proot-run.sh --smoke-test +``` + +Defaults: `JFX_BASE=$HOME/javaFX`, `ROOTFS=$JFX_BASE/debian-trixie`, +`START_XVFB=1`. It runs `javafxClasses` + `javafxRuntime` (download + copy to +`build/javafx`), binds that directory as `/jfx-modules`, and uses +`--module-path /jfx-modules`. Override with env vars and use `SKIP_BUILD=1` to +skip the Gradle build. + +For a display that already exists (VNC/Termux:X11): + +```bash +START_XVFB=0 DISPLAY_NUM=:0 bash proot-run.sh +``` + +### 4.4 Smoke test on glibc + +```bash +sh gradlew javafxSmoke +``` + +## 5. Entry point and architecture + +```text +JavaFXApplication + -> MainView (window shell, routes, theme, language) + -> DashboardFxView + -> WorkspaceFxView (structure/code/manifest/resources/analysis) + -> OperationFxView (build/merge/refactor/protect) + -> InformationFxView + -> SettingsFxView +``` + +- `MainView` builds the top bar, sidebar, content area, and status bar. It caches route views and rebuilds them when the language changes. +- The UI reuses shared code from `src/main`: + - `gui.model.AppState`: current APK, analysis, tree, theme, language, route, busy flag. + - `gui.service.ApkService`: analyzes APKs off the UI thread. + - `gui.service.OperationService`: runs engine operations on a background thread. + - `gui.service.LogService`: log entries and levels. + - `gui.i18n.I18n`: UI strings. + - `gui.navigation.Route`: route identifiers. + - `operations.OperationCatalog` / `core.OperationRegistry`: operations. +- Services still publish callbacks through `SwingUtilities.invokeLater` (AWT EDT). Wrap every JavaFX update with `Fx.run(...)` (`Platform.runLater`). + +Views must not call `Decompiler`, `Builder`, `Protector`, or other engine classes directly. All work goes through `OperationService`. + +## 6. Window layout + +### Top bar + +- Brand: gradient logo mark plus `APKEditor` and `FUTURISTIC` subtitle. +- APK chip: file name, path tooltip, and a change-file action. Opens the file chooser. +- Theme selector and language selector. +- Window controls (minimize/maximize/close). + +### Sidebar + +- **WORKSPACE**: Dashboard, Decompile, Analyze, Smali Viewer. +- **OPERATIONS**: Build, Merge, Refactor, Protect. +- **SYSTEM**: Information, Settings. +- Footer summary card with engine status. +- The active route gets the `active` style class. + +### Content + +A scrollable content region with the selected view. Cards, section headers, and +the two-column layouts (explorer + editor, infos + stats) live here. + +### Status bar + +- Memory usage and engine/status text. +- Busy operations switch the status to `Processing...` (warning color). + +## 7. Routes + +| Route | View | Description | +| --- | --- | --- | +| `DASHBOARD` | `DashboardFxView` | Overview cards, APK summary, and action tiles. | +| `DECOMPILE` (`DECODE`) | `WorkspaceFxView` | Main workspace with the decompile header and tree. | +| `ANALYZE` | `WorkspaceFxView` | Workspace with the analysis tab active. | +| `SMALI_VIEWER` | `WorkspaceFxView` | Workspace with the code tab active. | +| `BUILD` | `OperationFxView` | Build operation form and runner. | +| `MERGE` | `OperationFxView` | Merge operation form and runner. | +| `REFACTOR` | `OperationFxView` | Refactor operation form and runner. | +| `PROTECT` | `OperationFxView` | Protect operation form and runner. | +| `INFORMATION` | `InformationFxView` | Application information and statistics. | +| `SETTINGS` | `SettingsFxView` | Theme, language, and runtime information. | + +`LOGS` exists in the shared enum but is not a sidebar route; logs are embedded in the workspace. + +## 8. Workspace + +`WorkspaceFxView` contains: + +1. **Header**: title, subtitle, search, reset, and **Decompile** action. +2. **Explorer card**: filter field and a lazy file tree. Icons and colors depend on the file type (Smali, XML, resource, binary). +3. **Editor card**: tabbed editor (`TabPane`), find next, copy, and gutter toggle. +4. **Log panel**: timestamp, level icon, and message. +5. **Progress panel**: percentage, bar, and step list. +6. **Bottom cards**: `ApplicationInfoCard` and `StatisticsCard`. + +Workspace tabs: + +- **Structure**: package tree. +- **Code**: editor with the currently selected Smali/XML/text file. +- **Manifest**: AndroidManifest content. +- **Resources**: resource browser. +- **Analysis**: application/statistics summary. + +Caps (do not raise silently): + +- ZIP entries read are capped at **2 MB**. +- The file tree is capped at **6000** entries. +- The hex dump shows **4096** bytes. +- AAB files can be selected, but real support depends on the engine. + +## 9. Editor + +`com.reandroid.apkeditor.javafx.CodeEditor` is an editable `TextArea` plus a +read-only line-number gutter. + +- Gutter scrolls with the text through a listener (not a property binding). +- Status line: `Ln/Col`, spaces, encoding (`UTF-8`), and language. +- `toggleGutter()` hides/shows the gutter. +- No syntax highlighting. +- Edits are **in-memory only**; saving back to the APK/project is not implemented. + +## 10. Themes + +`FxTheme.NAMES`: + +| Theme | Character | +| --- | --- | +| `Futuristic` | Deep blue background, cyan primary, purple accents (default). | +| `Cyberpunk` | Purple background, magenta primary, cyan accents. | +| `Dark` | Neutral dark gray, sky-blue primary. | +| `Midnight` | Very dark blue, soft blue accents. | + +`src/javafx/resources/styles/app.css` is a **template**. It uses tokens such as +`-fx-apx-bg`, `-fx-apx-surface`, `-fx-apx-primary`, `-fx-apx-border`, +`-fx-apx-text`, etc. `FxTheme.renderCss(name, template)` replaces them with the +palette and `MainView` injects the result as a `data:text/css;base64,...` +stylesheet. + +Use `derive(, )` in the template for glows and translucent variants +so all four themes stay coherent. + +## 11. Language + +- Default language: **Spanish**. +- Strings: `src/main/resources/i18n/messages.properties` (English) and + `messages_es.properties` (Spanish). +- Every key must exist in both files; `I18n` returns the raw key on a miss. +- The top bar selector updates `AppState`; `MainView` clears caches and rebuilds. + +## 12. Icons + +`com.reandroid.apkeditor.javafx.Icons` stores SVG path data (no image assets): + +- `Icons.node(name, size, styleClass)` for regular icons. +- `Icons.holder(...)` for stacked/contained icons. +- `Icons.logo(...)` for the gradient brand mark. +- Used by the sidebar, tabs, cards, tree, log levels, and progress steps. + +## 13. Shortcuts + +| Shortcut | Action | +| --- | --- | +| `Ctrl+O` | Open APK/ZIP. | +| `F5` | Go to Decompile and run decode. | +| `Ctrl+1` | Go to Decompile. | +| `Ctrl+,` | Open Settings. | + +## 14. Operations + +Operations are registered once in `operations/OperationCatalog.java`: + +| ID | Name | Options | +| --- | --- | --- | +| `decode` | Decode APK | `DecompileOptions` | +| `build` | Build APK | `BuildOptions` | +| `merge` | Merge APK | `MergerOptions` | +| `refactor` | Refactor | `RefactorOptions` | +| `protect` | Protect | `ProtectorOptions` | +| `info` | APK Info | `InfoOptions` | + +`OperationFxView` builds the form for the selected operation ID, resolves input/output +paths, and delegates execution to `OperationService`. Progress steps are +`Preparing -> Running -> Finishing`; logs and errors are appended to the shared +`LogService`. + +## 15. Limitations + +- Editor edits are in-memory only. +- Theme, language, and session are not persisted across restarts. +- No syntax highlighting. +- ZIP/tree/hex caps listed in section 8. +- AAB support depends on the engine. +- No unit-test framework is configured; verify with `javafxClasses`, `guiFatJar`, + `javafxSmoke`, or `bash proot-run.sh --smoke-test`. + +## 16. Troubleshooting + +- **JavaFX dependency resolution fails**: run `sh gradlew javafxRuntime` with + network access once; later builds can use `--offline`. +- **`Permission denied` on `./gradlew`**: use `sh gradlew`. +- **`Unable to open DISPLAY`**: start Xvfb/VNC or use `bash proot-run.sh`. +- **`guiFatJar` succeeds but the jar fails to start**: add + `--module-path build/javafx --add-modules javafx.controls`. diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar old mode 100755 new mode 100644 diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties old mode 100755 new mode 100644 index 8a91a6b6..f5da588c --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https://services.gradle.org/distributions/gradle-6.5.1-all.zip +distributionUrl=https://services.gradle.org/distributions/gradle-8.5-all.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/gradlew.bat b/gradlew.bat old mode 100755 new mode 100644 diff --git a/libs/ARSCLib.jar b/libs/ARSCLib.jar index 1346ed6a..babcc09a 100644 Binary files a/libs/ARSCLib.jar and b/libs/ARSCLib.jar differ diff --git a/proot-run.sh b/proot-run.sh new file mode 100644 index 00000000..58929b16 --- /dev/null +++ b/proot-run.sh @@ -0,0 +1,144 @@ +#!/usr/bin/env bash +# +# Ejecuta la interfaz JavaFX nativa dentro de Debian (proot) para pruebas +# en Termux. El binario de JavaFX es glibc, por eso no se puede ejecutar +# directamente sobre la libc de Termux. +# +# Variables opcionales: +# JFX_BASE Directorio con el rootfs de Debian. Por defecto ~/javaFX +# ROOTFS Rootfs de Debian. Por defecto $JFX_BASE/debian-trixie +# JAVA_BIN Java dentro del rootfs. Por defecto java-21-openjdk-arm64 +# START_XVFB 1 (por defecto) inicia Xvfb; 0 usa el DISPLAY existente +# DISPLAY_NUM Display a usar, por ejemplo :99 +# SKIP_BUILD 1 omite la compilación previa con Gradle +# +# Ejemplos: +# bash proot-run.sh +# bash proot-run.sh --smoke-test +# START_XVFB=0 DISPLAY_NUM=:0 bash proot-run.sh + +set -euo pipefail + +SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" +JFX_BASE="${JFX_BASE:-$HOME/javaFX}" +ROOTFS="${ROOTFS:-$JFX_BASE/debian-trixie}" +JAVA_BIN="${JAVA_BIN:-/usr/lib/jvm/java-21-openjdk-arm64/bin/java}" +START_XVFB="${START_XVFB:-1}" +DISPLAY_NUM="${DISPLAY_NUM:-}" +SKIP_BUILD="${SKIP_BUILD:-0}" +TMPD="${TMPDIR:-/tmp}" +XVFB_PID="" + +cleanup() { + if [ -n "$XVFB_PID" ] && kill -0 "$XVFB_PID" 2>/dev/null; then + kill "$XVFB_PID" 2>/dev/null || true + wait "$XVFB_PID" 2>/dev/null || true + fi +} +trap cleanup EXIT INT TERM + +die() { + echo "[proot-run] $*" >&2 + exit 1 +} + +[ -d "$ROOTFS" ] || die "No existe el rootfs: $ROOTFS" +[ -x "$ROOTFS$JAVA_BIN" ] || die "No existe el Java del rootfs: $ROOTFS$JAVA_BIN" +command -v proot >/dev/null 2>&1 || die "Instala proot en Termux" + +if [ "$SKIP_BUILD" != "1" ]; then + echo "[proot-run] Compilando source set JavaFX con Gradle..." + ( + cd "$SCRIPT_DIR" + sh gradlew javafxClasses javafxRuntime --no-daemon --console=plain + ) +fi + +[ -d "$SCRIPT_DIR/build/javafx" ] || die \ + "No existe build/javafx. Ejecuta sin SKIP_BUILD=1 para descargar JavaFX." + +if [ "$START_XVFB" = "1" ]; then + command -v Xvfb >/dev/null 2>&1 || die "Instala xorg-server-xvfb o usa START_XVFB=0" + + if [ -z "$DISPLAY_NUM" ]; then + for n in $(seq 99 110); do + if [ ! -e "/tmp/.X${n}-lock" ] \ + && [ ! -e "$TMPD/.X${n}-lock" ] \ + && [ ! -e "$TMPD/.X11-unix/X${n}" ]; then + DISPLAY_NUM=":$n" + break + fi + done + fi + [ -n "$DISPLAY_NUM" ] || die "No se encontró un display libre para Xvfb" + + mkdir -p "$TMPD/.X11-unix" + echo "[proot-run] Iniciando Xvfb en $DISPLAY_NUM" + Xvfb "$DISPLAY_NUM" -screen 0 1280x800x24 -ac -nolisten tcp \ + >"$TMPD/xvfb.log" 2>&1 & + XVFB_PID=$! + + N="${DISPLAY_NUM#:}" + for _ in $(seq 1 50); do + [ -S "$TMPD/.X11-unix/X${N}" ] && break + kill -0 "$XVFB_PID" 2>/dev/null || break + sleep 0.1 + done + + if ! kill -0 "$XVFB_PID" 2>/dev/null || [ ! -S "$TMPD/.X11-unix/X${N}" ]; then + cat "$TMPD/xvfb.log" >&2 || true + die "Xvfb no pudo iniciar en $DISPLAY_NUM" + fi +else + DISPLAY_NUM="${DISPLAY_NUM:-${DISPLAY:-:0}}" + echo "[proot-run] Usando display existente $DISPLAY_NUM" +fi + +BINDS=( + -b /dev + -b /proc + -b /sys + -b "$SCRIPT_DIR:/project" + -b "$SCRIPT_DIR/build/javafx:/jfx-modules" +) +if [ -d "$TMPD/.X11-unix" ]; then + BINDS+=(-b "$TMPD/.X11-unix:/tmp/.X11-unix") +fi +if [ -f "$HOME/.Xauthority" ]; then + BINDS+=(-b "$HOME/.Xauthority:/root/.Xauthority") +fi + +PROOT_ENV=( + HOME=/root + PATH=/usr/sbin:/usr/bin:/sbin:/bin + LANG=C.UTF-8 + DISPLAY="$DISPLAY_NUM" +) +if [ -f "$HOME/.Xauthority" ]; then + PROOT_ENV+=(XAUTHORITY=/root/.Xauthority) +fi + +CLASSPATH_APP="/project/build/classes/java/javafx:/project/build/resources/javafx" +CLASSPATH_APP="$CLASSPATH_APP:/project/build/classes/java/main:/project/build/resources/main" +CLASSPATH_APP="$CLASSPATH_APP:/project/libs/*" + +echo "[proot-run] Ejecutando JavaFX (display $DISPLAY_NUM)" +echo "[proot-run] Argumentos: $*" + +set +e +env -u LD_PRELOAD -u LD_LIBRARY_PATH \ + proot --link2symlink -0 -r "$ROOTFS" \ + "${BINDS[@]}" -w /project \ + /usr/bin/env -i "${PROOT_ENV[@]}" \ + "$JAVA_BIN" \ + --module-path /jfx-modules \ + --add-modules javafx.controls \ + -Dprism.order=sw \ + -cp "$CLASSPATH_APP" \ + com.reandroid.apkeditor.javafx.JavaFXApplication "$@" +STATUS=$? +set -e + +cleanup +trap - EXIT INT TERM +exit "$STATUS" diff --git a/settings.gradle b/settings.gradle old mode 100755 new mode 100644 diff --git a/src/javafx/java/com/reandroid/apkeditor/javafx/ApplicationInfoCard.java b/src/javafx/java/com/reandroid/apkeditor/javafx/ApplicationInfoCard.java new file mode 100644 index 00000000..dc976f30 --- /dev/null +++ b/src/javafx/java/com/reandroid/apkeditor/javafx/ApplicationInfoCard.java @@ -0,0 +1,50 @@ +package com.reandroid.apkeditor.javafx; + +import com.reandroid.apkeditor.gui.i18n.I18n; +import com.reandroid.apkeditor.gui.model.ApkAnalysis; +import javafx.scene.control.Label; + +import java.util.LinkedHashMap; +import java.util.Map; + +/** + * Información de la aplicación cargada (manifest). + */ +public final class ApplicationInfoCard extends Card { + private final Map values = new LinkedHashMap<>(); + + public ApplicationInfoCard() { + super(I18n.t("applicationInfo.title"), I18n.t("applicationInfo.subtitle"), + Icons.node("android", 16, "icon-primary")); + addRow("applicationInfo.name"); + addRow("applicationInfo.version"); + addRow("applicationInfo.package"); + addRow("applicationInfo.minSdk"); + addRow("applicationInfo.targetSdk"); + setAnalysis(ApkAnalysis.demo()); + } + + private void addRow(String key) { + Label value = Fx.label("—", "value", "small"); + values.put(key, value); + getBody().getChildren().add(Fx.dataRow(I18n.t(key), value)); + } + + public void setAnalysis(ApkAnalysis analysis) { + if (analysis == null) { + return; + } + set("applicationInfo.name", analysis.getAppName()); + set("applicationInfo.version", analysis.getVersionName()); + set("applicationInfo.package", analysis.getPackageName()); + set("applicationInfo.minSdk", String.valueOf(analysis.getMinSdk())); + set("applicationInfo.targetSdk", String.valueOf(analysis.getTargetSdk())); + } + + private void set(String key, String value) { + Label label = values.get(key); + if (label != null) { + label.setText(value == null || value.isBlank() ? "—" : value); + } + } +} diff --git a/src/javafx/java/com/reandroid/apkeditor/javafx/Card.java b/src/javafx/java/com/reandroid/apkeditor/javafx/Card.java new file mode 100644 index 00000000..841b8422 --- /dev/null +++ b/src/javafx/java/com/reandroid/apkeditor/javafx/Card.java @@ -0,0 +1,60 @@ +package com.reandroid.apkeditor.javafx; + +import javafx.geometry.Pos; +import javafx.scene.Node; +import javafx.scene.control.Label; +import javafx.scene.layout.HBox; +import javafx.scene.layout.Priority; +import javafx.scene.layout.VBox; + +/** + * Tarjeta reutilizable con título, subtítulo y cuerpo. + */ +public class Card extends VBox { + private final VBox body = new VBox(6); + private final HBox header; + private final Label titleLabel; + private final Label subtitleLabel; + + public Card(String title, String subtitle) { + this(title, subtitle, null); + } + + public Card(String title, String subtitle, Node icon) { + getStyleClass().add("card"); + titleLabel = Fx.label(title, "card-title"); + subtitleLabel = Fx.label(subtitle, "card-subtitle"); + HBox titleRow = new HBox(8); + titleRow.setAlignment(Pos.CENTER_LEFT); + if (icon != null) { + titleRow.getChildren().add(icon); + } + titleRow.getChildren().add(titleLabel); + VBox titles = new VBox(2, titleRow, subtitleLabel); + header = new HBox(10, titles, Fx.hspacer()); + header.setAlignment(Pos.CENTER_LEFT); + body.getStyleClass().add("card-body"); + VBox.setVgrow(body, Priority.ALWAYS); + getChildren().addAll(header, body); + } + + public VBox getBody() { + return body; + } + + public HBox getHeader() { + return header; + } + + public void addHeaderAction(Node node) { + header.getChildren().add(node); + } + + public void setSubtitle(String subtitle) { + subtitleLabel.setText(subtitle); + } + + public void setTitle(String title) { + titleLabel.setText(title); + } +} diff --git a/src/javafx/java/com/reandroid/apkeditor/javafx/CodeEditor.java b/src/javafx/java/com/reandroid/apkeditor/javafx/CodeEditor.java new file mode 100644 index 00000000..6b2af4d5 --- /dev/null +++ b/src/javafx/java/com/reandroid/apkeditor/javafx/CodeEditor.java @@ -0,0 +1,115 @@ +package com.reandroid.apkeditor.javafx; + +import javafx.geometry.Pos; +import javafx.scene.control.Label; +import javafx.scene.control.TextArea; +import javafx.scene.layout.HBox; +import javafx.scene.layout.Priority; +import javafx.scene.layout.VBox; + +/** + * Editor de código con numeración de líneas y barra de estado. + */ +public final class CodeEditor extends VBox { + private final TextArea area = new TextArea(); + private final TextArea gutter = new TextArea(); + private final Label position = Fx.label("Ln 1, Col 1", "editor-status-item"); + private final String language; + + public CodeEditor(String content, String language) { + this.language = language == null ? "text" : language; + getStyleClass().add("code-editor"); + + area.setText(content == null ? "" : content); + area.getStyleClass().add("editor-area"); + area.setWrapText(false); + + gutter.setEditable(false); + gutter.setFocusTraversable(false); + gutter.setMouseTransparent(true); + gutter.setWrapText(false); + gutter.getStyleClass().add("line-gutter"); + area.scrollTopProperty().addListener( + (observable, oldValue, newValue) -> gutter.setScrollTop(newValue.doubleValue())); + + area.textProperty().addListener((observable, oldValue, newValue) -> updateGutter()); + area.caretPositionProperty().addListener((observable, oldValue, newValue) -> updateStatus()); + + HBox row = new HBox(gutter, area); + row.getStyleClass().add("code-row"); + HBox.setHgrow(area, Priority.ALWAYS); + VBox.setVgrow(row, Priority.ALWAYS); + + Label spaces = Fx.label("Spaces: 4", "editor-status-item"); + Label encoding = Fx.label("UTF-8", "editor-status-item"); + Label mode = Fx.label(this.language, "editor-status-item", "text-primary"); + HBox status = new HBox(10, position, Fx.label("│", "tiny", "muted"), + spaces, Fx.label("│", "tiny", "muted"), + encoding, Fx.label("│", "tiny", "muted"), + mode, Fx.hspacer()); + status.getStyleClass().add("editor-status"); + status.setAlignment(Pos.CENTER_LEFT); + + getChildren().addAll(row, status); + updateGutter(); + updateStatus(); + } + + public TextArea getArea() { + return area; + } + + public String getLanguage() { + return language; + } + + public void requestEditorFocus() { + area.requestFocus(); + } + + public void toggleGutter() { + boolean visible = !gutter.isVisible(); + gutter.setVisible(visible); + gutter.setManaged(visible); + } + + private void updateGutter() { + String text = area.getText() == null ? "" : area.getText(); + int lines = 1; + for (int i = 0; i < text.length(); i++) { + if (text.charAt(i) == '\n') { + lines++; + } + } + StringBuilder builder = new StringBuilder(); + for (int i = 1; i <= lines; i++) { + builder.append(i); + if (i < lines) { + builder.append('\n'); + } + } + gutter.setText(builder.toString()); + gutter.setScrollTop(area.getScrollTop()); + int digits = String.valueOf(lines).length(); + double width = 22 + Math.max(2, digits) * 8; + gutter.setPrefWidth(width); + gutter.setMinWidth(width); + gutter.setMaxWidth(width); + } + + private void updateStatus() { + String text = area.getText() == null ? "" : area.getText(); + int positionValue = Math.min(area.getCaretPosition(), text.length()); + int line = 1; + int column = 1; + for (int i = 0; i < positionValue; i++) { + if (text.charAt(i) == '\n') { + line++; + column = 1; + } else { + column++; + } + } + position.setText("Ln " + line + ", Col " + column); + } +} diff --git a/src/javafx/java/com/reandroid/apkeditor/javafx/Fx.java b/src/javafx/java/com/reandroid/apkeditor/javafx/Fx.java new file mode 100644 index 00000000..978bfbe2 --- /dev/null +++ b/src/javafx/java/com/reandroid/apkeditor/javafx/Fx.java @@ -0,0 +1,108 @@ +package com.reandroid.apkeditor.javafx; + +import javafx.application.Platform; +import javafx.geometry.Pos; +import javafx.scene.Node; +import javafx.scene.control.Button; +import javafx.scene.control.Label; +import javafx.scene.control.Tooltip; +import javafx.scene.layout.HBox; +import javafx.scene.layout.Priority; +import javafx.scene.layout.Region; +import javafx.scene.layout.VBox; + +/** + * Utilidades de construcción de la interfaz JavaFX. + */ +public final class Fx { + private Fx() {} + + public static void run(Runnable action) { + if (Platform.isFxApplicationThread()) { + action.run(); + } else { + Platform.runLater(action); + } + } + + public static Label label(String text, String... classes) { + Label label = new Label(text == null ? "" : text); + if (classes != null) { + label.getStyleClass().addAll(classes); + } + return label; + } + + public static Button labelButton(String text, String... classes) { + Button button = new Button(text == null ? "" : text); + if (classes != null) { + button.getStyleClass().addAll(classes); + } + return button; + } + + public static Button iconButton(Node graphic, String tooltip, String... classes) { + Button button = new Button(); + button.setGraphic(graphic); + if (classes != null) { + button.getStyleClass().addAll(classes); + } + if (tooltip != null) { + button.setTooltip(new Tooltip(tooltip)); + } + return button; + } + + public static Button iconTextButton(String text, Node graphic, String... classes) { + Button button = labelButton(text, classes); + button.setGraphic(graphic); + button.setGraphicTextGap(8); + return button; + } + + public static Region hspacer() { + Region region = new Region(); + HBox.setHgrow(region, Priority.ALWAYS); + return region; + } + + public static Region vspacer() { + Region region = new Region(); + VBox.setVgrow(region, Priority.ALWAYS); + return region; + } + + public static HBox dataRow(String key, Label value) { + Label label = label(key, "muted", "small"); + value.getStyleClass().addAll("value", "small"); + HBox row = new HBox(8, label, hspacer(), value); + row.setAlignment(Pos.CENTER_LEFT); + row.getStyleClass().add("data-row"); + return row; + } + + public static HBox dataRow(String key, Label value, Node icon) { + HBox row = dataRow(key, value); + if (icon != null) { + row.getChildren().add(0, icon); + } + return row; + } + + public static HBox titledRow(String title, Label subtitle, Region spacer) { + Label titleLabel = label(title, "card-title"); + VBox titles = new VBox(2, titleLabel, subtitle); + HBox header = new HBox(10, titles, spacer); + header.setAlignment(Pos.CENTER_LEFT); + return header; + } + + public static void setAllVisible(boolean visible, javafx.scene.Node... nodes) { + for (javafx.scene.Node node : nodes) { + if (node != null) { + node.setVisible(visible); + node.setManaged(visible); + } + } + } +} diff --git a/src/javafx/java/com/reandroid/apkeditor/javafx/FxTheme.java b/src/javafx/java/com/reandroid/apkeditor/javafx/FxTheme.java new file mode 100644 index 00000000..5cf7cdce --- /dev/null +++ b/src/javafx/java/com/reandroid/apkeditor/javafx/FxTheme.java @@ -0,0 +1,219 @@ +package com.reandroid.apkeditor.javafx; + +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Paletas de la interfaz JavaFX. La hoja de estilos es una plantilla con + * tokens -fx-apx-* que se sustituyen por colores concretos en cada tema. + * Cada paleta define la identidad visual de un tema. + */ +public final class FxTheme { + private FxTheme() {} + + public static final String[] NAMES = {"Futuristic", "Cyberpunk", "Dark", "Midnight"}; + private static String current = "Futuristic"; + + public static String current() { + return current; + } + + public static String paletteStyle(String name) { + return paletteOf(name).toStyle(); + } + + /** + * Sustituye los tokens -fx-apx-* de la plantilla por los colores del tema. + */ + public static String renderCss(String name, String template) { + Map colors = paletteOf(name).toMap(); + Matcher matcher = Pattern.compile("-fx-apx-([a-z0-9-]+)").matcher(template); + StringBuilder builder = new StringBuilder(); + while (matcher.find()) { + String replacement = colors.get(matcher.group(1)); + matcher.appendReplacement(builder, + Matcher.quoteReplacement(replacement == null ? matcher.group(0) : replacement)); + } + matcher.appendTail(builder); + return builder.toString(); + } + + private static Palette paletteOf(String name) { + if (name == null || name.isBlank()) { + name = "Futuristic"; + } + current = name; + return switch (name) { + case "Cyberpunk" -> cyberpunk(); + case "Dark" -> dark(); + case "Midnight" -> midnight(); + default -> futuristic(); + }; + } + + private static Palette futuristic() { + Palette p = new Palette(); + p.bg = "#020B18"; + p.bg2 = "#041426"; + p.surface = "#06182A"; + p.surface2 = "#071D32"; + p.panel = "#06182A"; + p.hover = "#0B2945"; + p.border = "#123653"; + p.borderActive = "#087FBC"; + p.primary = "#00BFFF"; + p.primaryBright = "#00D5FF"; + p.cyan = "#00E5FF"; + p.purple = "#A855F7"; + p.success = "#00E6A0"; + p.warning = "#FFB547"; + p.error = "#FF4668"; + p.text = "#E6F4FF"; + p.text2 = "#9CB4CC"; + p.muted = "#64829F"; + p.navActive = "#07304C"; + return p; + } + + private static Palette cyberpunk() { + Palette p = new Palette(); + p.bg = "#090012"; + p.bg2 = "#12001E"; + p.surface = "#1A0526"; + p.surface2 = "#240833"; + p.panel = "#1A0526"; + p.hover = "#351047"; + p.border = "#4A1B6B"; + p.borderActive = "#B52BFF"; + p.primary = "#FF2FB3"; + p.primaryBright = "#FF6FD8"; + p.cyan = "#00E5FF"; + p.purple = "#A855F7"; + p.success = "#00FF9C"; + p.warning = "#FFC857"; + p.error = "#FF3355"; + p.text = "#FFE8FA"; + p.text2 = "#C9A0DC"; + p.muted = "#8A6A9E"; + p.navActive = "#3B0D58"; + return p; + } + + private static Palette dark() { + Palette p = new Palette(); + p.bg = "#0B0F14"; + p.bg2 = "#0E131A"; + p.surface = "#121820"; + p.surface2 = "#171F29"; + p.panel = "#121820"; + p.hover = "#1E2A37"; + p.border = "#2A3A4A"; + p.borderActive = "#2B7DA8"; + p.primary = "#38BDF8"; + p.primaryBright = "#7DD3FC"; + p.cyan = "#22D3EE"; + p.purple = "#A78BFA"; + p.success = "#34D399"; + p.warning = "#FBBF24"; + p.error = "#F87171"; + p.text = "#E5EDF5"; + p.text2 = "#9FB0C0"; + p.muted = "#6B7C8F"; + p.navActive = "#1B3648"; + return p; + } + + private static Palette midnight() { + Palette p = new Palette(); + p.bg = "#030616"; + p.bg2 = "#050B22"; + p.surface = "#08102B"; + p.surface2 = "#0B1638"; + p.panel = "#08102B"; + p.hover = "#132155"; + p.border = "#1E3270"; + p.borderActive = "#4C7DFF"; + p.primary = "#4C7DFF"; + p.primaryBright = "#6C9BFF"; + p.cyan = "#29D3FF"; + p.purple = "#8B5CF6"; + p.success = "#2CE0B0"; + p.warning = "#FFC24B"; + p.error = "#FF5D7A"; + p.text = "#E7EEFF"; + p.text2 = "#A0B0D0"; + p.muted = "#65779E"; + p.navActive = "#172A63"; + return p; + } + + private static final class Palette { + String bg; + String bg2; + String surface; + String surface2; + String panel; + String hover; + String border; + String borderActive; + String primary; + String primaryBright; + String cyan; + String purple; + String success; + String warning; + String error; + String text; + String text2; + String muted; + String navActive; + + Map toMap() { + Map map = new LinkedHashMap<>(); + map.put("bg", bg); + map.put("bg2", bg2); + map.put("surface", surface); + map.put("surface2", surface2); + map.put("panel", panel); + map.put("hover", hover); + map.put("border", border); + map.put("border-active", borderActive); + map.put("primary", primary); + map.put("primary-bright", primaryBright); + map.put("cyan", cyan); + map.put("purple", purple); + map.put("success", success); + map.put("warning", warning); + map.put("error", error); + map.put("text", text); + map.put("text2", text2); + map.put("muted", muted); + map.put("nav-active", navActive); + return map; + } + + String toStyle() { + return "-fx-apx-bg: " + bg + ";" + + "-fx-apx-bg2: " + bg2 + ";" + + "-fx-apx-surface: " + surface + ";" + + "-fx-apx-surface2: " + surface2 + ";" + + "-fx-apx-panel: " + panel + ";" + + "-fx-apx-hover: " + hover + ";" + + "-fx-apx-border: " + border + ";" + + "-fx-apx-border-active: " + borderActive + ";" + + "-fx-apx-primary: " + primary + ";" + + "-fx-apx-primary-bright: " + primaryBright + ";" + + "-fx-apx-cyan: " + cyan + ";" + + "-fx-apx-purple: " + purple + ";" + + "-fx-apx-success: " + success + ";" + + "-fx-apx-warning: " + warning + ";" + + "-fx-apx-error: " + error + ";" + + "-fx-apx-text: " + text + ";" + + "-fx-apx-text2: " + text2 + ";" + + "-fx-apx-muted: " + muted + ";" + + "-fx-apx-nav-active: " + navActive + ";"; + } + } +} diff --git a/src/javafx/java/com/reandroid/apkeditor/javafx/Icons.java b/src/javafx/java/com/reandroid/apkeditor/javafx/Icons.java new file mode 100644 index 00000000..d81baf61 --- /dev/null +++ b/src/javafx/java/com/reandroid/apkeditor/javafx/Icons.java @@ -0,0 +1,120 @@ +package com.reandroid.apkeditor.javafx; + +import javafx.scene.Node; +import javafx.scene.layout.StackPane; +import javafx.scene.paint.Color; +import javafx.scene.paint.CycleMethod; +import javafx.scene.paint.LinearGradient; +import javafx.scene.paint.Stop; +import javafx.scene.shape.SVGPath; +import javafx.scene.shape.StrokeLineCap; +import javafx.scene.shape.StrokeLineJoin; + +import java.util.LinkedHashMap; +import java.util.Map; + +/** + * Iconos vectoriales de la interfaz (trazos estilo lineal, 24x24). + */ +public final class Icons { + private Icons() {} + + private static final Map PATHS = new LinkedHashMap<>(); + + static { + PATHS.put("home", "M3 11.5 12 4l9 7.5V20a1 1 0 0 1-1 1h-5v-6H9v6H4a1 1 0 0 1-1-1z"); + PATHS.put("decompile", "M12 3v11m0 0 4-4m-4 4-4-4M4 17v3h16v-3"); + PATHS.put("analyze", "M11 4a7 7 0 1 0 0 14 7 7 0 0 0 0-14zM16 16l5 5"); + PATHS.put("smali", "M8 3h8a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2zm3 3v2m2-2v2M10 12h4m-4 3h4"); + PATHS.put("build", "m12 2 8 4.5v9L12 20l-8-4.5v-9zM4 6.5l8 4.5 8-4.5M12 11v9"); + PATHS.put("merge", "M6 3v6a3 3 0 0 0 3 3h6a3 3 0 0 1 3 3v6M6 21V9M18 3v6"); + PATHS.put("refactor", "M16 3h5v5M4 20 21 3M21 16v5h-5M15 15l6 6M4 4l4 4"); + PATHS.put("protect", "M12 3 4 6v6c0 4.4 3.4 8.2 8 9 4.6-.8 8-4.6 8-9V6z"); + PATHS.put("info", "M12 3a9 9 0 1 0 0 18 9 9 0 0 0 0-18zM12 16v-5M12 8h.01"); + PATHS.put("settings", "M12 15a3 3 0 1 0 0-6 3 3 0 0 0 0 6zM19.4 15a1.7 1.7 0 0 0 .3 1.9l.1.1a2 2 0 1 1-2.8 2.8l-.1-.1a1.7 1.7 0 0 0-1.9-.3 1.7 1.7 0 0 0-1 1.5V21a2 2 0 1 1-4 0v-.1a1.7 1.7 0 0 0-1-1.6 1.7 1.7 0 0 0-1.9.3l-.1.1a2 2 0 1 1-2.8-2.8l.1-.1a1.7 1.7 0 0 0 .3-1.9 1.7 1.7 0 0 0-1.5-1H3a2 2 0 1 1 0-4h.1a1.7 1.7 0 0 0 1.6-1 1.7 1.7 0 0 0-.3-1.9l-.1-.1a2 2 0 1 1 2.8-2.8l.1.1a1.7 1.7 0 0 0 1.9.3H9a1.7 1.7 0 0 0 1-1.5V3a2 2 0 1 1 4 0v.1a1.7 1.7 0 0 0 1 1.6 1.7 1.7 0 0 0 1.9-.3l.1-.1a2 2 0 1 1 2.8 2.8l-.1.1a1.7 1.7 0 0 0-.3 1.9V9a1.7 1.7 0 0 0 1.5 1H21a2 2 0 1 1 0 4h-.1a1.7 1.7 0 0 0-1.5 1z"); + PATHS.put("search", "M11 4a7 7 0 1 0 0 14 7 7 0 0 0 0-14zM16 16l5 5"); + PATHS.put("close", "M6 6l12 12M18 6 6 18"); + PATHS.put("file", "M14 3H6a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V9zM14 3v6h6"); + PATHS.put("folder", "M3 7a2 2 0 0 1 2-2h4l2 2h8a2 2 0 0 1 2 2v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"); + PATHS.put("folder-open", "M3 7h6l2 2h10l-3 10H3z"); + PATHS.put("code", "m8 8-4 4 4 4M16 8l4 4-4 4"); + PATHS.put("xml", "M8 4H4v16h4M16 4h4v16h-4M9 9l3 3-3 3"); + PATHS.put("image", "M3 5h18v14H3zM8 11a2 2 0 1 0 0-4 2 2 0 0 0 0 4zM21 15l-5-5L5 21"); + PATHS.put("archive", "M3 4h18v4H3zM5 8v12h14V8M10 12h4"); + PATHS.put("log", "M8 6h13M8 12h13M8 18h13M3 6h.01M3 12h.01M3 18h.01"); + PATHS.put("trash", "M4 6h16M9 6V4h6v2M6 6l1 14h10l1-14"); + PATHS.put("export", "M12 3v12m0 0 4-4m-4 4-4-4M4 21h16"); + PATHS.put("chart", "M4 20V10M10 20V4M16 20v-7M22 20H2"); + PATHS.put("android", "M7 10a5 5 0 0 1 10 0v8H7zM9 7 7 4M15 7l2-3M9.5 13h.01M14.5 13h.01"); + PATHS.put("check-circle", "M12 3a9 9 0 1 0 0 18 9 9 0 0 0 0-18zM8.5 12.5l2.5 2.5 4.5-5"); + PATHS.put("circle", "M12 3a9 9 0 1 0 0 18 9 9 0 0 0 0-18z"); + PATHS.put("chevron-down", "m6 9 6 6 6-6"); + PATHS.put("chevron-right", "m9 6 6 6-6 6"); + PATHS.put("sun", "M12 8a4 4 0 1 0 0 8 4 4 0 0 0 0-8zM12 2v2M12 20v2M2 12h2M20 12h2M4.9 4.9l1.4 1.4M17.7 17.7l1.4 1.4M19.1 4.9l-1.4 1.4M6.3 17.7l-1.4 1.4"); + PATHS.put("globe", "M12 3a9 9 0 1 0 0 18 9 9 0 0 0 0-18zM3 12h18M12 3c2.5 2.5 4 5.5 4 9s-1.5 6.5-4 9c-2.5-2.5-4-5.5-4-9s1.5-6.5 4-9z"); + PATHS.put("minimize", "M5 12h14"); + PATHS.put("maximize", "M5 5h14v14H5z"); + PATHS.put("copy", "M9 9h10v10H9zM5 15H3V3h12v2"); + PATHS.put("layout", "M3 4h18v16H3zM9 4v16M9 10h12"); + PATHS.put("key", "M14.5 9.5a4 4 0 1 1-4 4L4 20H2v-3l6.5-6.5a4 4 0 0 1 6-1z"); + PATHS.put("bookmark", "M6 3h12v18l-6-4-6 4z"); + PATHS.put("clock", "M12 3a9 9 0 1 0 0 18 9 9 0 0 0 0-18zM12 7v5l3 2"); + PATHS.put("warning", "M12 3 2 21h20zM12 9v5M12 17h.01"); + PATHS.put("error", "M12 3a9 9 0 1 0 0 18 9 9 0 0 0 0-18zM9 9l6 6M15 9l-6 6"); + PATHS.put("cpu", "M5 5h14v14H5zM9 9h6v6H9zM9 2v3M15 2v3M9 19v3M15 19v3M2 9h3M2 15h3M19 9h3M19 15h3"); + PATHS.put("dot", "M12 10a2 2 0 1 0 0 4 2 2 0 0 0 0-4z"); + PATHS.put("grid", "M4 4h7v7H4zM13 4h7v7h-7zM4 13h7v7H4zM13 13h7v7h-7z"); + } + + public static SVGPath path(String name) { + SVGPath path = new SVGPath(); + path.setContent(PATHS.getOrDefault(name, PATHS.get("dot"))); + path.getStyleClass().add("icon"); + path.setFill(Color.TRANSPARENT); + path.setStroke(Color.web("#9CB4CC")); + path.setStrokeWidth(2); + path.setStrokeLineCap(StrokeLineCap.ROUND); + path.setStrokeLineJoin(StrokeLineJoin.ROUND); + return path; + } + + public static Node node(String name, double size, String... classes) { + return holder(path(name), size, classes); + } + + public static StackPane holder(SVGPath path, double size, String... classes) { + if (classes != null) { + path.getStyleClass().addAll(classes); + } + StackPane holder = new StackPane(path); + holder.getStyleClass().add("icon-holder"); + holder.setMinSize(size, size); + holder.setPrefSize(size, size); + holder.setMaxSize(size, size); + double scale = size / 24.0; + path.setScaleX(scale); + path.setScaleY(scale); + return holder; + } + + public static void setContent(SVGPath path, String name) { + path.setContent(PATHS.getOrDefault(name, PATHS.get("dot"))); + } + + public static Node logo(double size) { + SVGPath path = new SVGPath(); + path.setContent("M12 2.5 4 21M12 2.5 20 21M7.2 14.5h9.6"); + path.setFill(Color.TRANSPARENT); + path.setStroke(new LinearGradient(0, 0, 1, 1, true, CycleMethod.NO_CYCLE, + new Stop(0, Color.web("#9BF4FF")), + new Stop(0.42, Color.web("#00C8FF")), + new Stop(1, Color.web("#2F6BFF")))); + path.setStrokeWidth(2.4); + path.setStrokeLineCap(StrokeLineCap.ROUND); + path.setStrokeLineJoin(StrokeLineJoin.ROUND); + path.getStyleClass().add("logo-mark"); + StackPane holder = holder(path, size); + holder.getStyleClass().add("logo-holder"); + return holder; + } +} diff --git a/src/javafx/java/com/reandroid/apkeditor/javafx/JavaFXApplication.java b/src/javafx/java/com/reandroid/apkeditor/javafx/JavaFXApplication.java new file mode 100644 index 00000000..d5ddc465 --- /dev/null +++ b/src/javafx/java/com/reandroid/apkeditor/javafx/JavaFXApplication.java @@ -0,0 +1,63 @@ +package com.reandroid.apkeditor.javafx; + +import javafx.animation.PauseTransition; +import javafx.application.Application; +import javafx.application.Platform; +import javafx.geometry.Rectangle2D; +import javafx.scene.Scene; +import javafx.stage.Screen; +import javafx.stage.Stage; +import javafx.stage.StageStyle; +import javafx.util.Duration; + +/** + * Punto de entrada de la interfaz nativa JavaFX. Reutiliza el modelo y los + * servicios del GUI Swing; no usa WebView ni carga HTML. + */ +public final class JavaFXApplication extends Application { + private boolean smokeTest; + + @Override + public void init() { + smokeTest = getParameters().getRaw().contains("--smoke-test"); + } + + @Override + public void start(Stage stage) { + MainView root = new MainView(stage); + Scene scene = new Scene(root); + + stage.initStyle(StageStyle.UNDECORATED); + stage.setTitle("APKEditor Futuristic - JavaFX"); + stage.setScene(scene); + stage.setMinWidth(1000); + stage.setMinHeight(700); + + Rectangle2D bounds = Screen.getPrimary().getVisualBounds(); + double width = Math.min(1536, Math.max(1000, bounds.getWidth() - 40)); + double height = Math.min(1024, Math.max(700, bounds.getHeight() - 70)); + stage.setWidth(width); + stage.setHeight(height); + stage.setX(bounds.getMinX() + (bounds.getWidth() - width) / 2); + stage.setY(bounds.getMinY() + (bounds.getHeight() - height) / 2); + stage.show(); + + System.out.println("JAVAFX_STARTED JavaFX=" + + System.getProperty("javafx.version") + + " Java=" + System.getProperty("java.version")); + System.out.println("JAVAFX_UI_READY"); + System.out.println("JAVAFX_BRIDGE=APKEditor JavaFX " + + System.getProperty("javafx.version", "?") + + " / Java " + System.getProperty("java.version", "?")); + + if (smokeTest) { + PauseTransition pause = new PauseTransition(Duration.millis(900)); + pause.setOnFinished(event -> Platform.exit()); + pause.play(); + } + } + + public static void main(String[] args) { + launch(args); + } +} diff --git a/src/javafx/java/com/reandroid/apkeditor/javafx/LogFxPanel.java b/src/javafx/java/com/reandroid/apkeditor/javafx/LogFxPanel.java new file mode 100644 index 00000000..83ef2734 --- /dev/null +++ b/src/javafx/java/com/reandroid/apkeditor/javafx/LogFxPanel.java @@ -0,0 +1,132 @@ +package com.reandroid.apkeditor.javafx; + +import com.reandroid.apkeditor.gui.i18n.I18n; +import com.reandroid.apkeditor.gui.model.LogEntry; +import com.reandroid.apkeditor.gui.service.LogService; +import javafx.scene.control.Button; +import javafx.scene.control.Label; +import javafx.scene.control.ListCell; +import javafx.scene.control.ListView; +import javafx.scene.layout.HBox; +import javafx.scene.layout.Priority; +import javafx.scene.layout.VBox; +import javafx.stage.FileChooser; + +import java.io.File; +import java.io.IOException; +import java.util.List; + +/** + * Panel de log en tiempo real. LogService entrega los eventos en el EDT de + * Swing, por eso cada actualización se reenvía al hilo de JavaFX. + */ +public final class LogFxPanel extends Card implements LogService.Listener { + private final LogService logService; + private final ListView list = new ListView<>(); + + public LogFxPanel(LogService logService) { + super(I18n.t("log.panelTitle"), I18n.t("log.panelSubtitle"), + Icons.node("log", 16, "icon-primary")); + this.logService = logService; + + list.getStyleClass().add("log-list"); + list.setMinWidth(0); + list.setPrefWidth(260); + list.setCellFactory(view -> new ListCell<>() { + @Override + protected void updateItem(LogEntry entry, boolean empty) { + super.updateItem(entry, empty); + getStyleClass().removeIf(style -> style.startsWith("log-")); + if (empty || entry == null) { + setText(null); + setGraphic(null); + return; + } + String level = entry.getLevel().name().toLowerCase(); + Label time = new Label("[" + entry.getTimestamp() + "]"); + time.getStyleClass().add("log-time"); + Label text = new Label(entry.getMessage()); + text.getStyleClass().add("log-text"); + HBox row = new HBox(8, time, + Icons.node(levelIcon(level), 12, levelColor(level)), text); + row.setAlignment(javafx.geometry.Pos.CENTER_LEFT); + setText(null); + setGraphic(row); + getStyleClass().add("log-" + level); + } + }); + VBox.setVgrow(list, Priority.ALWAYS); + + Button clear = Fx.iconButton(Icons.node("trash", 14, "icon-muted"), + I18n.t("log.clear"), "ghost-button"); + clear.setOnAction(event -> logService.clear()); + Button export = Fx.iconButton(Icons.node("export", 14, "icon-muted"), + I18n.t("log.export"), "ghost-button"); + export.setOnAction(event -> exportLogs()); + addHeaderAction(clear); + addHeaderAction(export); + + List entries = logService.getEntries(); + list.getItems().setAll(entries); + getBody().getChildren().add(list); + logService.addListener(this); + scrollToBottom(); + } + + @Override + public void onEntry(LogEntry entry) { + Fx.run(() -> { + list.getItems().add(entry); + scrollToBottom(); + }); + } + + @Override + public void onCleared() { + Fx.run(() -> list.getItems().clear()); + } + + private String levelIcon(String level) { + return switch (level) { + case "success" -> "check-circle"; + case "warning" -> "warning"; + case "error" -> "error"; + case "command" -> "code"; + case "debug" -> "dot"; + default -> "info"; + }; + } + + private String levelColor(String level) { + return switch (level) { + case "success" -> "icon-success"; + case "warning" -> "icon-warning"; + case "error" -> "icon-error"; + case "command" -> "icon-primary-bright"; + case "debug" -> "icon-muted"; + default -> "icon-primary"; + }; + } + + private void scrollToBottom() { + if (!list.getItems().isEmpty()) { + list.scrollTo(list.getItems().size() - 1); + } + } + + private void exportLogs() { + FileChooser chooser = new FileChooser(); + chooser.setTitle(I18n.t("log.export")); + chooser.setInitialFileName("apkeditor-log.txt"); + File file = chooser.showSaveDialog(getScene() == null ? null : getScene().getWindow()); + if (file == null) { + return; + } + try { + logService.export(file); + logService.success(I18n.t("log.exported", file.getAbsolutePath())); + } catch (IOException error) { + logService.error(I18n.t("log.exportError", error.getMessage())); + } + } +} diff --git a/src/javafx/java/com/reandroid/apkeditor/javafx/MainView.java b/src/javafx/java/com/reandroid/apkeditor/javafx/MainView.java new file mode 100644 index 00000000..3e9105ee --- /dev/null +++ b/src/javafx/java/com/reandroid/apkeditor/javafx/MainView.java @@ -0,0 +1,552 @@ +package com.reandroid.apkeditor.javafx; + +import com.reandroid.apkeditor.core.OperationRegistry; +import com.reandroid.apkeditor.gui.i18n.I18n; +import com.reandroid.apkeditor.gui.model.AppState; +import com.reandroid.apkeditor.gui.navigation.Route; +import com.reandroid.apkeditor.gui.service.ApkService; +import com.reandroid.apkeditor.gui.service.LogService; +import com.reandroid.apkeditor.gui.service.OperationService; +import com.reandroid.apkeditor.javafx.view.DashboardFxView; +import com.reandroid.apkeditor.javafx.view.InformationFxView; +import com.reandroid.apkeditor.javafx.view.OperationFxView; +import com.reandroid.apkeditor.javafx.view.SettingsFxView; +import com.reandroid.apkeditor.javafx.view.WorkspaceFxView; +import com.reandroid.apkeditor.operations.OperationCatalog; +import javafx.animation.Animation; +import javafx.animation.KeyFrame; +import javafx.animation.Timeline; +import javafx.geometry.Insets; +import javafx.geometry.Pos; +import javafx.geometry.Rectangle2D; +import javafx.scene.Node; +import javafx.scene.Scene; +import javafx.scene.control.Button; +import javafx.scene.control.ComboBox; +import javafx.scene.control.Label; +import javafx.scene.control.ProgressBar; +import javafx.scene.control.Tooltip; +import javafx.scene.input.KeyCode; +import javafx.scene.input.KeyEvent; +import javafx.scene.input.MouseEvent; +import javafx.scene.layout.BorderPane; +import javafx.scene.layout.HBox; +import javafx.scene.layout.Priority; +import javafx.scene.layout.Region; +import javafx.scene.layout.StackPane; +import javafx.scene.layout.VBox; +import javafx.scene.paint.Color; +import javafx.scene.shape.Circle; +import javafx.stage.FileChooser; +import javafx.stage.Screen; +import javafx.stage.Stage; +import javafx.util.Duration; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.util.Base64; +import java.util.EnumMap; +import java.util.HashMap; +import java.util.Map; +import java.util.function.Supplier; + +/** + * Ventana principal nativa de JavaFX. Compone la barra superior, el sidebar, + * las vistas y la barra de estado, reutilizando el modelo y los servicios del + * GUI Swing (AppState, ApkService, OperationService, LogService). + */ +public final class MainView extends BorderPane { + private static final String CSS_TEMPLATE = loadCssTemplate(); + + private final Stage stage; + private final AppState state = new AppState(); + private final LogService logs = LogService.get(); + private final OperationRegistry registry = OperationCatalog.create(); + private final OperationService operations = new OperationService(registry, logs); + + private final Map viewCache = new HashMap<>(); + private final Map navButtons = new EnumMap<>(Route.class); + + private StackPane content; + private WorkspaceFxView workspace; + private Button apkButton; + private Label apkName; + private ComboBox themeSelector; + private ComboBox languageSelector; + private Label statusLabel; + private Label memoryLabel; + private ProgressBar memoryBar; + private Route currentRoute = Route.DECOMPILE; + + private boolean rebuilding; + private boolean maximized; + private double restoredX; + private double restoredY; + private double restoredWidth; + private double restoredHeight; + private double dragOffsetX; + private double dragOffsetY; + private Timeline memoryTimer; + private String appliedStylesheet; + + public MainView(Stage stage) { + this.stage = stage; + I18n.setLanguage(state.getLanguageCode()); + logs.seedDemoIfEmpty(); + applyPalette(); + build(); + installShortcuts(); + installStateListeners(); + startMemoryTimer(); + updateApkButton(); + sceneProperty().addListener((observable, oldScene, newScene) -> { + if (newScene != null) { + applyPalette(); + } + }); + } + + private void build() { + setTop(createTopBar()); + setLeft(createSidebar()); + content = new StackPane(); + content.getStyleClass().add("content"); + setCenter(content); + setBottom(createStatusBar()); + showRoute(currentRoute); + } + + private HBox createTopBar() { + Node logo = Icons.logo(46); + Label name = Fx.label("APKEDITOR", "brand-title"); + Label subtitle = Fx.label(I18n.t("app.brandSubtitle"), "brand-subtitle"); + Label dot = Fx.label("·", "tiny", "muted"); + Label tagline = Fx.label(I18n.t("app.tagline"), "brand-link"); + HBox meta = new HBox(8, subtitle, dot, tagline); + meta.setAlignment(Pos.CENTER_LEFT); + VBox brandText = new VBox(2, name, meta); + brandText.setAlignment(Pos.CENTER_LEFT); + HBox brand = new HBox(12, logo, brandText); + brand.setAlignment(Pos.CENTER_LEFT); + + apkButton = new Button(); + apkButton.getStyleClass().add("apk-chip"); + apkName = Fx.label(I18n.t("apk.placeholder"), "apk-chip-name"); + HBox chip = new HBox(10, + Icons.node("file", 18, "icon-primary"), + apkName, + Icons.node("chevron-down", 14, "icon-muted")); + chip.setAlignment(Pos.CENTER_LEFT); + apkButton.setGraphic(chip); + apkButton.setMinWidth(300); + apkButton.setPrefWidth(340); + apkButton.setMaxWidth(390); + apkButton.setOnAction(event -> chooseApk()); + + Label themeLabel = Fx.label(I18n.t("topbar.theme"), "topbar-label"); + themeSelector = new ComboBox<>(); + themeSelector.getItems().addAll(FxTheme.NAMES); + themeSelector.setValue(state.getThemeName()); + themeSelector.getStyleClass().add("top-combo"); + HBox themeBox = new HBox(6, Icons.node("sun", 14, "icon-primary"), themeSelector); + themeBox.getStyleClass().add("top-control"); + themeBox.setAlignment(Pos.CENTER_LEFT); + themeSelector.setOnAction(event -> { + if (!rebuilding) { + String value = themeSelector.getValue(); + if (value != null && !value.equals(state.getThemeName())) { + state.setThemeName(value); + } + } + }); + + Label languageLabel = Fx.label(I18n.t("topbar.language"), "topbar-label"); + languageSelector = new ComboBox<>(); + languageSelector.getItems().addAll(I18n.Language.values()); + languageSelector.setValue(I18n.getLanguage()); + languageSelector.getStyleClass().add("top-combo"); + HBox languageBox = new HBox(6, Icons.node("globe", 14, "icon-muted"), languageSelector); + languageBox.getStyleClass().add("top-control"); + languageBox.setAlignment(Pos.CENTER_LEFT); + languageSelector.setOnAction(event -> { + if (!rebuilding) { + I18n.Language value = languageSelector.getValue(); + if (value != null && !value.getCode().equals(state.getLanguageCode())) { + state.setLanguageCode(value.getCode()); + } + } + }); + + Button minimize = windowButton("minimize", I18n.t("window.minimize"), "ghost-button"); + minimize.setOnAction(event -> stage.setIconified(true)); + Button maximize = windowButton("maximize", I18n.t("window.maximize"), "ghost-button"); + maximize.setOnAction(event -> toggleMaximize()); + Button close = windowButton("close", I18n.t("window.close"), "danger-button"); + close.setOnAction(event -> stage.close()); + + HBox top = new HBox(12); + top.getStyleClass().add("topbar"); + top.setAlignment(Pos.CENTER_LEFT); + top.getChildren().addAll(brand, Fx.hspacer(), apkButton, Fx.hspacer(), + themeLabel, themeBox, languageLabel, languageBox, + Fx.hspacer(), minimize, maximize, close); + installWindowDrag(top, brand, name); + return top; + } + + private Button windowButton(String icon, String tooltip, String styleClass) { + String iconClass = "close".equals(icon) ? "icon-error" : "icon-muted"; + Button button = Fx.iconButton(Icons.node(icon, 14, iconClass), tooltip, + styleClass, "window-button"); + return button; + } + + private VBox createSidebar() { + VBox sidebar = new VBox(2); + sidebar.getStyleClass().add("sidebar"); + sidebar.setPadding(new Insets(16, 12, 12, 12)); + + addNav(sidebar, Route.DASHBOARD, "nav.dashboard", "home"); + addNav(sidebar, Route.DECOMPILE, "nav.decompile", "decompile"); + addNav(sidebar, Route.ANALYZE, "nav.analyze", "analyze"); + addNav(sidebar, Route.SMALI_VIEWER, "nav.smaliViewer", "smali"); + addNav(sidebar, Route.BUILD, "nav.build", "build"); + addNav(sidebar, Route.MERGE, "nav.merge", "merge"); + addNav(sidebar, Route.REFACTOR, "nav.refactor", "refactor"); + addNav(sidebar, Route.PROTECT, "nav.protect", "protect"); + addNav(sidebar, Route.INFORMATION, "nav.information", "info"); + addNav(sidebar, Route.SETTINGS, "nav.settings", "settings"); + + sidebar.getChildren().add(Fx.vspacer()); + + Label footerName = Fx.label("APKEDITOR", "footer-title"); + Label footerVersion = Fx.label(I18n.t("app.version"), "tiny", "muted"); + Label footerTagline = Fx.label(I18n.t("app.sidebarTagline"), "footer-tagline"); + footerTagline.setWrapText(true); + footerTagline.setAlignment(Pos.CENTER); + VBox footer = new VBox(6, Icons.logo(46), footerName, footerVersion, footerTagline); + footer.setAlignment(Pos.CENTER); + footer.getStyleClass().add("sidebar-footer"); + sidebar.getChildren().add(footer); + return sidebar; + } + + private void addNav(VBox parent, Route route, String labelKey, String iconName) { + Button button = Fx.iconTextButton(I18n.t(labelKey), + Icons.node(iconName, 18, "icon-nav"), "nav-button"); + button.setMaxWidth(Double.MAX_VALUE); + button.setAlignment(Pos.CENTER_LEFT); + button.setOnAction(event -> showRoute(route)); + navButtons.put(route, button); + parent.getChildren().add(button); + } + + private HBox createStatusBar() { + Circle dot = new Circle(4, Color.web("#00E6A0")); + dot.getStyleClass().add("status-dot"); + statusLabel = Fx.label(I18n.t("status.ready"), "small", "text-success"); + Label separator = Fx.label("│", "small", "muted"); + memoryLabel = Fx.label(I18n.t("status.memory", "—"), "small", "muted"); + memoryBar = new ProgressBar(0); + memoryBar.getStyleClass().add("memory-bar"); + memoryBar.setPrefWidth(150); + Label footer = Fx.label(I18n.t("status.footer"), "status-footer"); + + HBox bar = new HBox(9, dot, statusLabel, separator, + Icons.node("cpu", 13, "icon-muted"), memoryLabel, memoryBar, + Fx.hspacer(), footer); + bar.getStyleClass().add("statusbar"); + bar.setAlignment(Pos.CENTER_LEFT); + return bar; + } + + public void navigate(Route route) { + showRoute(route); + } + + private void showRoute(Route route) { + if (route == Route.DECODE) { + route = Route.DECOMPILE; + } + if (route == null || content == null) { + return; + } + currentRoute = route; + Node view; + switch (route) { + case DASHBOARD -> view = view("dashboard", + () -> new DashboardFxView(state, this::showRoute)); + case ANALYZE -> { + view = workspace(); + ((WorkspaceFxView) view).showAnalysis(); + } + case SMALI_VIEWER -> { + view = workspace(); + ((WorkspaceFxView) view).showEditor(); + } + case BUILD, MERGE, REFACTOR, PROTECT -> view = operation(route); + case INFORMATION -> view = view("information", + () -> new InformationFxView(state, this::showRoute)); + case SETTINGS -> view = view("settings", + () -> new SettingsFxView(state, registry)); + default -> { + view = workspace(); + ((WorkspaceFxView) view).showStructure(); + } + } + content.getChildren().setAll(view); + for (Button button : navButtons.values()) { + button.getStyleClass().remove("active"); + } + Button active = navButtons.get(route); + if (active != null) { + active.getStyleClass().add("active"); + } + state.setActiveRoute(route.name().toLowerCase()); + } + + private WorkspaceFxView workspace() { + if (workspace == null) { + workspace = new WorkspaceFxView(state, operations, logs, this::loadApk); + } + return workspace; + } + + private Node operation(Route route) { + String id = route.name().toLowerCase(); + String key = "op:" + id; + return view(key, () -> new OperationFxView(id, registry.get(id), + state, operations, logs)); + } + + private Node view(String key, Supplier factory) { + Node node = viewCache.get(key); + if (node == null) { + node = factory.get(); + viewCache.put(key, node); + } + return node; + } + + private void chooseApk() { + FileChooser chooser = new FileChooser(); + chooser.setTitle(I18n.t("apk.dialogTitle")); + chooser.getExtensionFilters().add(new FileChooser.ExtensionFilter( + I18n.t("apk.filter"), "*.apk", "*.zip", "*.aab")); + File file = chooser.showOpenDialog(stage); + if (file != null) { + loadApk(file); + } + } + + public void loadApk(File file) { + if (file == null || !file.exists()) { + logs.warning(I18n.t("log.apk.notExists")); + return; + } + state.setCurrentInput(file); + logs.info(I18n.t("log.apk.loading", file.getName())); + new Thread(() -> { + try { + ApkService.Result result = ApkService.analyze(file); + Fx.run(() -> { + state.setAnalysis(result.getAnalysis()); + state.setCurrentTree(result.getTree()); + for (String message : result.getMessages()) { + logs.info(message); + } + logs.success(I18n.t("log.apk.loaded", + result.getAnalysis().getAppName(), + result.getAnalysis().getFileCount())); + showRoute(Route.DECOMPILE); + }); + } catch (Exception error) { + Fx.run(() -> logs.error(I18n.t("log.apk.analyzeError", shortMessage(error)))); + } + }, "apkeditor-load").start(); + } + + private void installStateListeners() { + state.addPropertyChangeListener(AppState.INPUT_CHANGED, + event -> Fx.run(this::updateApkButton)); + state.addPropertyChangeListener(AppState.THEME_CHANGED, + event -> Fx.run(() -> { + String theme = (String) event.getNewValue(); + if (themeSelector != null) { + rebuilding = true; + themeSelector.setValue(theme); + rebuilding = false; + } + applyPalette(); + })); + state.addPropertyChangeListener(AppState.LANGUAGE_CHANGED, + event -> Fx.run(this::applyLanguage)); + state.addPropertyChangeListener(AppState.BUSY_CHANGED, + event -> Fx.run(() -> setBusy((Boolean) event.getNewValue()))); + } + + private void installShortcuts() { + addEventFilter(KeyEvent.KEY_PRESSED, event -> { + if (event.getCode() == KeyCode.F5) { + showRoute(Route.DECOMPILE); + workspace().runDecode(); + event.consume(); + } else if (event.isControlDown() && event.getCode() == KeyCode.O) { + chooseApk(); + event.consume(); + } else if (event.isControlDown() && event.getCode() == KeyCode.DIGIT1) { + showRoute(Route.DECOMPILE); + event.consume(); + } else if (event.isControlDown() && event.getCode() == KeyCode.COMMA) { + showRoute(Route.SETTINGS); + event.consume(); + } + }); + } + + private void applyPalette() { + Scene scene = getScene(); + if (scene == null) { + return; + } + String css = FxTheme.renderCss(state.getThemeName(), CSS_TEMPLATE); + String stylesheet = "data:text/css;base64," + Base64.getEncoder() + .encodeToString(css.getBytes(StandardCharsets.UTF_8)); + if (appliedStylesheet != null) { + scene.getStylesheets().remove(appliedStylesheet); + } + scene.getStylesheets().add(stylesheet); + appliedStylesheet = stylesheet; + } + + private static String loadCssTemplate() { + try (InputStream input = MainView.class.getResourceAsStream("/styles/app.css")) { + if (input == null) { + return ""; + } + return new String(input.readAllBytes(), StandardCharsets.UTF_8); + } catch (IOException error) { + return ""; + } + } + + private void applyLanguage() { + if (rebuilding) { + return; + } + rebuilding = true; + try { + I18n.setLanguage(state.getLanguageCode()); + viewCache.clear(); + workspace = null; + getChildren().clear(); + build(); + } finally { + rebuilding = false; + } + } + + private void updateApkButton() { + if (apkButton == null) { + return; + } + File input = state.getCurrentInput(); + if (apkName != null) { + apkName.setText(input == null ? I18n.t("apk.placeholder") : input.getName()); + } + apkButton.setTooltip(new Tooltip(input == null + ? I18n.t("apk.selectTooltip") : input.getAbsolutePath())); + } + + private void setBusy(boolean busy) { + if (statusLabel == null) { + return; + } + statusLabel.setText(I18n.t(busy ? "status.processing" : "status.ready")); + statusLabel.getStyleClass().removeAll("text-success", "text-warning"); + statusLabel.getStyleClass().add(busy ? "text-warning" : "text-success"); + } + + private void startMemoryTimer() { + memoryTimer = new Timeline(new KeyFrame(Duration.millis(2500), + event -> updateMemory())); + memoryTimer.setCycleCount(Animation.INDEFINITE); + memoryTimer.play(); + updateMemory(); + } + + private void updateMemory() { + if (memoryLabel == null || memoryBar == null) { + return; + } + Runtime runtime = Runtime.getRuntime(); + long used = runtime.totalMemory() - runtime.freeMemory(); + long max = runtime.maxMemory(); + memoryLabel.setText(I18n.t("status.memory", format(used) + " / " + format(max))); + memoryBar.setProgress(max <= 0 ? 0 : Math.min(1.0, (double) used / max)); + } + + private String format(long bytes) { + double mb = bytes / (1024.0 * 1024.0); + if (mb >= 1024) { + return String.format("%.1f GB", mb / 1024.0); + } + return String.format("%.0f MB", mb); + } + + private void installWindowDrag(Region bar, Region brand, Label name) { + dragOffsetX = 0; + dragOffsetY = 0; + javafx.event.EventHandler pressed = event -> { + if (!maximized) { + dragOffsetX = event.getScreenX() - stage.getX(); + dragOffsetY = event.getScreenY() - stage.getY(); + } + }; + javafx.event.EventHandler dragged = event -> { + if (!maximized) { + stage.setX(event.getScreenX() - dragOffsetX); + stage.setY(event.getScreenY() - dragOffsetY); + } + }; + javafx.event.EventHandler clicked = event -> { + if (event.getClickCount() == 2) { + toggleMaximize(); + } + }; + for (javafx.scene.Node node : new javafx.scene.Node[]{bar, brand, name}) { + node.addEventHandler(MouseEvent.MOUSE_PRESSED, pressed); + node.addEventHandler(MouseEvent.MOUSE_DRAGGED, dragged); + node.addEventHandler(MouseEvent.MOUSE_CLICKED, clicked); + } + } + + private void toggleMaximize() { + if (!maximized) { + restoredX = stage.getX(); + restoredY = stage.getY(); + restoredWidth = stage.getWidth(); + restoredHeight = stage.getHeight(); + Rectangle2D bounds = Screen.getPrimary().getVisualBounds(); + stage.setX(bounds.getMinX()); + stage.setY(bounds.getMinY()); + stage.setWidth(bounds.getWidth()); + stage.setHeight(bounds.getHeight()); + maximized = true; + } else { + stage.setX(restoredX); + stage.setY(restoredY); + stage.setWidth(restoredWidth); + stage.setHeight(restoredHeight); + maximized = false; + } + } + + private String shortMessage(Throwable error) { + String message = error.getMessage(); + return message == null || message.isBlank() + ? error.getClass().getSimpleName() : message; + } +} diff --git a/src/javafx/java/com/reandroid/apkeditor/javafx/ProgressPane.java b/src/javafx/java/com/reandroid/apkeditor/javafx/ProgressPane.java new file mode 100644 index 00000000..80a8a3bc --- /dev/null +++ b/src/javafx/java/com/reandroid/apkeditor/javafx/ProgressPane.java @@ -0,0 +1,128 @@ +package com.reandroid.apkeditor.javafx; + +import com.reandroid.apkeditor.gui.i18n.I18n; +import javafx.geometry.Pos; +import javafx.scene.control.Label; +import javafx.scene.control.ProgressBar; +import javafx.scene.layout.HBox; +import javafx.scene.layout.StackPane; +import javafx.scene.layout.VBox; +import javafx.scene.shape.SVGPath; + +import java.util.ArrayList; +import java.util.List; + +/** + * Panel de progreso con descripción y etapas, equivalente al de Swing. + */ +public final class ProgressPane extends Card { + public enum StepStatus { COMPLETED, ACTIVE, PENDING } + + private static final String[] COLOR_CLASSES = { + "text-primary", "text-success", "text-warning", "text-error", "text-muted" + }; + + private final Label description = Fx.label("", "small", "muted"); + private final ProgressBar bar = new ProgressBar(0); + private final Label percent = Fx.label("0%", "value", "small", "text-primary-bright"); + private final Label state = Fx.label(I18n.t("progress.state.running"), "tiny", "text-warning"); + private final HBox steps = new HBox(18); + private final List