From 8269d0d5b6343815a05465de3bd97b488b621ac5 Mon Sep 17 00:00:00 2001 From: Radiquum Date: Wed, 3 Sep 2025 10:41:55 +0500 Subject: [PATCH 1/6] feat: read apk name when --sign arg is provided from apktool.yml --- main.py | 48 ++++++++++++++++++++++++++---------------------- requirements.txt | 3 ++- 2 files changed, 28 insertions(+), 23 deletions(-) diff --git a/main.py b/main.py index 75b4003..d83d1a3 100644 --- a/main.py +++ b/main.py @@ -2,18 +2,19 @@ from scripts.download_tools import check_and_download_all_tools from scripts.select_apk import get_apks, select_apk from scripts.select_patches import apply_patches, get_patches, select_patches from scripts.utils import check_java_version, compile_apk, decompile_apk, sign_apk -from config import log, console +from config import config, log, console from time import time import math - import argparse -parser = argparse.ArgumentParser(prog='anixart patcher') -parser.add_argument("--no-decompile", action='store_true') -parser.add_argument("--no-compile", action='store_true') +import yaml -parser.add_argument("--patch", action='store_true') -parser.add_argument("--sign", action='store_true') + +parser = argparse.ArgumentParser(prog="anixart patcher") +parser.add_argument("--no-decompile", action="store_true") +parser.add_argument("--no-compile", action="store_true") +parser.add_argument("--patch", action="store_true") +parser.add_argument("--sign", action="store_true") def patch(): @@ -30,51 +31,54 @@ def patch(): else: console.print(f"{status['name']}: ✘", style="bold red") statuses_err.append(status["name"]) - return patches, statuses_ok, statuses_err if __name__ == "__main__": args = parser.parse_args() - + check_and_download_all_tools() check_java_version() - if not args.patch: + if not args.patch and not args.sign: apks = get_apks() if not apks: log.fatal(f"apks folder is empty") exit(1) apk = select_apk(apks) if not apk: - log.info('cancelled') + log.info("cancelled") exit(0) - else: + elif args.patch: patch() exit(0) - - if args.sign: - sign_apk(f"{apk.removesuffix('.apk')}-patched.apk") + elif args.sign: + apkFileName = None + with open( + f"{config["folders"]["decompiled"]}/apktool.yml", "r", encoding="utf-8" + ) as f: + data = yaml.load(f.read(), Loader=yaml.Loader) + apkFileName = data.get("apkFileName", None) + if not apkFileName: + log.fatal( + f"can't find apk file name in {config['folders']['decompiled']}/apktool.yml" + ) + exit(1) + sign_apk(f"{apkFileName.removesuffix('.apk')}-patched.apk") exit(0) start_time = time() - if not args.no_decompile: decompile_apk(apk) - patches, statuses_ok, statuses_err = patch() - if not args.no_compile: compile_apk(f"{apk.removesuffix(".apk")}-patched.apk") - end_time = time() - if not args.no_compile: sign_apk(f"{apk.removesuffix(".apk")}-patched.apk") - log.info("Finished") - log.info(f"install this apk file: {apk.removesuffix(".apk")}-patched-aligned-signed.apk") + log.info(f"install this apk file: `{config["folders"]["dist"]}/{apk.removesuffix(".apk")}-patched-aligned-signed.apk`") log.info(f"used and successful patches: {", ".join(statuses_ok)}") log.info(f"used and unsuccessful patches: {", ".join(statuses_err)}") log.info(f"time taken: {math.floor(end_time - start_time)}s") diff --git a/requirements.txt b/requirements.txt index 154e25e..97fc3bc 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,5 @@ requests lxml rich -beaupy \ No newline at end of file +beaupy +pyyaml \ No newline at end of file From bf59b2bbae6d5ad4cc11e7ddcd0b5dadced553b2 Mon Sep 17 00:00:00 2001 From: Radiquum Date: Wed, 3 Sep 2025 10:55:15 +0500 Subject: [PATCH 2/6] feat: introduce config path argument --- .gitignore | 4 +++- config.py | 18 +++++++++++++----- main.py | 18 +++++------------- patches/change_package_name.config.json | 2 +- patches/force_static_request_urls.config.json | 2 +- 5 files changed, 23 insertions(+), 21 deletions(-) diff --git a/.gitignore b/.gitignore index f412ff3..750dd16 100644 --- a/.gitignore +++ b/.gitignore @@ -7,4 +7,6 @@ decompiled dist __pycache__ keystore.jks -help \ No newline at end of file +help +dev_patches +dev.config.json \ No newline at end of file diff --git a/config.py b/config.py index d8a9199..0adb060 100644 --- a/config.py +++ b/config.py @@ -1,3 +1,4 @@ +import argparse import json import logging import os @@ -42,19 +43,26 @@ class Config(TypedDict): xml_ns: ConfigXmlNS +parser = argparse.ArgumentParser(prog="anixart patcher") +parser.add_argument("--config", help="path to config.json file", default="config.json") +parser.add_argument("--no-decompile", action="store_true") +parser.add_argument("--no-compile", action="store_true") +parser.add_argument("--patch", action="store_true") +parser.add_argument("--sign", action="store_true") +args = parser.parse_args() + + def load_config() -> Config: config = None - if not os.path.exists("config.json"): + if not os.path.exists(args.config): log.exception("file `config.json` is not found!") exit(1) - with open("./config.json", "r", encoding="utf-8") as file: + with open(args.config, "r", encoding="utf-8") as file: config = json.loads(file.read()) log.setLevel(config.get("log_level", "NOTSET").upper()) return config - - -config = load_config() +config = load_config() \ No newline at end of file diff --git a/main.py b/main.py index d83d1a3..03c1588 100644 --- a/main.py +++ b/main.py @@ -2,28 +2,20 @@ from scripts.download_tools import check_and_download_all_tools from scripts.select_apk import get_apks, select_apk from scripts.select_patches import apply_patches, get_patches, select_patches from scripts.utils import check_java_version, compile_apk, decompile_apk, sign_apk -from config import config, log, console +from config import args, config, log, console from time import time import math -import argparse import yaml -parser = argparse.ArgumentParser(prog="anixart patcher") -parser.add_argument("--no-decompile", action="store_true") -parser.add_argument("--no-compile", action="store_true") -parser.add_argument("--patch", action="store_true") -parser.add_argument("--sign", action="store_true") - - def patch(): patches = get_patches() patches = select_patches(patches) statuses = apply_patches(patches) + statuses_ok = [] statuses_err = [] - for status in statuses: if status["status"]: console.print(f"{status['name']}: ✔", style="bold green") @@ -35,8 +27,6 @@ def patch(): if __name__ == "__main__": - args = parser.parse_args() - check_and_download_all_tools() check_java_version() @@ -78,7 +68,9 @@ if __name__ == "__main__": sign_apk(f"{apk.removesuffix(".apk")}-patched.apk") log.info("Finished") - log.info(f"install this apk file: `{config["folders"]["dist"]}/{apk.removesuffix(".apk")}-patched-aligned-signed.apk`") + log.info( + f"install this apk file: `{config["folders"]["dist"]}/{apk.removesuffix(".apk")}-patched-aligned-signed.apk`" + ) log.info(f"used and successful patches: {", ".join(statuses_ok)}") log.info(f"used and unsuccessful patches: {", ".join(statuses_err)}") log.info(f"time taken: {math.floor(end_time - start_time)}s") diff --git a/patches/change_package_name.config.json b/patches/change_package_name.config.json index 56b2452..6c73991 100644 --- a/patches/change_package_name.config.json +++ b/patches/change_package_name.config.json @@ -1,3 +1,3 @@ { - "new_package_name": "com.radiquum.anixart" + "new_package_name": "com.swiftsoft.anixartd" } \ No newline at end of file diff --git a/patches/force_static_request_urls.config.json b/patches/force_static_request_urls.config.json index 6827b01..8415c42 100644 --- a/patches/force_static_request_urls.config.json +++ b/patches/force_static_request_urls.config.json @@ -1,5 +1,5 @@ { - "base_url": "https://anix-api.wah.su/", + "base_url": "https://api-s.anixsekai.com/", "values": [ { "file_path": "smali_classes2/com/swiftsoft/anixartd/network/api/ConfigApi.smali", From ea5a1a2bededacc22fa196483469619af79ef498 Mon Sep 17 00:00:00 2001 From: Radiquum Date: Wed, 3 Sep 2025 11:04:44 +0500 Subject: [PATCH 3/6] fix: [BUG] If `compress` and `force_static_request_urls` patches are applied, app won't load Fixes #9 --- patches/compress.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/patches/compress.py b/patches/compress.py index bdf0635..a3280c7 100644 --- a/patches/compress.py +++ b/patches/compress.py @@ -47,7 +47,7 @@ def remove_debug_lines(): file_content = get_smali_lines(file_path) new_content = [] for line in file_content: - if line.find(".line") >= 0 or line.find(".source") >= 0: + if line.find(".line") >= 0: continue new_content.append(line) save_smali_lines(file_path, new_content) From 2fef0a32b7802e69080ab48095d9994738e81598 Mon Sep 17 00:00:00 2001 From: Radiquum Date: Wed, 3 Sep 2025 15:43:41 +0500 Subject: [PATCH 4/6] feat: add more settings to compress patch --- patches/compress.config.json | 8 +- patches/compress.md | 30 ++++++ patches/compress.py | 202 +++++++++++++++++++++++++++++++++-- patches/resources/blank.mp3 | Bin 0 -> 1370 bytes 4 files changed, 231 insertions(+), 9 deletions(-) create mode 100644 patches/compress.md create mode 100644 patches/resources/blank.mp3 diff --git a/patches/compress.config.json b/patches/compress.config.json index 645e742..62b15f9 100644 --- a/patches/compress.config.json +++ b/patches/compress.config.json @@ -1,3 +1,9 @@ { - "keep_dirs": ["META-INF", "kotlin"] + "remove_language_files": true, + "remove_AI_voiceover": true, + "remove_debug_lines": true, + "remove_drawable_files": false, + "remove_unknown_files": false, + "remove_unknown_files_keep_dirs": ["META-INF", "kotlin"], + "compress_png_files": false } \ No newline at end of file diff --git a/patches/compress.md b/patches/compress.md new file mode 100644 index 0000000..581f1a1 --- /dev/null +++ b/patches/compress.md @@ -0,0 +1,30 @@ +# Compress + +Patch is used to compress and remove resources to reduce final apk filesize + +## settings (compress.config.json) + +- remove_unknown_files: true/false - removes files from decompiled/unknown directory +- remove_unknown_files_keep_dirs: list[str] - keeps specified directories in decompiled/unknown directory +- remove_debug_lines: true/false - removes `.line n` from decompiled smali files +- remove_AI_voiceover: true/false - replaces voiceover of anixa character with a blank mp3 +- compress_png_files: true/false - compresses PNG in decompiled/res directory +- remove_drawable_files: true/false - removes some drawable-* from decompiled/res +- remove_language_files: true/false - removes all languages except ru and en + +## efficiency + +Tested with 9.0 Beta 7 + +diff = original apk bytes - patch apk bytes + +| Setting | Filesize | Diff | % | +| :----------- | :-------------------: | :-----------------: | :-: | +| None | 17092 bytes - 17.1 MB | - | - | +| Compress PNG | 17072 bytes - 17.1 MB | 20 bytes - 0.0 MB | 0.11% | +| Remove files | 17020 bytes - 17.0 MB | 72 bytes - 0.1 MB | 0.42% | +| Remove draws | 16940 bytes - 16.9 MB | 152 bytes - 0.2 MB | 0.89% | +| Remove lines | 16444 bytes - 16.4 MB | 648 bytes - 0.7 MB | 3.79% | +| Remove ai vo | 15812 bytes - 15.8 MB | 1280 bytes - 1.3 MB | 7.49% | +| Remove langs | 15764 bytes - 15.7 MB | 1328 bytes - 1.3 MB | 7.76% | +| All enabled | 13592 bytes - 13.6 MB | 3500 bytes - 4.8 MB | 20.5% | diff --git a/patches/compress.py b/patches/compress.py index a3280c7..6b2c83f 100644 --- a/patches/compress.py +++ b/patches/compress.py @@ -18,10 +18,16 @@ from scripts.smali_parser import get_smali_lines, save_smali_lines # Patch class PatchConfig_Compress(TypedDict): - keep_dirs: list[str] + remove_language_files: bool + remove_AI_voiceover: bool + remove_debug_lines: bool + remove_drawable_files: bool + remove_unknown_files: bool + remove_unknown_files_keep_dirs: list[str] + compress_png_files: bool -def remove_files(patch_config: PatchConfig_Compress): +def remove_unknown_files(patch_config: PatchConfig_Compress): path = f"{config['folders']['decompiled']}/unknown" items = os.listdir(path) @@ -31,7 +37,7 @@ def remove_files(patch_config: PatchConfig_Compress): os.remove(item_path) log.debug(f"[COMPRESS] removed file: {item_path}") elif os.path.isdir(item_path): - if item not in patch_config["keep_dirs"]: + if item not in patch_config["remove_unknown_files_keep_dirs"]: shutil.rmtree(item_path) log.debug(f"[COMPRESS] removed directory: {item_path}") @@ -87,7 +93,7 @@ def compress_png(png_path: str): exit(1) -def compress_pngs(): +def compress_png_files(): compressed = [] for root, _, files in os.walk(f"{config['folders']['decompiled']}"): if len(files) < 0: @@ -101,10 +107,190 @@ def compress_pngs(): log.debug(f"[COMPRESS] {len(compressed)} pngs have been compressed") -def apply(patch_config: PatchConfig_Compress) -> bool: +def remove_AI_voiceover(): + blank = f"{config['folders']['patches']}/resources/blank.mp3" + path = f"{config['folders']['decompiled']}/res/raw" + files = [ + "reputation_1.mp3", + "reputation_2.mp3", + "reputation_3.mp3", + "sound_beta_1.mp3", + "sound_create_blog_1.mp3", + "sound_create_blog_2.mp3", + "sound_create_blog_3.mp3", + "sound_create_blog_4.mp3", + "sound_create_blog_5.mp3", + "sound_create_blog_6.mp3", + "sound_create_blog_reputation_1.mp3", + "sound_create_blog_reputation_2.mp3", + "sound_create_blog_reputation_3.mp3", + "sound_create_blog_reputation_4.mp3", + "sound_create_blog_reputation_5.mp3", + "sound_create_blog_reputation_6.mp3", + ] - remove_files(patch_config) - remove_debug_lines() - # compress_pngs() + for file in files: + if os.path.exists(f"{path}/{file}"): + os.remove(f"{path}/{file}") + shutil.copyfile(blank, f"{path}/{file}") + log.debug(f"[COMPRESS] {file} has been replaced with blank.mp3") + + log.debug(f"[COMPRESS] ai voiceover has been removed") + + +def remove_language_files(): + path = f"{config['folders']['decompiled']}/res" + folders = [ + "values-af", + "values-am", + "values-ar", + "values-as", + "values-az", + "values-b+es+419", + "values-b+sr+Latn", + "values-be", + "values-bg", + "values-bn", + "values-bs", + "values-ca", + "values-cs", + "values-da", + "values-de", + "values-el", + "values-en-rAU", + "values-en-rCA", + "values-en-rGB", + "values-en-rIN", + "values-en-rXC", + "values-es", + "values-es-rGT", + "values-es-rUS", + "values-et", + "values-eu", + "values-fa", + "values-fi", + "values-fr", + "values-fr-rCA", + "values-gl", + "values-gu", + "values-hi", + "values-hr", + "values-hu", + "values-hy", + "values-in", + "values-is", + "values-it", + "values-iw", + "values-ja", + "values-ka", + "values-kk", + "values-km", + "values-kn", + "values-ko", + "values-ky", + "values-lo", + "values-lt", + "values-lv", + "values-mk", + "values-ml", + "values-mn", + "values-mr", + "values-ms", + "values-my", + "values-nb", + "values-ne", + "values-nl", + "values-or", + "values-pa", + "values-pl", + "values-pt", + "values-pt-rBR", + "values-pt-rPT", + "values-ro", + "values-si", + "values-sk", + "values-sl", + "values-sq", + "values-sr", + "values-sv", + "values-sw", + "values-ta", + "values-te", + "values-th", + "values-tl", + "values-tr", + "values-uk", + "values-ur", + "values-uz", + "values-vi", + "values-zh", + "values-zh-rCN", + "values-zh-rHK", + "values-zh-rTW", + "values-zu", + "values-watch", + ] + + for folder in folders: + if os.path.exists(f"{path}/{folder}"): + shutil.rmtree(f"{path}/{folder}") + log.debug(f"[COMPRESS] {folder} has been removed") + + +def remove_drawable_files(): + path = f"{config['folders']['decompiled']}/res" + folders = [ + "drawable-en-hdpi", + "drawable-en-ldpi", + "drawable-en-mdpi", + "drawable-en-xhdpi", + "drawable-en-xxhdpi", + "drawable-en-xxxhdpi", + "drawable-ldrtl-hdpi", + "drawable-ldrtl-mdpi", + "drawable-ldrtl-xhdpi", + "drawable-ldrtl-xxhdpi", + "drawable-ldrtl-xxxhdpi", + "drawable-tr-anydpi", + "drawable-tr-hdpi", + "drawable-tr-ldpi", + "drawable-tr-mdpi", + "drawable-tr-xhdpi", + "drawable-tr-xxhdpi", + "drawable-tr-xxxhdpi", + "drawable-watch", + "layout-watch", + ] + + for folder in folders: + if os.path.exists(f"{path}/{folder}"): + shutil.rmtree(f"{path}/{folder}") + log.debug(f"[COMPRESS] {folder} has been removed") + + +def apply(patch_config: PatchConfig_Compress) -> bool: + if patch_config['remove_unknown_files']: + log.info("[COMPRESS] removing unknown files") + remove_unknown_files(patch_config) + + if patch_config["remove_drawable_files"]: + log.info("[COMPRESS] removing drawable-xx dirs") + remove_drawable_files() + + if patch_config["compress_png_files"]: + log.info("[COMPRESS] compressing PNGs") + compress_png_files() + + if patch_config["remove_language_files"]: + log.info("[COMPRESS] removing languages") + remove_language_files() + + if patch_config["remove_AI_voiceover"]: + log.info("[COMPRESS] removing AI voiceover") + remove_AI_voiceover() + + if patch_config["remove_debug_lines"]: + log.info("[COMPRESS] stripping debug lines") + remove_debug_lines() return True diff --git a/patches/resources/blank.mp3 b/patches/resources/blank.mp3 new file mode 100644 index 0000000000000000000000000000000000000000..bfab560aba4720e1ac4ae95a1d14e2c844997383 GIT binary patch literal 1370 zcmeZtF=k-^0i}@OU{@f`$H2hslUSB!W~gVbXJ}vmmV^-he>)sN;zF37d1?7T7C#Vk zGcd5~F_^=EHw?tXKrsxo!@z79SPuh-VcmU|=w5U|?YJarAXH26-=(5`ba~$R5pRG Date: Wed, 3 Sep 2025 16:40:15 +0500 Subject: [PATCH 5/6] feat: add patch init arg --- config.py | 5 ++-- main.py | 6 ++++- scripts/utils.py | 65 +++++++++++++++++++++++++++++++++++++++++++++--- 3 files changed, 69 insertions(+), 7 deletions(-) diff --git a/config.py b/config.py index 0adb060..8b1d7cb 100644 --- a/config.py +++ b/config.py @@ -47,8 +47,9 @@ parser = argparse.ArgumentParser(prog="anixart patcher") parser.add_argument("--config", help="path to config.json file", default="config.json") parser.add_argument("--no-decompile", action="store_true") parser.add_argument("--no-compile", action="store_true") -parser.add_argument("--patch", action="store_true") -parser.add_argument("--sign", action="store_true") +parser.add_argument("--patch", action="store_true", help="only patch decompiled") +parser.add_argument("--sign", action="store_true", help="only sign compiled apk") +parser.add_argument("--init", action="store_true", help="init a new patch") args = parser.parse_args() diff --git a/main.py b/main.py index 03c1588..75e0efd 100644 --- a/main.py +++ b/main.py @@ -1,7 +1,7 @@ from scripts.download_tools import check_and_download_all_tools from scripts.select_apk import get_apks, select_apk from scripts.select_patches import apply_patches, get_patches, select_patches -from scripts.utils import check_java_version, compile_apk, decompile_apk, sign_apk +from scripts.utils import check_java_version, compile_apk, decompile_apk, sign_apk, init_patch from config import args, config, log, console from time import time @@ -29,6 +29,10 @@ def patch(): if __name__ == "__main__": check_and_download_all_tools() check_java_version() + + if args.init: + init_patch() + exit(0) if not args.patch and not args.sign: apks = get_apks() diff --git a/scripts/utils.py b/scripts/utils.py index d72319e..310e5d4 100644 --- a/scripts/utils.py +++ b/scripts/utils.py @@ -2,6 +2,7 @@ import os import shutil import subprocess from config import log, config +from beaupy import prompt def check_java_version(): @@ -80,10 +81,16 @@ def compile_apk(apk: str): def sign_apk(apk: str): log.info(f"sign and align apk: `{apk}`") - if os.path.exists(f"{config['folders']['dist']}/{apk.removesuffix('.apk')}-aligned.apk"): + if os.path.exists( + f"{config['folders']['dist']}/{apk.removesuffix('.apk')}-aligned.apk" + ): os.remove(f"{config['folders']['dist']}/{apk.removesuffix('.apk')}-aligned.apk") - if os.path.exists(f"{config['folders']['dist']}/{apk.removesuffix('.apk')}-aligned-signed.apk"): - os.remove(f"{config['folders']['dist']}/{apk.removesuffix('.apk')}-aligned-signed.apk") + if os.path.exists( + f"{config['folders']['dist']}/{apk.removesuffix('.apk')}-aligned-signed.apk" + ): + os.remove( + f"{config['folders']['dist']}/{apk.removesuffix('.apk')}-aligned-signed.apk" + ) command = "" try: @@ -111,5 +118,55 @@ def sign_apk(apk: str): stderr=subprocess.PIPE, ) except subprocess.CalledProcessError as e: - log.fatal(f"error of running a command: %s :: %s", command, e.stderr, exc_info=True) + log.fatal( + f"error of running a command: %s :: %s", command, e.stderr, exc_info=True + ) exit(1) + +def patch_config_name(patch_name: str) -> str: + components = patch_name.split("_") + return "PatchConfig_" + "".join(x.title() for x in components) + +def init_patch(): + if not os.path.exists(config["folders"]["patches"]): + log.info(f"creating `patches` folder: {config['folders']['patches']}") + os.mkdir(config["folders"]["patches"]) + + if not os.path.exists(f"{config['folders']['patches']}/__init__.py"): + with open(f"{config['folders']['patches']}/__init__.py", "w") as f: + f.write("") + + name = prompt("Patch name: ", lambda x: x.strip().lower().replace(" ", "_")) + description = prompt("Patch description: ", lambda x: x.strip()) + priority = prompt("Patch priority: ", target_type=int, initial_value="0") + + patch_content = f"""\"\"\"{description}\"\"\" + +# patch settings +# priority, default: {priority} +priority = {priority} + +# imports +## bundled +from typing import TypedDict + +## custom +from config import config, log + + +# Patch +class {patch_config_name(name)}(TypedDict): + pass + + +def apply(patch_conf: {patch_config_name(name)}) -> bool: + log.info("patch `{name}` applied, nothing changed") + return True +""" + + with open(f"{config['folders']['patches']}/{name}.py", "w", encoding="utf-8") as f: + f.write(patch_content) + with open(f"{config['folders']['patches']}/{name}.config.json", "w", encoding="utf-8") as f: + f.write("{}") + + log.info(f"patch `{name}` created") \ No newline at end of file From e12967efaf8e1c06f4163c0585f99e9b80db9f29 Mon Sep 17 00:00:00 2001 From: Radiquum Date: Wed, 3 Sep 2025 17:15:03 +0500 Subject: [PATCH 6/6] feat: [R] bookmark location change patch Fixes #1 --- patches/change_navigation_bar.config.json | 16 +++++++ patches/change_navigation_bar.py | 52 +++++++++++++++++++++++ 2 files changed, 68 insertions(+) create mode 100644 patches/change_navigation_bar.config.json create mode 100644 patches/change_navigation_bar.py diff --git a/patches/change_navigation_bar.config.json b/patches/change_navigation_bar.config.json new file mode 100644 index 0000000..379e7df --- /dev/null +++ b/patches/change_navigation_bar.config.json @@ -0,0 +1,16 @@ +{ + "portrait": [ + "home", + "discover", + "feed", + "bookmarks", + "profile" + ], + "landscape": [ + "home", + "discover", + "feed", + "bookmarks", + "profile" + ] +} \ No newline at end of file diff --git a/patches/change_navigation_bar.py b/patches/change_navigation_bar.py new file mode 100644 index 0000000..0202cc0 --- /dev/null +++ b/patches/change_navigation_bar.py @@ -0,0 +1,52 @@ +"""Move and replace navigation bar tabs""" + +# patch settings +# priority, default: 0 +priority = 0 + +# imports +## bundled +from typing import TypedDict + +## installed +from lxml import etree + +## custom +from config import config, log + + +# Patch +class PatchConfig_ChangeNavigationBar(TypedDict): + portrait: list[str] + landscape: list[str] + + +allowed_items = ["home", "discover", "feed", "bookmarks", "profile"] + + +def modify_menu(menu: list[str], path: str) -> None: + for item in menu: + if item not in allowed_items: + log.warning(f"menu item `{item}` is not allowed, removing from list") + menu.remove(item) + + root = etree.Element("menu", nsmap={"android": config['xml_ns']['android']}) + for item in menu: + element = etree.SubElement(root, "item") + element.set(f"{{{config['xml_ns']['android']}}}icon", f"@drawable/nav_{item}") + element.set(f"{{{config['xml_ns']['android']}}}id", f"@id/tab_{item}") + element.set(f"{{{config['xml_ns']['android']}}}title", f"@string/{item}") + + tree = etree.ElementTree(root) + tree.write( + path, + pretty_print=True, + xml_declaration=True, + encoding="utf-8", + ) + + +def apply(patch_conf: PatchConfig_ChangeNavigationBar) -> bool: + modify_menu(patch_conf["portrait"], f"{config['folders']['decompiled']}/res/menu/bottom.xml") + modify_menu(patch_conf["landscape"], f"{config['folders']['decompiled']}/res/menu/navigation_rail_menu.xml") + return True