1
0
Fork 0
mirror of https://github.com/Radiquum/anixart-patcher.git synced 2025-09-05 18:55:33 +05:00

refactor: tqdm -> rich.progress

This commit is contained in:
Kentai Radiquum 2025-09-02 08:47:52 +05:00
parent 43b1406b4a
commit 7b5ba163bd
Signed by: Radiquum
GPG key ID: 858E8EE696525EED
10 changed files with 260 additions and 141 deletions

View file

@ -1,4 +1,5 @@
{ {
"log_level": "INFO",
"tools": [ "tools": [
{ {
"tool": "apktool.jar", "tool": "apktool.jar",
@ -11,5 +12,9 @@
"decompiled": "./decompiled", "decompiled": "./decompiled",
"patches": "./patches", "patches": "./patches",
"dist": "./dist" "dist": "./dist"
},
"xml_ns": {
"android": "http://schemas.android.com/apk/res/android",
"app": "http://schemas.android.com/apk/res-auto"
} }
} }

View file

@ -30,18 +30,31 @@ class ConfigFolders(TypedDict):
dist: str dist: str
class ConfigXmlNS(TypedDict):
android: str
app: str
class Config(TypedDict): class Config(TypedDict):
log_level: str
tools: list[ConfigTools] tools: list[ConfigTools]
folders: ConfigFolders folders: ConfigFolders
xml_ns: ConfigXmlNS
def load_config() -> Config: def load_config() -> Config:
config = None
if not os.path.exists("config.json"): if not os.path.exists("config.json"):
log.exception("file `config.json` is not found!") log.exception("file `config.json` is not found!")
exit(1) exit(1)
with open("./config.json", "r", encoding="utf-8") as file: with open("./config.json", "r", encoding="utf-8") as file:
return json.loads(file.read()) config = json.loads(file.read())
log.setLevel(config.get("log_level", "NOTSET").upper())
return config
config = load_config() config = load_config()

View file

@ -1,73 +1,88 @@
"""Change app color theme""" """Change app color theme"""
# patch settings
# priority, default: -99 (run before last)
priority = -99 priority = -99
import os # imports
## bundled
from typing import TypedDict from typing import TypedDict
from beaupy import select from beaupy import select
from tqdm import tqdm
## installed
from lxml import etree from lxml import etree
## custom
from config import config, log, console
class PatchConfig_ChangeColorThemeValue(TypedDict):
attributes: list[dict[str, str]]
text: list[dict[str, str]]
files: list[dict[str, str]]
class PatchConfig_ChangeColorTheme(TypedDict): class PatchConfig_ChangeColorTheme(TypedDict):
src: str themes: list[str]
themes: str key: PatchConfig_ChangeColorThemeValue
def apply(config: PatchConfig_ChangeColorTheme) -> bool:
print("select color theme to apply")
theme = select(config["themes"], cursor="->", cursor_style="cyan") def apply(patch_config: PatchConfig_ChangeColorTheme) -> bool:
theme_attr = config[theme]['attributes']
theme_text = config[theme]['text']
theme_files = config[theme]['files']
with tqdm( console.print("select color theme to apply (press [bold]enter[/bold] to confirm)")
total=len(theme_attr), theme = select(patch_config["themes"], cursor="->", cursor_style="cyan")
unit="attr", if not theme:
unit_divisor=1, console.print(f"theme: default")
desc="color attributes" return False
) as bar: console.print(f"theme: {theme}")
for attr in theme_attr:
parser = etree.XMLParser(remove_blank_text=True)
tree = etree.parse(f"{config['src']}/{attr['file_path']}", parser)
root = tree.getroot()
root.find(attr['tag_path']).set(attr['attr_name'], attr['attr_value']['to'])
tree.write(
f"{config['src']}/{attr['file_path']}",
pretty_print=True,
xml_declaration=True,
encoding="utf-8",
)
bar.update()
with tqdm(
total=len(theme_text),
unit="attr",
unit_divisor=1,
desc="color values"
) as bar:
for text in theme_text:
parser = etree.XMLParser(remove_blank_text=True)
tree = etree.parse(f"{config['src']}/{text['file_path']}", parser)
root = tree.getroot()
root.find(text['tag_path']).text = text['text']['to']
tree.write(
f"{config['src']}/{text['file_path']}",
pretty_print=True,
xml_declaration=True,
encoding="utf-8",
)
bar.update()
theme_attr = patch_config[theme]["attributes"]
theme_text = patch_config[theme]["text"]
theme_files = patch_config[theme]["files"]
for item in theme_attr:
parser = etree.XMLParser(remove_blank_text=True)
tree = etree.parse(
f"{config['folders']['decompiled']}/{item['file_path']}", parser
)
root = tree.getroot()
root.find(item["tag_path"]).set(item["attr_name"], item["attr_value"]["to"])
tree.write(
f"{config['folders']['decompiled']}/{item['file_path']}",
pretty_print=True,
xml_declaration=True,
encoding="utf-8",
)
log.debug(
f"[CHANGE_COLOR_THEME/ATTRIBUTES] set attribute `{item['attr_name']}` from `{item['attr_value']['from']}` to `{item['attr_value']['to']}`"
)
for item in theme_text:
parser = etree.XMLParser(remove_blank_text=True)
tree = etree.parse(
f"{config['folders']['decompiled']}/{item['file_path']}", parser
)
root = tree.getroot()
root.find(item["tag_path"]).text = item["text"]["to"]
tree.write(
f"{config['folders']['decompiled']}/{item['file_path']}",
pretty_print=True,
xml_declaration=True,
encoding="utf-8",
)
log.debug(
f"[CHANGE_COLOR_THEME/VALUES] set text from `{item['text']['from']}` to `{item['text']['to']}`"
)
if len(theme_files) > 0: if len(theme_files) > 0:
with tqdm( for item in theme_files:
total=len(theme_files), with open(
unit="files", f"{config['folders']['decompiled']}/{item['file_path']}",
unit_divisor=1, "w",
desc="color files" encoding="utf-8",
) as bar: ) as f:
for file in theme_files: f.write("\n".join(item["file_content"]))
with open(f"{config['src']}/{file['file_path']}", "w", encoding="utf-8") as f: log.debug(f"[CHANGE_COLOR_THEME/FILES] replaced file {item['file_path']}")
f.write("\n".join(file['file_content']))
bar.update() log.debug(f"[CHANGE_COLOR_THEME] color theme `{theme}` has been applied")
return True
return True

View file

@ -1,36 +1,51 @@
"""Change package name""" """Change package name"""
# patch settings
# priority, default: -100 (run last)
priority = -100 priority = -100
# imports
## bundled
import os import os
from typing import TypedDict from typing import TypedDict
## custom
from config import config, log
class PatchConfig_ChangePackageName(TypedDict): class PatchConfig_ChangePackageName(TypedDict):
src: str
new_package_name: str new_package_name: str
def rename_dir(src, dst): def rename_dir(src, dst):
os.makedirs(dst, exist_ok=True) os.makedirs(dst, exist_ok=True)
os.rename(src, dst) os.rename(src, dst)
def apply(config: dict) -> bool:
assert config["new_package_name"] is not None, "new_package_name is not configured"
for root, dirs, files in os.walk(f"{config['src']}"): def apply(patch_config: PatchConfig_ChangePackageName) -> bool:
assert (
patch_config["new_package_name"] is not None
), "new_package_name is not configured"
for root, dirs, files in os.walk(f"{config['folders']['decompiled']}"):
if len(files) < 0:
continue
dir_name = root.removeprefix(f"{config['folders']['decompiled']}/")
for filename in files: for filename in files:
file_path = os.path.join(root, filename) file_path = os.path.join(root, filename)
if os.path.isfile(file_path): if os.path.isfile(file_path):
try: try:
with open(file_path, "r", encoding="utf-8") as file: with open(file_path, "r", encoding="utf-8") as file:
file_contents = file.read() file_contents = file.read()
new_contents = file_contents.replace( new_contents = file_contents.replace(
"com.swiftsoft.anixartd", config["new_package_name"] "com.swiftsoft.anixartd", patch_config["new_package_name"]
) )
new_contents = new_contents.replace( new_contents = new_contents.replace(
"com/swiftsoft/anixartd", "com/swiftsoft/anixartd",
config["new_package_name"].replace(".", "/"), patch_config["new_package_name"].replace(".", "/"),
) )
with open(file_path, "w", encoding="utf-8") as file: with open(file_path, "w", encoding="utf-8") as file:
@ -38,22 +53,31 @@ def apply(config: dict) -> bool:
except: except:
pass pass
if os.path.exists(f"{config['src']}/smali/com/swiftsoft/anixartd"): if os.path.exists(
f"{config['folders']['decompiled']}/smali/com/swiftsoft/anixartd"
):
rename_dir( rename_dir(
f"{config['src']}/smali/com/swiftsoft/anixartd", f"{config['folders']['decompiled']}/smali/com/swiftsoft/anixartd",
os.path.join( os.path.join(
f"{config['src']}", "smali", config["new_package_name"].replace(".", "/") f"{config['folders']['decompiled']}",
"smali",
patch_config["new_package_name"].replace(".", "/"),
), ),
) )
if os.path.exists(f"{config['src']}/smali_classes2/com/swiftsoft/anixartd"): if os.path.exists(
f"{config['folders']['decompiled']}/smali_classes2/com/swiftsoft/anixartd"
):
rename_dir( rename_dir(
f"{config['src']}/smali_classes2/com/swiftsoft/anixartd", f"{config['folders']['decompiled']}/smali_classes2/com/swiftsoft/anixartd",
os.path.join( os.path.join(
f"{config['src']}", f"{config['folders']['decompiled']}",
"smali_classes2", "smali_classes2",
config["new_package_name"].replace(".", "/"), patch_config["new_package_name"].replace(".", "/"),
), ),
) )
return True log.debug(
f"[CHANGE_PACKAGE_NAME] package name has been changed to {patch_config['new_package_name']}"
)
return True

View file

@ -1,25 +1,45 @@
"""Remove unnecessary resources""" """Remove unnecessary resources"""
# patch settings
# priority, default: 0
priority = 0 priority = 0
from tqdm import tqdm
# imports
## bundled
import os import os
import shutil import shutil
from typing import TypedDict from typing import TypedDict
## installed
from rich.progress import track
## custom
from config import config, log, console
# Patch
class PatchConfig_Compress(TypedDict): class PatchConfig_Compress(TypedDict):
src: str
keep_dirs: list[str] keep_dirs: list[str]
def apply(config: PatchConfig_Compress) -> bool: def apply(patch_config: PatchConfig_Compress) -> bool:
for item in os.listdir(f"{config['src']}/unknown/"): path = f"{config['folders']['decompiled']}/unknown"
item_path = os.path.join(f"{config['src']}/unknown/", item) items = os.listdir(path)
for item in track(
items,
console=console,
description="[COMPRESS]",
total=len(items),
):
item_path = f"{path}/{item}"
if os.path.isfile(item_path): if os.path.isfile(item_path):
os.remove(item_path) os.remove(item_path)
tqdm.write(f"removed file: {item_path}") log.debug(f"[COMPRESS] removed file: {item_path}")
elif os.path.isdir(item_path): elif os.path.isdir(item_path):
if item not in config["keep_dirs"]: if item not in patch_config["keep_dirs"]:
shutil.rmtree(item_path) shutil.rmtree(item_path)
tqdm.write(f"removed directory: {item_path}") log.debug(f"[COMPRESS] removed directory: {item_path}")
log.debug(f"[COMPRESS] resources have been removed")
return True return True

View file

@ -1,8 +1,12 @@
"""Disable ad banners""" """Disable ad banners"""
# patch settings
# priority, default: 0
priority = 0 priority = 0
from typing import TypedDict
# imports
## custom
from config import config, log
from scripts.smali_parser import ( from scripts.smali_parser import (
find_smali_method_end, find_smali_method_end,
find_smali_method_start, find_smali_method_start,
@ -11,10 +15,7 @@ from scripts.smali_parser import (
) )
class PatchConfig_DisableAdBanner(TypedDict): # Patch
src: str
replace = """ .locals 0 replace = """ .locals 0
const/4 p0, 0x1 const/4 p0, 0x1
@ -23,9 +24,10 @@ replace = """ .locals 0
""" """
def apply(config: PatchConfig_DisableAdBanner) -> bool: def apply(__no_config__) -> bool:
path = f"{config['src']}/smali_classes2/com/swiftsoft/anixartd/Prefs.smali" path = f"{config['folders']['decompiled']}/smali_classes2/com/swiftsoft/anixartd/Prefs.smali"
lines = get_smali_lines(path) lines = get_smali_lines(path)
for index, line in enumerate(lines): for index, line in enumerate(lines):
if line.find("IS_SPONSOR") >= 0: if line.find("IS_SPONSOR") >= 0:
method_start = find_smali_method_start(lines, index) method_start = find_smali_method_start(lines, index)
@ -33,7 +35,8 @@ def apply(config: PatchConfig_DisableAdBanner) -> bool:
new_content = replace_smali_method_body( new_content = replace_smali_method_body(
lines, method_start, method_end, replace lines, method_start, method_end, replace
) )
with open(path, "w", encoding="utf-8") as file: with open(path, "w", encoding="utf-8") as file:
file.writelines(new_content) file.writelines(new_content)
log.debug(f"[DISABLE_AD] file {path} has been modified")
return True return True

View file

@ -1,22 +1,22 @@
"""Remove beta banner""" """Remove beta banner"""
# patch settings
# priority, default: 0
priority = 0 priority = 0
# imports
## bundled
import os import os
from tqdm import tqdm
## installed
from lxml import etree from lxml import etree
from typing import TypedDict ## custom
from config import config, log
class PatchConfig_DisableBetaBanner(TypedDict): def apply(__no_config__) -> bool:
src: str beta_banner_xml = f"{config['folders']['decompiled']}/res/layout/item_beta.xml"
def apply(config: PatchConfig_DisableBetaBanner) -> bool:
xml_ns = {
"android": "http://schemas.android.com/apk/res/android",
"app": "http://schemas.android.com/apk/res-auto",
}
attributes = [ attributes = [
"paddingTop", "paddingTop",
"paddingBottom", "paddingBottom",
@ -27,19 +27,23 @@ def apply(config: PatchConfig_DisableBetaBanner) -> bool:
"layout_marginTop", "layout_marginTop",
"layout_marginBottom", "layout_marginBottom",
"layout_marginStart", "layout_marginStart",
"layout_marginEnd" "layout_marginEnd",
] ]
beta_banner_xml = f"{config['src']}/res/layout/item_beta.xml"
if os.path.exists(beta_banner_xml): if os.path.exists(beta_banner_xml):
parser = etree.XMLParser(remove_blank_text=True) parser = etree.XMLParser(remove_blank_text=True)
tree = etree.parse(beta_banner_xml, parser) tree = etree.parse(beta_banner_xml, parser)
root = tree.getroot() root = tree.getroot()
for attr in attributes: for attr in attributes:
tqdm.write(f"set {attr} = 0.0dip") log.debug(
root.set(f"{{{xml_ns['android']}}}{attr}", "0.0dip") f"[DISABLE_BETA_BANNER] set attribute `{attr}` from `{root.get(attr)}` to `0.0dip`"
)
root.set(f"{{{config['xml_ns']['android']}}}{attr}", "0.0dip")
tree.write(beta_banner_xml, pretty_print=True, xml_declaration=True, encoding="utf-8") tree.write(
beta_banner_xml, pretty_print=True, xml_declaration=True, encoding="utf-8"
)
log.debug(f"[DISABLE_BETA_BANNER] file {beta_banner_xml} has been modified")
return True return True

View file

@ -1,5 +1,4 @@
requests requests
tqdm
lxml lxml
rich rich
beaupy beaupy

View file

@ -1,7 +1,28 @@
import requests
import os import os
from tqdm import tqdm import requests
from config import config, log import logging
from config import config, log, console
from rich.progress import (
BarColumn,
DownloadColumn,
Progress,
TextColumn,
TimeRemainingColumn,
TransferSpeedColumn,
)
progress = Progress(
TextColumn("[bold blue]{task.fields[filename]}", justify="right"),
BarColumn(bar_width=None),
"[progress.percentage]{task.percentage:>3.1f}%",
"",
DownloadColumn(),
"",
TransferSpeedColumn(),
"",
TimeRemainingColumn(),
console=console
)
def check_if_tool_exists(tool: str) -> bool: def check_if_tool_exists(tool: str) -> bool:
@ -19,27 +40,30 @@ def check_if_tool_exists(tool: str) -> bool:
else: else:
return True return True
requests_log = logging.getLogger("urllib3.connectionpool")
requests_log.setLevel(logging.WARNING)
def download_tool(url: str, tool: str): def download_tool(url: str, tool: str):
if not check_if_tool_exists(tool): if not check_if_tool_exists(tool):
log.info(f"downloading a tool: `{tool}`") progress.start()
try: try:
log.info(f"Requesting {url}")
response = requests.get(url, stream=True) response = requests.get(url, stream=True)
total = int(response.headers.get("content-length", 0)) total = int(response.headers.get("content-length", None))
with open(f"{config['folders']['tools']}/{tool}", "wb") as file, tqdm( task_id = progress.add_task("download", start=False, total=total, filename=tool)
desc=tool,
total=total, with open(f"{config['folders']['tools']}/{tool}", "wb") as file:
unit="iB", progress.start_task(task_id)
unit_scale=True, for bytes in response.iter_content(chunk_size=32768):
unit_divisor=1024,
) as bar:
for bytes in response.iter_content(chunk_size=8192):
size = file.write(bytes) size = file.write(bytes)
bar.update(size) progress.update(task_id, advance=size)
log.info(f"`{tool}` downloaded") log.info(f"`{tool}` downloaded")
except Exception as e: except Exception as e:
log.error(f"error while downloading `{tool}`: {e}") log.error(f"error while downloading `{tool}`: {e}")
progress.stop()
def check_and_download_all_tools(): def check_and_download_all_tools():
for tool in config["tools"]: for tool in config["tools"]:

View file

@ -1,8 +1,10 @@
import os, json import os, json
import importlib import importlib
from typing import TypedDict from typing import TypedDict
from beaupy import select_multiple from beaupy import select_multiple
from tqdm import tqdm from rich.progress import BarColumn, Progress, TextColumn
from config import config, log, console from config import config, log, console
@ -59,6 +61,14 @@ class PatchStatus(TypedDict):
status: bool status: bool
progress = Progress(
"[progress.description]{task.description}",
TextColumn(text_format="{task.fields[patch]}"),
BarColumn(bar_width=None),
"[blue]{task.completed}/{task.total}",
)
def apply_patches(patches: list[str]) -> list[PatchStatus]: def apply_patches(patches: list[str]) -> list[PatchStatus]:
modules = [] modules = []
statuses = [] statuses = []
@ -69,25 +79,27 @@ def apply_patches(patches: list[str]) -> list[PatchStatus]:
) )
modules.append(Patch(name, module)) modules.append(Patch(name, module))
modules.sort(key=lambda x: x.package.priority, reverse=True) modules.sort(key=lambda x: x.package.priority, reverse=True)
with tqdm( with progress:
total=len(modules), task = progress.add_task("applying patch:", total=len(modules), patch="")
unit="patch", for module in modules:
unit_divisor=1, progress.update(task, patch=module.name)
) as bar:
for patch in modules: patch_conf = {}
bar.set_description(f"{patch.name}") if os.path.exists(
conf = {} f"{config['folders']['patches']}/{module.name}.config.json"
if os.path.exists(f"{config['folders']['patches']}/{patch.name}.config.json"): ):
with open( with open(
f"{config['folders']['patches']}/{patch.name}.config.json", f"{config['folders']['patches']}/{module.name}.config.json",
"r", "r",
encoding="utf-8", encoding="utf-8",
) as conf: ) as f:
conf = json.loads(conf.read()) patch_conf = json.loads(f.read())
conf["src"] = config["folders"]["decompiled"]
status = patch.apply(conf) status = module.apply(patch_conf)
statuses.append({"name": patch.name, "status": status}) statuses.append({"name": module.name, "status": status})
bar.update() progress.update(task, advance=1)
progress.update(task, description="patches applied", patch="")
return statuses return statuses