9 Commits

17 changed files with 394 additions and 71 deletions
+65
View File
@@ -0,0 +1,65 @@
name: Build mod
on:
workflow_dispatch:
#schedule: # раз в 36 часов
# - cron: "0 0 */3 * *" # каждые 3 дня в 00:00
# - cron: "0 12 */3 * *" # каждые 3 дня в 12:00
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v3
- name: Download APK
run: |
curl -L -o app.apk "https://mirror-dl.anixart-app.com/anixart-beta.apk"
- name: Ensure aapt is installed
run: |
if ! command -v aapt &> /dev/null; then
echo "aapt не найден, устанавливаем..."
sudo apt-get update && sudo apt-get install -y --no-install-recommends android-sdk-build-tools
fi
- name: Export secrets
env:
KEYSTORE: ${{ secrets.KEYSTORE }}
KEYSTORE_PASS: ${{ secrets.KEYSTORE_PASS }}
run: |
# Export so later steps can reference them
echo "$KEYSTORE" | base64 -d > keystore.jks
echo "$KEYSTORE_PASS" > keystore.pass
- name: Build APK
id: build
run: |
mkdir original
mv app.apk original/
python ./main.py -f
- name: Read title from report.log
id: get_title
run: |
TITLE=$(head -n 1 modified/report.log)
echo "title=${TITLE}" >> $GITHUB_OUTPUT
- name: Setup go
if: steps.build.outputs.BUILD_EXIT == '0'
uses: actions/setup-go@v4
with:
go-version: '>=1.20'
- name: Make release
if: steps.build.outputs.BUILD_EXIT == '0'
uses: https://gitea.com/actions/release-action@main
with:
title: ${{ steps.get_title.outputs.title }}
body_path: modified/report.log
draft: true
api_key: '${{secrets.RELEASE_TOKEN}}'
files: |-
modified/**-mod.apk
modified/report.log
Vendored
+2
View File
@@ -5,3 +5,5 @@ tools
__pycache__ __pycache__
.venv .venv
*.jks
*.pass
+32
View File
@@ -5,6 +5,7 @@
--- ---
### Структура проекта: ### Структура проекта:
- `main.py` Главный файл - `main.py` Главный файл
- `patches` Модули патчей - `patches` Модули патчей
@@ -13,6 +14,37 @@
- `patches/resources` Ресурсы, используемые патчами - `patches/resources` Ресурсы, используемые патчами
- `todo_drafts` Заметки для новых патчей(можно в любом формате) - `todo_drafts` Заметки для новых патчей(можно в любом формате)
### Схема
```mermaid
---
title: Процесс модифицирования приложения
---
flowchart TD
A([Оригинальный apk]) f1@==> B[поиск и выбор apk]
B f2@==> p[Декомпиляция]
subgraph p["Применение патчей по возрастанию приоритета"]
C[Патч 1] --> D
D[Патч 2] --...--> E[Патч n]
end
p f3@==> F[Сборка apk обратно]
F f4@==> G[Выравнивание zipaling]
G f5@==> H[Подпись V2+V3]
H f6@==> I([Модифицированый apk])
f1@{ animate: true }
f2@{ animate: true }
f3@{ animate: true }
f4@{ animate: true }
f5@{ animate: true }
f6@{ animate: true }
```
### Установка и использование: ### Установка и использование:
1. Клонируйте репозиторий: 1. Клонируйте репозиторий:
+12 -4
View File
@@ -3,16 +3,17 @@
"apktool_jar_url": "https://bitbucket.org/iBotPeaches/apktool/downloads/apktool_2.12.0.jar", "apktool_jar_url": "https://bitbucket.org/iBotPeaches/apktool/downloads/apktool_2.12.0.jar",
"apktool_wrapper_url": "https://raw.githubusercontent.com/iBotPeaches/Apktool/master/scripts/linux/apktool" "apktool_wrapper_url": "https://raw.githubusercontent.com/iBotPeaches/Apktool/master/scripts/linux/apktool"
}, },
"new_package_name": "com.wowlikon.anixart", "new_package_name": "com.wowlikon.anixart2",
"server": "https://anixarty.wowlikon.tech/modding", "server": "https://anixarty.wowlikon.tech/modding",
"theme": { "theme": {
"colors": { "colors": {
"primary": "#ccff00", "primary": "#ccff00",
"secondary": "#ffffd700", "secondary": "#ffffd700",
"background": "#FFFFFF", "background": "#ffffff",
"text": "#000000" "text": "#000000"
}, },
"gradient": { "gradient": {
"angle": "135.0",
"from": "#ffff6060", "from": "#ffff6060",
"to": "#ffccff00" "to": "#ffccff00"
} }
@@ -29,10 +30,17 @@
"icon": "@drawable/ic_custom_telegram", "icon": "@drawable/ic_custom_telegram",
"icon_space_reserved": "false" "icon_space_reserved": "false"
}, },
{
"title": "Kentai Radiquum",
"description": "Разработчик",
"url": "https://t.me/radiquum",
"icon": "@drawable/ic_custom_telegram",
"icon_space_reserved": "false"
},
{ {
"title": "Мы в Telegram", "title": "Мы в Telegram",
"description": "Подпишитесь на канал, чтобы быть в курсе последних новостей.", "description": "Подпишитесь на канал, чтобы быть в курсе последних новостей.",
"url": "https://t.me/wowlikon", "url": "https://t.me/http_teapod",
"icon": "@drawable/ic_custom_telegram", "icon": "@drawable/ic_custom_telegram",
"icon_space_reserved": "false" "icon_space_reserved": "false"
} }
@@ -51,5 +59,5 @@
"android": "http://schemas.android.com/apk/res/android", "android": "http://schemas.android.com/apk/res/android",
"app": "http://schemas.android.com/apk/res-auto" "app": "http://schemas.android.com/apk/res-auto"
}, },
"speeds": [0.25, 0.5, 0.75, 1.0, 1.25, 1.5, 1.75, 2.0, 2.5, 3.0, 9.0] "speeds": [9.0]
} }
+104 -26
View File
@@ -1,9 +1,12 @@
import os import os
import sys import sys
import json import json
import yaml
import requests import requests
import argparse
import colorama import colorama
import importlib import importlib
import traceback
import subprocess import subprocess
from tqdm import tqdm from tqdm import tqdm
@@ -12,7 +15,7 @@ def init() -> dict:
if not os.path.exists(directory): if not os.path.exists(directory):
os.makedirs(directory) os.makedirs(directory)
with open("./patches/config.json", "r") as config_file: with open("./config.json", "r") as config_file:
conf = json.load(config_file) conf = json.load(config_file)
if not os.path.exists("./tools/apktool.jar"): if not os.path.exists("./tools/apktool.jar"):
@@ -61,6 +64,11 @@ def select_apk() -> str:
print("Нет файлов .apk в текущей директории") print("Нет файлов .apk в текущей директории")
sys.exit(1) sys.exit(1)
if len(apks) == 1:
apk = apks[0]
print(f"Выбран файл {apk}")
return apk
while True: while True:
print("Выберете файл для модификации") print("Выберете файл для модификации")
for index, apk in enumerate(apks): for index, apk in enumerate(apks):
@@ -101,6 +109,53 @@ def decompile_apk(apk: str):
sys.exit(1) sys.exit(1)
def compile_apk(apk: str):
print("Компилируем apk...")
try:
subprocess.run(
"tools/apktool b decompiled -o " + os.path.join("modified", apk),
shell=True,
check=True,
text=True,
stdout=subprocess.DEVNULL,
stderr=subprocess.PIPE,
)
subprocess.run(
"zipalign -v 4 " + os.path.join("modified", apk) + " " + os.path.join("modified", apk.replace(".apk", "-aligned.apk")),
shell=True,
check=True,
text=True,
stdout=subprocess.DEVNULL,
stderr=subprocess.PIPE,
)
subprocess.run(
"apksigner sign " +
"--v1-signing-enabled false " +
"--v2-signing-enabled true " +
"--v3-signing-enabled true " +
"--ks keystore.jks " +
"--ks-pass file:keystore.pass " +
"--out " + os.path.join("modified", apk.replace(".apk", "-mod.apk")) +
" " + os.path.join("modified", apk.replace(".apk", "-aligned.apk")),
shell=True,
check=True,
text=True,
stdout=subprocess.DEVNULL,
stderr=subprocess.PIPE,
)
title = "anixart mod "
with open('./decompiled/apktool.yml') as f:
package = yaml.safe_load(f)
title += ' '.join([f'{k}: {v}' for k, v in package['versionInfo'].items()])
with open("./modified/report.log", "w") as log_file:
log_file.write(title+'\n')
log_file.write("\n".join([f"{patch.name}: {'applied' if patch.applied else 'failed'}" for patch in patches]))
except subprocess.CalledProcessError as e:
print("Ошибка при выполнении команды:")
print(e.stderr)
sys.exit(1)
class Patch: class Patch:
def __init__(self, name, pkg): def __init__(self, name, pkg):
self.name = name self.name = name
@@ -117,36 +172,59 @@ class Patch:
return True return True
except Exception as e: except Exception as e:
print(f"Ошибка при применении патча {self.name}: {e}") print(f"Ошибка при применении патча {self.name}: {e}")
print(e.args) print(type(e), e.args)
traceback.print_exc()
return False return False
if __name__ == "__main__":
parser = argparse.ArgumentParser(
description="Автоматический патчер anixart"
)
conf = init() parser.add_argument("-v", "--verbose",
apk = select_apk() action="store_true",
patch = decompile_apk(apk) help="Выводить подробные сообщения")
patches = [] parser.add_argument("-f", "--force",
for filename in os.listdir("patches/"): action="store_true",
if filename.endswith(".py") and filename != "__init__.py": help="Принудительно собрать APK")
module_name = filename[:-3]
module = importlib.import_module(f"patches.{module_name}")
patches.append(Patch(module_name, module))
patches.sort(key=lambda x: x.package.priority, reverse=True) args = parser.parse_args()
for patch in tqdm(patches, colour="green", desc="Применение патчей"): conf = init()
tqdm.write(f"Применение патча: {patch.name}") apk = select_apk()
patch.apply(conf) patch = decompile_apk(apk)
statuses = {} if args.verbose: conf["verbose"] = True
for patch in patches:
statuses[patch.name] = patch.applied
marker = colorama.Fore.GREEN + "" if patch.applied else colorama.Fore.RED + ""
print(f"{marker}{colorama.Style.RESET_ALL} {patch.name}")
if all(statuses.values()): patches = []
print("Все патчи успешно применены") for filename in os.listdir("patches/"):
elif any(statuses.values()): if filename.endswith(".py") and filename != "__init__.py" and not filename.startswith("todo_"):
print("Некоторые патчи не были успешно применены") module_name = filename[:-3]
else: module = importlib.import_module(f"patches.{module_name}")
print("Ни один патч не был успешно применен") patches.append(Patch(module_name, module))
patches.sort(key=lambda x: x.package.priority, reverse=True)
for patch in tqdm(patches, colour="green", desc="Применение патчей"):
tqdm.write(f"Применение патча: {patch.name}")
patch.apply(conf)
statuses = {}
for patch in patches:
statuses[patch.name] = patch.applied
marker = colorama.Fore.GREEN + "" if patch.applied else colorama.Fore.RED + ""
print(f"{marker}{colorama.Style.RESET_ALL} {patch.name}")
if all(statuses.values()):
print(f"{colorama.Fore.GREEN}Все патчи успешно применены{colorama.Style.RESET_ALL}")
compile_apk(apk)
elif any(statuses.values()):
print(f"{colorama.Fore.YELLOW}{colorama.Style.RESET_ALL} Некоторые патчи не были успешно применены")
if args.force or input("Продолжить? (y/n): ").lower() == "y":
compile_apk(apk)
else:
print(colorama.Fore.RED + "Операция отменена" + colorama.Style.RESET_ALL)
else:
print(f"{colorama.Fore.RED}Ни один патч не был успешно применен{colorama.Style.RESET_ALL}")
sys.exit(1)
+31 -6
View File
@@ -5,11 +5,36 @@ from tqdm import tqdm
import json import json
import requests import requests
def apply(config: dict) -> bool: def apply(config: dict) -> bool:
response = requests.get(config['server']) response = requests.get(config['server'])
if response.status_code == 200: assert response.status_code == 200, f"Failed to fetch data {response.status_code} {response.text}"
for item in json.loads(response.text)["modding"]: new_api = json.loads(response.text)
tqdm.write(item) for item in new_api['modifications']:
return True tqdm.write(f"Изменение {item['file']}")
tqdm.write(f"Failed to fetch data {response.status_code} {response.text}") filepath = './decompiled/smali_classes2/com/swiftsoft/anixartd/network/api/'+item['file']
return False with open(filepath, 'r') as f:
content = f.read()
with open(filepath, 'w') as f:
if content.count(item['src']) == 0:
tqdm.write(f"Не найдено {item['src']}")
f.write(content.replace(item['src'], item['dst']))
tqdm.write(f"Изменение Github ссылки")
filepath = './decompiled/smali_classes2/com/swiftsoft/anixartd/utils/anixnet/GithubPagesNetFetcher.smali'
with open(filepath, 'r') as f:
content = f.read()
with open(filepath, 'w') as f:
f.write(content.replace('const-string v1, "https://anixhelper.github.io/pages/urls.json"', f'const-string v1, "{new_api["gh"]}"'))
content = ""
tqdm.write("Удаление динамического выбора сервера")
filepath = './decompiled/smali_classes2/com/swiftsoft/anixartd/DaggerApp_HiltComponents_SingletonC$SingletonCImpl$SwitchingProvider.smali'
with open(filepath, 'r') as f:
for line in f.readlines():
if "addInterceptor" in line: continue
content += line
with open(filepath, 'w') as f:
f.write(content)
return True
+5 -2
View File
@@ -5,15 +5,18 @@ from tqdm import tqdm
import os import os
import shutil import shutil
def apply(config: dict) -> bool: def apply(config: dict) -> bool:
for item in os.listdir("./decompiled/unknown/"): for item in os.listdir("./decompiled/unknown/"):
item_path = os.path.join("./decompiled/unknown/", item) item_path = os.path.join("./decompiled/unknown/", item)
if os.path.isfile(item_path): if os.path.isfile(item_path):
os.remove(item_path) os.remove(item_path)
tqdm.write(f'Удалён файл: {item_path}') if config.get("verbose", False):
tqdm.write(f'Удалён файл: {item_path}')
elif os.path.isdir(item_path): elif os.path.isdir(item_path):
if item not in config["cleanup"]["keep_dirs"]: if item not in config["cleanup"]["keep_dirs"]:
shutil.rmtree(item_path) shutil.rmtree(item_path)
tqdm.write(f'Удалена папка: {item_path}') if config.get("verbose", False):
tqdm.write(f'Удалена папка: {item_path}')
return True return True
+57 -2
View File
@@ -1,12 +1,19 @@
"""Change application theme""" """Change application theme"""
priority = 0 priority = 0
from tqdm import tqdm
from lxml import etree from lxml import etree
from utils.public import (
insert_after_public,
insert_after_color,
insert_after_id,
change_color,
)
def apply(config: dict) -> bool: def apply(config: dict) -> bool:
main_color = config["theme"]["colors"]["primary"] main_color = config["theme"]["colors"]["primary"]
splash_color = config["theme"]["colors"]["secondary"] splash_color = config["theme"]["colors"]["secondary"]
gradient_angle = config["theme"]["gradient"]["angle"]
gradient_from = config["theme"]["gradient"]["from"] gradient_from = config["theme"]["gradient"]["from"]
gradient_to = config["theme"]["gradient"]["to"] gradient_to = config["theme"]["gradient"]["to"]
@@ -31,6 +38,7 @@ def apply(config: dict) -> bool:
root = tree.getroot() root = tree.getroot()
# Change attributes with namespace # Change attributes with namespace
root.set(f"{{{config['xml_ns']['android']}}}angle", gradient_angle)
root.set(f"{{{config['xml_ns']['android']}}}startColor", gradient_from) root.set(f"{{{config['xml_ns']['android']}}}startColor", gradient_from)
root.set(f"{{{config['xml_ns']['android']}}}endColor", gradient_to) root.set(f"{{{config['xml_ns']['android']}}}endColor", gradient_to)
@@ -45,7 +53,7 @@ def apply(config: dict) -> bool:
root = tree.getroot() root = tree.getroot()
# Finding "path" # Finding "path"
for el in root.findall("path", namespaces=config['xml_ns']): for el in root.findall("path", namespaces=config["xml_ns"]):
name = el.get(f"{{{config['xml_ns']['android']}}}name") name = el.get(f"{{{config['xml_ns']['android']}}}name")
if name == "path": if name == "path":
el.set(f"{{{config['xml_ns']['android']}}}fillColor", splash_color) el.set(f"{{{config['xml_ns']['android']}}}fillColor", splash_color)
@@ -53,4 +61,51 @@ def apply(config: dict) -> bool:
# Save back # Save back
tree.write(file_path, pretty_print=True, xml_declaration=True, encoding="utf-8") tree.write(file_path, pretty_print=True, xml_declaration=True, encoding="utf-8")
for filename in ["$ic_launcher_foreground__0", "$ic_banner_foreground__0"]:
file_path = f"./decompiled/res/drawable-v24/{filename}.xml"
parser = etree.XMLParser(remove_blank_text=True)
tree = etree.parse(file_path, parser)
root = tree.getroot()
# Change attributes with namespace
root.set(f"{{{config['xml_ns']['android']}}}angle", gradient_angle)
items = root.findall("item", namespaces=config['xml_ns'])
assert len(items) == 2
items[0].set(f"{{{config['xml_ns']['android']}}}color", gradient_from)
items[1].set(f"{{{config['xml_ns']['android']}}}color", gradient_to)
# Save back
tree.write(file_path, pretty_print=True, xml_declaration=True, encoding="utf-8")
insert_after_public("carmine", "custom_color")
insert_after_public("carmine_alpha_10", "custom_color_alpha_10")
insert_after_color("carmine", "custom_color", main_color[0]+'ff'+main_color[1:])
insert_after_color("carmine_alpha_10", "custom_color_alpha_10", main_color[0]+'1a'+main_color[1:])
change_color("accent_alpha_10", main_color[0]+'1a'+main_color[1:])
change_color("accent_alpha_20", main_color[0]+'33'+main_color[1:])
change_color("accent_alpha_50", main_color[0]+'80'+main_color[1:])
change_color("accent_alpha_70", main_color[0]+'b3'+main_color[1:])
change_color("colorAccent", main_color[0]+'ff'+main_color[1:])
change_color("link_color", main_color[0]+'ff'+main_color[1:])
change_color("link_color_alpha_70", main_color[0]+'b3'+main_color[1:])
change_color("refresh_progress", main_color[0]+'ff'+main_color[1:])
change_color("ic_launcher_background", "#ff000000")
change_color("bottom_nav_indicator_active", "#ffffffff")
change_color("bottom_nav_indicator_icon_checked", main_color[0]+'ff'+main_color[1:])
change_color("bottom_nav_indicator_label_checked", main_color[0]+'ff'+main_color[1:])
insert_after_public("warning_error_counter_background", "ic_custom_telegram")
insert_after_public("warning_error_counter_background", "ic_custom_crown")
try:
last = "speed75"
for speed in config.get("speeds", []):
insert_after_public(last, f"speed{int(float(speed)*10)}")
insert_after_id(last, f"speed{int(float(speed)*10)}")
last = f"speed{int(float(speed)*10)}"
except Exception as e:
print(f"Error occurred while processing speeds: {e}")
return True return True
+5 -7
View File
@@ -1,5 +1,4 @@
"""Compress PNGs""" """Compress PNGs"""
priority = -1 priority = -1
from tqdm import tqdm from tqdm import tqdm
@@ -7,15 +6,15 @@ import os
import subprocess import subprocess
def compress_pngs(root_dir): def compress_pngs(root_dir: str, verbose: bool = False):
compressed_files = [] compressed_files = []
for dirpath, _, filenames in os.walk(root_dir): for dirpath, _, filenames in os.walk(root_dir):
for filename in filenames: for filename in filenames:
if filename.lower().endswith(".png"): if filename.lower().endswith(".png"):
filepath = os.path.join(dirpath, filename) filepath = os.path.join(dirpath, filename)
tqdm.write(f"Сжимаю: {filepath}") if verbose: tqdm.write(f"Сжимаю: {filepath}")
try: try:
subprocess.run( assert subprocess.run(
[ [
"pngquant", "pngquant",
"--force", "--force",
@@ -24,9 +23,8 @@ def compress_pngs(root_dir):
"--quality=65-90", "--quality=65-90",
filepath, filepath,
], ],
check=True,
capture_output=True, capture_output=True,
) ).returncode in [0, 99]
compressed_files.append(filepath) compressed_files.append(filepath)
except subprocess.CalledProcessError as e: except subprocess.CalledProcessError as e:
tqdm.write(f"Ошибка при сжатии {filepath}: {e}") tqdm.write(f"Ошибка при сжатии {filepath}: {e}")
@@ -34,5 +32,5 @@ def compress_pngs(root_dir):
def apply(config: dict) -> bool: def apply(config: dict) -> bool:
files = compress_pngs("./decompiled") files = compress_pngs("./decompiled", config.get("verbose", False))
return len(files) > 0 and any(files) return len(files) > 0 and any(files)
+1 -1
View File
@@ -1,5 +1,4 @@
"""Disable ad banners""" """Disable ad banners"""
priority = 0 priority = 0
from utils.smali_parser import ( from utils.smali_parser import (
@@ -9,6 +8,7 @@ from utils.smali_parser import (
replace_smali_method_body, replace_smali_method_body,
) )
replace = """ .locals 0 replace = """ .locals 0
const/4 p0, 0x1 const/4 p0, 0x1
+2 -4
View File
@@ -1,11 +1,9 @@
"""Remove beta banner""" """Remove beta banner"""
priority = 0 priority = 0
import os
from tqdm import tqdm from tqdm import tqdm
from lxml import etree
from typing import TypedDict import os
from lxml import etree
def apply(config) -> bool: def apply(config) -> bool:
-2
View File
@@ -1,7 +1,5 @@
"""Insert new files""" """Insert new files"""
priority = 0 priority = 0
from tqdm import tqdm
import shutil import shutil
import os import os
+11 -2
View File
@@ -1,9 +1,8 @@
"""Change package name of apk""" """Change package name of apk"""
priority = -1 priority = -1
from tqdm import tqdm
import os import os
from lxml import etree
def rename_dir(src, dst): def rename_dir(src, dst):
@@ -88,6 +87,16 @@ def apply(config: dict) -> bool:
except: except:
pass pass
file_path = "./decompiled/res/layout/fragment_sign_in.xml"
parser = etree.XMLParser(remove_blank_text=True)
tree = etree.parse(file_path, parser)
root = tree.getroot()
last_linear = root.xpath("//LinearLayout/LinearLayout[4]")[0]
last_linear.set(f"{{{config['xml_ns']['android']}}}visibility", "gone")
tree.write(file_path, pretty_print=True, xml_declaration=True, encoding="utf-8")
return True return True
+1 -2
View File
@@ -1,10 +1,9 @@
"""Add new settings""" """Add new settings"""
priority = 0 priority = 0
from tqdm import tqdm
from lxml import etree from lxml import etree
# Generate PreferenceCategory
def make_category(ns, name, items): def make_category(ns, name, items):
cat = etree.Element("PreferenceCategory", nsmap=ns) cat = etree.Element("PreferenceCategory", nsmap=ns)
cat.set(f"{{{ns['android']}}}title", name) cat.set(f"{{{ns['android']}}}title", name)
@@ -1,9 +1,6 @@
"""Change application icon""" """Change application icon"""
priority = 0 priority = 0
from tqdm import tqdm
import time
def apply(config: dict) -> bool: def apply(config: dict) -> bool:
time.sleep(0.2)
return False return False
@@ -1,7 +1,5 @@
"""Change application icon""" """Change application icon"""
priority = 0 priority = 0
from tqdm import tqdm
import struct import struct
+65 -7
View File
@@ -2,15 +2,15 @@ from lxml import etree
from copy import deepcopy from copy import deepcopy
def insert_after(anchor_name: str, elem_name: str): def insert_after_public(anchor_name: str, elem_name: str):
file_path = "../decompiled/res/values/public.xml" file_path = "./decompiled/res/values/public.xml"
parser = etree.XMLParser(remove_blank_text=True) parser = etree.XMLParser(remove_blank_text=True)
tree = etree.parse(file_path, parser) tree = etree.parse(file_path, parser)
root = tree.getroot() root = tree.getroot()
anchor = None anchor = None
types = {} types = {}
for idx, elem in enumerate(root): for elem in root:
assert elem.tag == "public" assert elem.tag == "public"
assert elem.keys() == ["type", "name", "id"] assert elem.keys() == ["type", "name", "id"]
attrs = dict(zip(elem.keys(), elem.values())) attrs = dict(zip(elem.keys(), elem.values()))
@@ -25,7 +25,6 @@ def insert_after(anchor_name: str, elem_name: str):
if i not in group: if i not in group:
free_ids.add(i) free_ids.add(i)
assert len(free_ids) > 0
new_id = None new_id = None
for i in free_ids: for i in free_ids:
if i > int(anchor[1]["id"], 16): if i > int(anchor[1]["id"], 16):
@@ -38,12 +37,71 @@ def insert_after(anchor_name: str, elem_name: str):
if name == anchor[1]["type"]: if name == anchor[1]["type"]:
continue continue
if new_id in group: if new_id in group:
new_id = max(group) assert False, f"ID {new_id} already exists in group {name}"
new_elem = deepcopy(anchor[0]) new_elem = deepcopy(anchor[0])
new_elem.set("id", new_id) new_elem.set("id", hex(new_id))
new_elem.set("name", elem_name) new_elem.set("name", elem_name)
anchor[0].addnext(new_elem) anchor[0].addnext(new_elem)
tree.write(file_path, pretty_print=True, xml_declaration=True, encoding="utf-8") tree.write(file_path, pretty_print=True, xml_declaration=True, encoding="utf-8")
return new_id return new_id
def insert_after_id(anchor_name: str, elem_name: str):
file_path = "./decompiled/res/values/ids.xml"
parser = etree.XMLParser(remove_blank_text=True)
tree = etree.parse(file_path, parser)
root = tree.getroot()
anchor = None
for elem in root:
assert elem.tag == "item"
assert elem.keys() == ["type", "name"]
attrs = dict(zip(elem.keys(), elem.values()))
if attrs["name"] == anchor_name:
assert anchor == None
anchor = (elem, attrs)
new_elem = deepcopy(anchor[0])
new_elem.set("name", elem_name)
anchor[0].addnext(new_elem)
tree.write(file_path, pretty_print=True, xml_declaration=True, encoding="utf-8")
def change_color(name: str, value: str):
file_path = "./decompiled/res/values/colors.xml"
parser = etree.XMLParser(remove_blank_text=True)
tree = etree.parse(file_path, parser)
root = tree.getroot()
replacements = 0
for elem in root:
assert elem.tag == "color"
assert elem.keys() == ["name"]
attrs = dict(zip(elem.keys(), elem.values()))
if attrs["name"] == name:
elem.set("name", name)
elem.text = value
replacements += 1
assert replacements >= 1
tree.write(file_path, pretty_print=True, xml_declaration=True, encoding="utf-8")
def insert_after_color(anchor_name: str, elem_name: str, elem_value: str):
file_path = "./decompiled/res/values/colors.xml"
parser = etree.XMLParser(remove_blank_text=True)
tree = etree.parse(file_path, parser)
root = tree.getroot()
anchor = None
for elem in root:
assert elem.tag == "color"
assert elem.keys() == ["name"]
attrs = dict(zip(elem.keys(), elem.values()))
if attrs["name"] == anchor_name:
assert anchor == None
anchor = (elem, attrs)
new_elem = deepcopy(anchor[0])
new_elem.set("name", elem_name)
anchor[0].addnext(new_elem)
tree.write(file_path, pretty_print=True, xml_declaration=True, encoding="utf-8")