11 Commits

Author SHA1 Message Date
19e1ce2f45 Патч для выделения информации о релизе 2025-10-02 10:32:51 +03:00
fbc8b3e017 Обновление структуры проекта, использование pydantic для конфигураций, улучшение отчёта о патчах
Build mod / build (push) Successful in 6m39s
2025-10-01 23:07:37 +03:00
5ba590cc31 Обновить README.md 2025-09-28 08:40:25 +00:00
0a4aa544a2 Обновить README.md 2025-09-28 08:24:57 +00:00
40f9cf0307 Обновить README.md 2025-09-28 08:22:32 +00:00
8b8ca63bb1 Удаление ненужного 2025-09-22 09:39:19 +03:00
670c53ba69 Перенос resources и добавления assets в патч настроек 2025-09-22 09:35:51 +03:00
5ff882a8d5 Рефакторинг патчей, реализация Список патчей:
settings_urls: ✔ enabled
  disable_ad: ✔ enabled
  disable_beta_banner: ✔ enabled
  insert_new: ✔ enabled
  color_theme: ✔ enabled
  change_server: ✘ disabled
  package_name: ✔ enabled
  replace_navbar: ✔ enabled
  compress: ✔ enabled, обновление описаний
2025-09-20 23:00:00 +03:00
66336f3a5c Обновление ссылки 2001-01-01 00:00:00 +00:00
85aef3d997 Добавлеие вывода информации о включеных патчах
Build mod / build (push) Successful in 5m27s
2025-09-18 10:54:30 +03:00
41399eca2c Запуск сборки на создание тэга
Build mod / build (push) Successful in 5m1s
2025-09-15 23:34:24 +03:00
44 changed files with 606 additions and 371 deletions
+5 -1
View File
@@ -2,6 +2,9 @@ name: Build mod
on:
workflow_dispatch:
push:
tags:
- 'v*'
#schedule: # раз в 36 часов
# - cron: "0 0 */3 * *" # каждые 3 дня в 00:00
# - cron: "0 12 */3 * *" # каждые 3 дня в 12:00
@@ -57,6 +60,7 @@ jobs:
id: get_title
run: |
TITLE=$(head -n 1 modified/report.log)
tail -n +2 modified/report.log > modified/report.log.tmp
echo "title=${TITLE}" >> $GITHUB_OUTPUT
- name: Setup go
@@ -70,7 +74,7 @@ jobs:
uses: https://gitea.com/actions/release-action@main
with:
title: ${{ steps.get_title.outputs.title }}
body_path: modified/report.log
body_path: modified/report.log.tmp
draft: true
api_key: '${{secrets.RELEASE_TOKEN}}'
files: |-
+5 -5
View File
@@ -11,7 +11,7 @@
- `patches` Модули патчей
- `utils` Вспомогательные модули
- `tools` Инструменты для модификации
- `patches/resources` Ресурсы, используемые патчами
- `resources` Ресурсы, используемые патчами
- `todo_drafts` Заметки для новых патчей(можно в любом формате)
### Схема
@@ -52,7 +52,7 @@ flowchart TD
git clone https://git.wowlikon.tech/anixart-mod/patcher.git
```
Требования:
- Python 3.6+
- Python 3.8+
- Java 8+
- zipalign
- apksigner
@@ -76,6 +76,6 @@ flowchart TD
Этот проект лицензирован под лицензией MIT. См. [LICENSE](./LICENSE) для получения подробной информации.
### Вклад в проект:
- Seele - Оригинальные патчи в начале разработки основаны на модификации от Seele [[GitHub](https://github.com/seeleme) | [Telegram](https://t.me/seele_off)]
- Kentai Radiquum - Значительный вклад в развитие патчера, разработка [anix](https://github.com/AniX-org/AniX) и помощь с API [[GitHub](https://github.com/Radiquum) | [Telegram](https://t.me/radiquum)]
- ReCode Liner - Помощь в модификации приложения [[Telegram](https://t.me/recodius)]
- Kentai Radiquum - Значительный вклад в развитие патчера, разработка [anix](https://github.com/AniX-org/AniX) и помощь с API [[GitHub](https://github.com/adiquum) | [Telegram](https://t.me/radiquum)]
- Seele - Оригинальные патчи в начале разработки основаны на модификации от Seele
- ReCode Liner - Помощь в изучении моддинга приложения [[Telegram](https://t.me/recodius)]
+1 -74
View File
@@ -1,85 +1,12 @@
{
"base": {
"tools": {
"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"
},
"base": {
"xml_ns": {
"android": "http://schemas.android.com/apk/res/android",
"app": "http://schemas.android.com/apk/res-auto"
}
},
"patches": {
"package_name": {
"new_package_name": "com.wowlikon.anixart"
},
"compress": {
"remove_language_files": true,
"remove_AI_voiceover": true,
"remove_debug_lines": true,
"remove_drawable_files": false,
"remove_unknown_files": true,
"remove_unknown_files_keep_dirs": ["META-INF", "kotlin"],
"compress_png_files": true
},
"change_server": {
"server": "https://anixarty.wowlikon.tech/modding"
},
"color_theme": {
"colors": {
"primary": "#ccff00",
"secondary": "#ffffd700",
"background": "#ffffff",
"text": "#000000"
},
"gradient": {
"angle": "135.0",
"from": "#ffff6060",
"to": "#ffccff00"
}
},
"replace_navbar": {
"items": ["home", "discover", "feed", "bookmarks", "profile"]
},
"custom_speed": {
"speeds": [9.0]
},
"settings_urls": {
"menu": {
"Мы в социальных сетях": [
{
"title": "wowlikon",
"description": "Разработчик",
"url": "https://t.me/wowlikon",
"icon": "@drawable/ic_custom_telegram",
"icon_space_reserved": "false"
},
{
"title": "Kentai Radiquum",
"description": "Разработчик",
"url": "https://t.me/radiquum",
"icon": "@drawable/ic_custom_telegram",
"icon_space_reserved": "false"
},
{
"title": "Мы в Telegram",
"description": "Подпишитесь на канал, чтобы быть в курсе последних новостей.",
"url": "https://t.me/http_teapod",
"icon": "@drawable/ic_custom_telegram",
"icon_space_reserved": "false"
}
],
"Прочее": [
{
"title": "Помочь проекту",
"description": "Вы можете помочь нам в разработке мода, написании кода или тестировании.",
"url": "https://t.me/wowlikon",
"icon": "@drawable/ic_custom_crown",
"icon_space_reserved": "false"
}
]
},
"version": " by wowlikon"
}
}
}
+1
View File
@@ -0,0 +1 @@
{"enabled":true,"server":"https://anixarty.0x174.su/patch"}
+1
View File
@@ -0,0 +1 @@
{"enabled":true,"logo":{"gradient":{"angle":0.0,"start_color":"#ffccff00","end_color":"#ffcccc00"},"ears_color":"#ffd0d0d0"},"colors":{"primary":"#ccff00","secondary":"#ffcccc00","background":"#ffffff","text":"#000000"}}
+1
View File
@@ -0,0 +1 @@
{"enabled":true,"remove_language_files":true,"remove_AI_voiceover":true,"remove_debug_lines":false,"remove_drawable_files":false,"remove_unknown_files":true,"remove_unknown_files_keep_dirs":["META-INF","kotlin"],"compress_png_files":true}
+1
View File
@@ -0,0 +1 @@
{"enabled":true}
+1
View File
@@ -0,0 +1 @@
{"enabled":true}
+1
View File
@@ -0,0 +1 @@
{"enabled":true}
+1
View File
@@ -0,0 +1 @@
{"enabled":true,"package_name":"com.wowlikon.anixart"}
+1
View File
@@ -0,0 +1 @@
{"enabled":true,"items":["home","discover","feed","bookmarks","profile"]}
+1
View File
@@ -0,0 +1 @@
{"enabled":true}
+1
View File
@@ -0,0 +1 @@
{"enabled":true,"version":" by wowlikon","menu":{"Мы в социальных сетях":[{"title":"wowlikon","description":"Разработчик","url":"https://t.me/wowlikon","icon":"@drawable/ic_custom_telegram","icon_space_reserved":"false"},{"title":"Kentai Radiquum","description":"Разработчик","url":"https://t.me/radiquum","icon":"@drawable/ic_custom_telegram","icon_space_reserved":"false"},{"title":"Мы в Telegram","description":"Подпишитесь на канал, чтобы быть в курсе последних новостей.","url":"https://t.me/http_teapod","icon":"@drawable/ic_custom_telegram","icon_space_reserved":"false"}],"Прочее":[{"title":"Помочь проекту","description":"Вы можете помочь нам в разработке мода, написании кода или тестировании.","url":"https://git.wowlikon.tech/anixart-mod","icon":"@drawable/ic_custom_crown","icon_space_reserved":"false"}]}}
+89 -130
View File
@@ -1,93 +1,45 @@
from pathlib import Path
from typing import List
from typing import List, Dict, Any
import httpx
import typer
import importlib
import traceback
import yaml
from pydantic import BaseModel, ValidationError
from plumbum import local, ProcessExecutionError
from rich.console import Console
from rich.progress import Progress
from rich.prompt import Prompt
from rich.table import Table
from utils.config import *
from utils.tools import *
# --- Paths ---
TOOLS = Path("tools")
ORIGINAL = Path("original")
MODIFIED = Path("modified")
DECOMPILED = Path("decompiled")
PATCHES = Path("patches")
console = Console()
app = typer.Typer()
# ======================= CONFIG =========================
class ToolsConfig(BaseModel):
apktool_jar_url: str
apktool_wrapper_url: str
class XmlNamespaces(BaseModel):
android: str
app: str
class BaseSection(BaseModel):
tools: ToolsConfig
xml_ns: XmlNamespaces
class Config(BaseModel):
base: BaseSection
patches: dict
def load_config() -> Config:
# ======================= PATCHING =========================
class Patch:
def __init__(self, name: str, module):
self.name = name
self.module = module
self.applied = False
self.priority = getattr(module, "priority", 0)
try:
return Config.model_validate_json(Path("config.json").read_text())
except FileNotFoundError:
console.print("[red]Файл config.json не найден")
raise typer.Exit(1)
except ValidationError as e:
console.print("[red]Ошибка валидации config.json:", e)
raise typer.Exit(1)
self.config = module.Config.model_validate_json((CONFIGS / f"{name}.json").read_text())
except Exception as e:
console.print(f"[red]Ошибка при загрузке конфигурации патча {name}: {e}")
self.config = module.Config()
# ======================= UTILS =========================
def ensure_dirs():
for d in [TOOLS, ORIGINAL, MODIFIED, DECOMPILED, PATCHES]:
d.mkdir(exist_ok=True)
def run(cmd: List[str], hide_output=True):
prog = local[cmd[0]][cmd[1:]]
def apply(self, conf: Dict[str, Any]) -> bool:
try:
prog() if hide_output else prog & FG
except ProcessExecutionError as e:
console.print(f"[red]Ошибка при выполнении команды: {' '.join(cmd)}")
console.print(e.stderr)
raise typer.Exit(1)
def download(url: str, dest: Path):
console.print(f"[cyan]Скачивание {url}{dest.name}")
with httpx.Client(follow_redirects=True, timeout=60.0) as client:
with client.stream("GET", url) as response:
response.raise_for_status()
total = int(response.headers.get("Content-Length", 0))
dest.parent.mkdir(parents=True, exist_ok=True)
with open(dest, "wb") as f, Progress(console=console) as progress:
task = progress.add_task("Загрузка", total=total if total else None)
for chunk in response.iter_bytes(chunk_size=8192):
f.write(chunk)
progress.update(task, advance=len(chunk))
self.applied = bool(self.module.apply(self.config, conf))
return self.applied
except Exception as e:
console.print(f"[red]Ошибка в патче {self.name}: {e}")
traceback.print_exc()
return False
# ======================= INIT =========================
@@ -95,13 +47,20 @@ def download(url: str, dest: Path):
def init():
"""Создание директорий и скачивание инструментов"""
ensure_dirs()
conf = load_config()
conf = load_config(console)
for f in PATCHES.glob("*.py"):
if f.name.startswith("todo_") or f.name == "__init__.py":
continue
patch = Patch(f.stem, __import__(f"patches.{f.stem}", fromlist=[""]))
json_string = patch.config.model_dump_json()
(CONFIGS / f"{patch.name}.json").write_text(json_string)
if not (TOOLS / "apktool.jar").exists():
download(conf.base.tools.apktool_jar_url, TOOLS / "apktool.jar")
download(console, conf.tools.apktool_jar_url, TOOLS / "apktool.jar")
if not (TOOLS / "apktool").exists():
download(conf.base.tools.apktool_wrapper_url, TOOLS / "apktool")
download(console, conf.tools.apktool_wrapper_url, TOOLS / "apktool")
(TOOLS / "apktool").chmod(0o755)
try:
@@ -112,22 +71,26 @@ def init():
raise typer.Exit(1)
# ======================= PATCHING =========================
class Patch:
def __init__(self, name: str, module):
self.name = name
self.module = module
self.applied = False
self.priority = getattr(module, "priority", 0)
def apply(self, conf: dict) -> bool:
try:
self.applied = bool(self.module.apply(conf))
return self.applied
except Exception as e:
console.print(f"[red]Ошибка в патче {self.name}: {e}")
traceback.print_exc()
return False
# ======================= INFO =========================
@app.command()
def info(patch_name: str = ""):
"""Вывод информации о патче"""
conf = load_config(console).model_dump()
if patch_name:
patch = Patch(patch_name, __import__(f"patches.{patch_name}", fromlist=[""]))
console.print(f"[green]Информация о патче {patch.name}:")
console.print(f" [yellow]Приоритет: {patch.priority}")
console.print(f" [yellow]Описание: {patch.module.__doc__}")
else:
console.print("[cyan]Список патчей:")
for f in PATCHES.glob("*.py"):
if f.name.startswith("todo_") or f.name == "__init__.py":
continue
name = f.stem
if conf["patches"].get(name, {}).get("enabled", True):
console.print(f" [yellow]{name}: [green]✔ enabled")
else:
console.print(f" [yellow]{name}: [red]✘ disabled")
def select_apk() -> Path:
@@ -151,14 +114,12 @@ def select_apk() -> Path:
def decompile(apk: Path):
console.print("[yellow]Декомпиляция apk...")
run(
console,
[
"java",
"-jar",
str(TOOLS / "apktool.jar"),
"d",
"-f",
"-o",
str(DECOMPILED),
"-jar", str(TOOLS / "apktool.jar"),
"d", "-f",
"-o", str(DECOMPILED),
str(apk),
]
)
@@ -166,52 +127,51 @@ def decompile(apk: Path):
def compile(apk: Path, patches: List[Patch]):
console.print("[yellow]Сборка apk...")
out_apk = MODIFIED / apk.name
with open(DECOMPILED / "apktool.yml", encoding="utf-8") as f:
meta = yaml.safe_load(f)
version_info = meta.get("versionInfo", {})
version_code = version_info.get("versionCode", 0)
version_name = version_info.get("versionName", "unknown")
filename_version = version_name.lower().replace(" ", "-").replace(".", "-")
out_apk = MODIFIED / f"Anixarty-mod-v{filename_version}.apk"
aligned = out_apk.with_stem(out_apk.stem + "-aligned")
signed = out_apk.with_stem(out_apk.stem + "-mod")
run(
console,
[
"java",
"-jar",
str(TOOLS / "apktool.jar"),
"b",
str(DECOMPILED),
"-o",
str(out_apk),
"-jar", str(TOOLS / "apktool.jar"),
"b", str(DECOMPILED),
"-o", str(out_apk),
]
)
run(["zipalign", "-v", "4", str(out_apk), str(aligned)])
run(
console,
["zipalign", "-v", "4", str(out_apk), str(aligned)]
)
run(
console,
[
"apksigner",
"sign",
"--v1-signing-enabled",
"false",
"--v2-signing-enabled",
"true",
"--v3-signing-enabled",
"true",
"--ks",
"keystore.jks",
"--ks-pass",
"file:keystore.pass",
"--out",
str(signed),
"apksigner", "sign",
"--v1-signing-enabled", "false",
"--v2-signing-enabled", "true",
"--v3-signing-enabled", "true",
"--ks", "keystore.jks",
"--ks-pass", "file:keystore.pass",
"--out", str(signed),
str(aligned),
]
)
with open(DECOMPILED / "apktool.yml", encoding="utf-8") as f:
meta = yaml.safe_load(f)
version_str = " ".join(
f"{k}:{v}" for k, v in meta.get("versionInfo", {}).items()
)
console.print("[green]✔ APK успешно собран и подписан")
with open(MODIFIED / "report.log", "w", encoding="utf-8") as f:
f.write(f"anixart mod {version_str}\n")
f.write(f"Anixarty mod v {version_name} ({version_code})\n")
for p in patches:
f.write(f"{p.name}: {'applied' if p.applied else 'failed'}\n")
f.write(f"{'' if p.applied else ''} {p.name}\n")
@app.command()
@@ -220,22 +180,21 @@ def build(
verbose: bool = typer.Option(False, "--verbose", "-v", help="Подробный вывод"),
):
"""Декомпиляция, патчи и сборка apk"""
conf = load_config().model_dump()
conf = load_config(console)
apk = select_apk()
decompile(apk)
patch_settings = conf.get("patches", {})
patch_objs: List[Patch] = []
conf.base |= {"verbose": verbose}
for f in PATCHES.glob("*.py"):
if f.name.startswith("todo_") or f.name == "__init__.py":
continue
name = f.stem
settings = patch_settings.get(name, {})
if not settings.get("enable", True):
module = importlib.import_module(f"patches.{name}")
if not module.Config.model_validate_json((CONFIGS / f"{name}.json").read_text()):
console.print(f"[yellow]≫ Пропускаем {name}")
continue
module = importlib.import_module(f"patches.{name}")
patch_objs.append(Patch(name, module))
patch_objs.sort(key=lambda p: p.priority, reverse=True)
@@ -244,7 +203,7 @@ def build(
with Progress() as progress:
task = progress.add_task("Патчи", total=len(patch_objs))
for p in patch_objs:
ok = p.apply(patch_settings.get(p.name, {}) | conf.get("base", {}))
ok = p.apply(conf.base)
progress.console.print(f"{'' if ok else ''} {p.name}")
progress.advance(task)
+22 -4
View File
@@ -1,26 +1,40 @@
"""
Заменяет сервер api
Заменяет сервер api
"change_server": {
"server": "https://anixarty.wowlikon.tech/modding"
"enabled": true,
"server": "https://anixarty.0x174.su/patch"
}
"""
priority = 0
# imports
import json
import requests
from tqdm import tqdm
from typing import Dict, Any
from pydantic import Field
from utils.config import PatchConfig
def apply(config: dict) -> bool:
response = requests.get(config['server'])
#Config
class Config(PatchConfig):
server: str = Field("https://anixarty.0x174.su/patch", description="URL сервера")
# Patch
def apply(config: Config, base: Dict[str, Any]) -> bool:
response = requests.get(config.server)
assert response.status_code == 200, f"Failed to fetch data {response.status_code} {response.text}"
new_api = json.loads(response.text)
for item in new_api['modifications']:
tqdm.write(f"Изменение {item['file']}")
filepath = './decompiled/smali_classes2/com/swiftsoft/anixartd/network/api/'+item['file']
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']}")
@@ -28,18 +42,22 @@ def apply(config: dict) -> bool:
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)
+52 -23
View File
@@ -1,37 +1,63 @@
"""
Изменяет цветовую тему приложения и иконку
Изменяет цветовую тему приложения и иконку
"color_theme": {
"enabled": true,
"logo": {
"gradient": {
"angle": 0.0,
"start_color": "#ffccff00",
"end_color": "#ffcccc00"
},
"ears_color": "#ffffd0d0"
},
"colors": {
"primary": "#ccff00",
"secondary": "#ffffd700",
"secondary": "#ffcccc00",
"background": "#ffffff",
"text": "#000000"
},
"gradient": {
"angle": "135.0",
"from": "#ffff6060",
"to": "#ffccff00"
}
}
"""
priority = 0
# imports
from lxml import etree
from typing import Dict, Any
from pydantic import Field, BaseModel
from utils.config import PatchConfig
from utils.public import (
insert_after_public,
insert_after_color,
change_color,
)
#Config
class Gradient(BaseModel):
angle: float = Field(0.0, description="Угол градиента")
start_color: str = Field("#ffccff00", description="Начальный цвет градиента")
end_color: str = Field("#ffcccc00", description="Конечный цвет градиента")
def apply(config: dict) -> bool:
main_color = config["colors"]["primary"]
splash_color = config["colors"]["secondary"]
gradient_angle = config["gradient"]["angle"]
gradient_from = config["gradient"]["from"]
gradient_to = config["gradient"]["to"]
class Logo(BaseModel):
gradient: Gradient = Field(Gradient(), description="Настройки градиента") # type: ignore [reportCallIssue]
ears_color: str = Field("#ffd0d0d0", description="Цвет ушей логотипа")
class Colors(BaseModel):
primary: str = Field("#ccff00", description="Основной цвет")
secondary: str = Field("#ffcccc00", description="Вторичный цвет")
background: str = Field("#ffffff", description="Фоновый цвет")
text: str = Field("#000000", description="Цвет текста")
class Config(PatchConfig):
logo: Logo = Field(Logo(), description="Настройки цветов логотипа") # type: ignore [reportCallIssue]
colors: Colors = Field(Colors(), description="Настройки цветов") # type: ignore [reportCallIssue]
# Patch
def apply(config: Config, base: Dict[str, Any]) -> bool:
main_color = config.colors.primary
splash_color = config.colors.secondary
# No connection alert coolor
with open("./decompiled/assets/no_connection.html", "r", encoding="utf-8") as file:
@@ -54,9 +80,9 @@ def apply(config: dict) -> bool:
root = tree.getroot()
# 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']}}}endColor", gradient_to)
root.set(f"{{{base['xml_ns']['android']}}}angle", str(config.logo.gradient.angle))
root.set(f"{{{base['xml_ns']['android']}}}startColor", config.logo.gradient.start_color)
root.set(f"{{{base['xml_ns']['android']}}}endColor", config.logo.gradient.end_color)
# Save back
tree.write(file_path, pretty_print=True, xml_declaration=True, encoding="utf-8")
@@ -69,10 +95,12 @@ def apply(config: dict) -> bool:
root = tree.getroot()
# Finding "path"
for el in root.findall("path", namespaces=config["xml_ns"]):
name = el.get(f"{{{config['xml_ns']['android']}}}name")
for el in root.findall("path", namespaces=base["xml_ns"]):
name = el.get(f"{{{base['xml_ns']['android']}}}name")
if name == "path":
el.set(f"{{{config['xml_ns']['android']}}}fillColor", splash_color)
el.set(f"{{{base['xml_ns']['android']}}}fillColor", config.colors.secondary)
elif name in ["path_1", "path_2"]:
el.set(f"{{{base['xml_ns']['android']}}}fillColor", config.logo.ears_color)
# Save back
tree.write(file_path, pretty_print=True, xml_declaration=True, encoding="utf-8")
@@ -85,11 +113,11 @@ def apply(config: dict) -> bool:
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'])
root.set(f"{{{base['xml_ns']['android']}}}angle", str(config.logo.gradient.angle))
items = root.findall("item", namespaces=base['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)
items[0].set(f"{{{base['xml_ns']['android']}}}color", config.logo.gradient.start_color)
items[1].set(f"{{{base['xml_ns']['android']}}}color", config.logo.gradient.end_color)
# Save back
tree.write(file_path, pretty_print=True, xml_declaration=True, encoding="utf-8")
@@ -103,6 +131,7 @@ def apply(config: dict) -> bool:
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:])
-30
View File
@@ -1,30 +0,0 @@
# Compress
Патч удаляет ненужные ресурсы что-бы уменьшить размер АПК
## настройки (compress в config.json)
- remove_unknown_files: true/false - удаляет файлы из директории decompiled/unknown
- remove_unknown_files_keep_dirs: list[str] - оставляет указанные директории в decompiled/unknown
- remove_debug_lines: true/false - удаляет строки `.line n` из декомпилированных smali файлов использованные для дебага
- remove_AI_voiceover: true/false - заменяет ИИ озвучку маскота аниксы пустыми mp3 файлами
- compress_png_files: true/false - сжимает PNG в директории decompiled/res
- remove_drawable_files: true/false - удаляет неиспользованные drawable-* из директории decompiled/res
- remove_language_files: true/false - удаляет все языки кроме русского и английского
## efficiency
Проверено с версией 9.0 Beta 7
разница = оригинальный размер апк - патченный размер апк, не учитывая другие патчи
| Настройка | Размер файла | Разница | % |
| :----------- | :-------------------: | :-----------------: | :-: |
| 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% |
| Все включены | 13592 bytes - 13.6 MB | 3500 bytes - 4.8 MB | 20.5% |
+66 -27
View File
@@ -1,4 +1,31 @@
"""Remove and compress resources"""
"""
Удаляет ненужное и сжимает ресурсы что-бы уменьшить размер АПК
Эффективность на проверена на версии 9.0 Beta 7
разница = оригинальный размер апк - патченный размер апк, не учитывая другие патчи
| Настройка | Размер файла | Разница | % |
| :----------- | :-------------------: | :-----------------: | :-: |
| 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% |
| Все включены | 13592 bytes - 13.6 MB | 3500 bytes - 4.8 MB | 20.5% |
"compress": {
"enabled": true,
"remove_language_files": true, // удаляет все языки кроме русского и английского
"remove_AI_voiceover": true, // заменяет ИИ озвучку маскота аниксы пустыми mp3 файлами
"remove_debug_lines": false, // удаляет строки `.line n` из smali файлов использованные для дебага
"remove_drawable_files": false, // удаляет неиспользованные drawable-* из директории decompiled/res
"remove_unknown_files": true, // удаляет файлы из директории decompiled/unknown
"remove_unknown_files_keep_dirs": ["META-INF", "kotlin"], // оставляет указанные директории в decompiled/unknown
"compress_png_files": true // сжимает PNG в директории decompiled/res
}
"""
priority = -1
@@ -7,28 +34,41 @@ import os
import shutil
import subprocess
from tqdm import tqdm
from typing import Dict, List, Any
from pydantic import Field
from utils.config import PatchConfig
from utils.smali_parser import get_smali_lines, save_smali_lines
#Config
class Config(PatchConfig):
remove_language_files: bool = Field(True, description="Удаляет все языки кроме русского и английского")
remove_AI_voiceover: bool = Field(True, description="Заменяет ИИ озвучку маскота аниксы пустыми mp3 файлами")
remove_debug_lines: bool = Field(False, description="Удаляет строки `.line n` из smali файлов использованные для дебага")
remove_drawable_files: bool = Field(False, description="Удаляет неиспользованные drawable-* из директории decompiled/res")
remove_unknown_files: bool = Field(True, description="Удаляет файлы из директории decompiled/unknown")
remove_unknown_files_keep_dirs: List[str] = Field(["META-INF", "kotlin"], description="Оставляет указанные директории в decompiled/unknown")
compress_png_files: bool = Field(True, description="Сжимает PNG в директории decompiled/res")
# Patch
def remove_unknown_files(config):
def remove_unknown_files(config: Config, base: Dict[str, Any]):
path = "./decompiled/unknown"
items = os.listdir(path)
for item in items:
item_path = f"{path}/{item}"
if os.path.isfile(item_path):
os.remove(item_path)
if config.get("verbose", False):
if base.get("verbose", False):
tqdm.write(f"Удалён файл: {item_path}")
elif os.path.isdir(item_path):
if item not in config["remove_unknown_files_keep_dirs"]:
if item not in config.remove_unknown_files_keep_dirs:
shutil.rmtree(item_path)
if config.get("verbose", False):
if base.get("verbose", False):
tqdm.write(f"Удалёна директория: {item_path}")
return True
def remove_debug_lines(config):
def remove_debug_lines(config: Dict[str, Any]):
for root, _, files in os.walk("./decompiled"):
for filename in files:
file_path = os.path.join(root, filename)
@@ -45,14 +85,13 @@ def remove_debug_lines(config):
return True
def compress_png(config, png_path: str):
def compress_png(config: Dict[str, Any], png_path: str):
try:
assert subprocess.run(
[
"pngquant",
"--force",
"--ext",
".png",
"--ext", ".png",
"--quality=65-90",
png_path,
],
@@ -66,7 +105,7 @@ def compress_png(config, png_path: str):
return False
def compress_png_files(config):
def compress_png_files(config: Dict[str, Any]):
compressed = []
for root, _, files in os.walk("./decompiled"):
for file in files:
@@ -76,8 +115,8 @@ def compress_png_files(config):
return len(compressed) > 0 and any(compressed)
def remove_AI_voiceover(config):
blank = "./patches/resources/blank.mp3"
def remove_AI_voiceover(config: Dict[str, Any]):
blank = "./resources/blank.mp3"
path = "./decompiled/res/raw"
files = [
"reputation_1.mp3",
@@ -108,7 +147,7 @@ def remove_AI_voiceover(config):
return True
def remove_language_files(config):
def remove_language_files(config: Dict[str, Any]):
path = "./decompiled/res"
folders = [
"values-af",
@@ -209,7 +248,7 @@ def remove_language_files(config):
return True
def remove_drawable_files(config):
def remove_drawable_files(config: Dict[str, Any]):
path = "./decompiled/res"
folders = [
"drawable-en-hdpi",
@@ -242,29 +281,29 @@ def remove_drawable_files(config):
return True
def apply(config) -> bool:
if config["remove_unknown_files"]:
def apply(config: Config, base: Dict[str, Any]) -> bool:
if config.remove_unknown_files:
tqdm.write(f"Удаление неизвестных файлов...")
remove_unknown_files(config)
remove_unknown_files(config, base)
if config["remove_drawable_files"]:
if config.remove_drawable_files:
tqdm.write(f"Удаление директорий drawable-xx...")
remove_drawable_files(config)
remove_drawable_files(base)
if config["compress_png_files"]:
if config.compress_png_files:
tqdm.write(f"Сжатие PNG файлов...")
compress_png_files(config)
compress_png_files(base)
if config["remove_language_files"]:
if config.remove_language_files:
tqdm.write(f"Удаление языков...")
remove_language_files(config)
remove_language_files(base)
if config["remove_AI_voiceover"]:
if config.remove_AI_voiceover:
tqdm.write(f"Удаление ИИ озвучки...")
remove_AI_voiceover(config)
remove_AI_voiceover(base)
if config["remove_debug_lines"]:
if config.remove_debug_lines:
tqdm.write(f"Удаление дебаг линий...")
remove_debug_lines(config)
remove_debug_lines(base)
return True
+21 -7
View File
@@ -1,8 +1,19 @@
"""
Удаляет баннеры рекламы
Удаляет баннеры рекламы
"disable_ad": {
"enabled": true
}
"""
priority = 0
# imports
import textwrap
from tqdm import tqdm
from typing import Dict, Any
from utils.config import PatchConfig
from utils.smali_parser import (
find_smali_method_end,
find_smali_method_start,
@@ -11,15 +22,18 @@ from utils.smali_parser import (
)
replace = """ .locals 0
#Config
class Config(PatchConfig): ...
# Patch
def apply(config: Config, base: Dict[str, Any]) -> bool:
replacement = [f'\t{line}\n' for line in textwrap.dedent("""\
.locals 0
const/4 p0, 0x1
return p0
"""
""").splitlines()]
def apply(config) -> bool:
path = "./decompiled/smali_classes2/com/swiftsoft/anixartd/Prefs.smali"
lines = get_smali_lines(path)
for index, line in enumerate(lines):
@@ -27,7 +41,7 @@ def apply(config) -> bool:
method_start = find_smali_method_start(lines, index)
method_end = find_smali_method_end(lines, index)
new_content = replace_smali_method_body(
lines, method_start, method_end, replace
lines, method_start, method_end, replacement
)
with open(path, "w", encoding="utf-8") as file:
+18 -4
View File
@@ -1,14 +1,27 @@
"""
Удаляет баннеры бета-версии
Удаляет баннеры бета-версии
"disable_beta_banner": {
"enabled": true
}
"""
priority = 0
# imports
import os
from tqdm import tqdm
from lxml import etree
from typing import Dict, Any
from utils.config import PatchConfig
from utils.smali_parser import get_smali_lines, save_smali_lines
def apply(config) -> bool:
#Config
class Config(PatchConfig): ...
# Patch
def apply(config: Config, base: Dict[str, Any]) -> bool:
attributes = [
"paddingTop",
"paddingBottom",
@@ -29,8 +42,9 @@ def apply(config) -> bool:
root = tree.getroot()
for attr in attributes:
# tqdm.write(f"set {attr} = 0.0dip")
root.set(f"{{{config["xml_ns"]['android']}}}{attr}", "0.0dip")
if base.get("verbose", False):
tqdm.write(f"set {attr} = 0.0dip")
root.set(f"{{{base['xml_ns']['android']}}}{attr}", "0.0dip")
tree.write(
beta_banner_xml, pretty_print=True, xml_declaration=True, encoding="utf-8"
+19 -26
View File
@@ -1,41 +1,34 @@
"""
Вставляет новые файлы в проект
Вставляет новые файлы в проект
"insert_new": {
"enabled": true
}
"""
priority = 0
import shutil
# imports
import os
import shutil
from typing import Dict, Any
from utils.config import PatchConfig
from utils.public import insert_after_public
def apply(config: dict) -> bool:
# Mod first launch window
shutil.copytree(
"./patches/resources/smali_classes4/", "./decompiled/smali_classes4/"
)
#Config
class Config(PatchConfig): ...
# Mod assets
shutil.copy("./patches/resources/avatar.png", "./decompiled/assets/avatar.png")
# Patch
def apply(config: Config, base: Dict[str, Any]) -> bool:
# Mod first launch window
shutil.copy("./resources/avatar.png", "./decompiled/assets/avatar.png")
shutil.copy(
"./patches/resources/OpenSans-Regular.ttf",
"./resources/OpenSans-Regular.ttf",
"./decompiled/assets/OpenSans-Regular.ttf",
)
shutil.copy(
"./patches/resources/ic_custom_crown.xml",
"./decompiled/res/drawable/ic_custom_crown.xml",
shutil.copytree(
"./resources/smali_classes4/", "./decompiled/smali_classes4/"
)
shutil.copy(
"./patches/resources/ic_custom_telegram.xml",
"./decompiled/res/drawable/ic_custom_telegram.xml",
)
shutil.copy(
"./patches/resources/ytsans_medium.ttf",
"./decompiled/res/font/ytsans_medium.ttf",
)
os.remove("./decompiled/res/font/ytsans_medium.otf")
insert_after_public("warning_error_counter_background", "ic_custom_telegram")
insert_after_public("warning_error_counter_background", "ic_custom_crown")
return True
+21 -12
View File
@@ -1,22 +1,34 @@
"""
Изменяет имя пакета в apk, удаляет вход по google и vk
Изменяет имя пакета в apk, удаляет вход по google и vk
"package_name": {
"enabled": true,
"new_package_name": "com.wowlikon.anixart"
}
"""
priority = -1
# imports
import os
from lxml import etree
from tqdm import tqdm
from typing import Dict, Any
from pydantic import Field
from utils.config import PatchConfig
#Config
class Config(PatchConfig):
package_name: str = Field("com.wowlikon.anixart", description="Название пакета")
# Patch
def rename_dir(src, dst):
os.makedirs(os.path.dirname(dst), exist_ok=True)
os.rename(src, dst)
def apply(config: dict) -> bool:
def apply(config: Config, base: Dict[str, Any]) -> bool:
for root, dirs, files in os.walk("./decompiled"):
for filename in files:
file_path = os.path.join(root, filename)
@@ -27,11 +39,11 @@ def apply(config: dict) -> bool:
file_contents = file.read()
new_contents = file_contents.replace(
"com.swiftsoft.anixartd", config["new_package_name"]
"com.swiftsoft.anixartd", config.package_name
)
new_contents = new_contents.replace(
"com/swiftsoft/anixartd",
config["new_package_name"].replace(".", "/"),
config.package_name.replace(".", "/"),
)
with open(file_path, "w", encoding="utf-8") as file:
file.write(new_contents)
@@ -42,7 +54,7 @@ def apply(config: dict) -> bool:
rename_dir(
"./decompiled/smali/com/swiftsoft/anixartd",
os.path.join(
"./decompiled", "smali", config["new_package_name"].replace(".", "/")
"./decompiled", "smali", config.package_name.replace(".", "/")
),
)
if os.path.exists("./decompiled/smali_classes2/com/swiftsoft/anixartd"):
@@ -51,7 +63,7 @@ def apply(config: dict) -> bool:
os.path.join(
"./decompiled",
"smali_classes2",
config["new_package_name"].replace(".", "/"),
config.package_name.replace(".", "/"),
),
)
if os.path.exists("./decompiled/smali_classes4/com/swiftsoft"):
@@ -60,7 +72,7 @@ def apply(config: dict) -> bool:
os.path.join(
"./decompiled",
"smali_classes4",
"/".join(config["new_package_name"].split(".")[:-1]),
"/".join(config.package_name.split(".")[:-1]),
),
)
@@ -84,7 +96,7 @@ def apply(config: dict) -> bool:
new_contents = file_contents.replace(
"com/swiftsoft",
"/".join(config["new_package_name"].split(".")[:-1]),
"/".join(config.package_name.split(".")[:-1]),
)
with open(file_path, "w", encoding="utf-8") as file:
file.write(new_contents)
@@ -97,11 +109,8 @@ def apply(config: dict) -> bool:
root = tree.getroot()
last_linear = root.xpath("//LinearLayout/LinearLayout[4]")[0]
last_linear.set(f"{{{config['xml_ns']['android']}}}visibility", "gone")
last_linear.set(f"{{{base['xml_ns']['android']}}}visibility", "gone")
tree.write(file_path, pretty_print=True, xml_declaration=True, encoding="utf-8")
return True
# smali_classes2/com/wowlikon/anixart/utils/DeviceInfoUtil.smali: const-string v3, "\u0411\u0430\u0433-\u0440\u0435\u043f\u043e\u0440\u0442 9.0 BETA 5 (25062213)"
+19 -7
View File
@@ -1,42 +1,54 @@
"""
Меняет порядок вкладок в панели навигации
Меняет порядок вкладок в панели навигации
"replace_navbar": {
"enabled": true,
"items": ["home", "discover", "feed", "bookmarks", "profile"]
}
"""
priority = 0
# imports
from lxml import etree
from tqdm import tqdm
from typing import Dict, List, Any
from pydantic import Field
def apply(config: dict) -> bool:
from utils.config import PatchConfig
#Config
class Config(PatchConfig):
items: List[str] = Field(["home", "discover", "feed", "bookmarks", "profile"], description="Список элементов в панели навигации")
# Patch
def apply(config: Config, base: Dict[str, Any]) -> bool:
file_path = "./decompiled/res/menu/bottom.xml"
parser = etree.XMLParser(remove_blank_text=True)
tree = etree.parse(file_path, parser)
root = tree.getroot()
items = root.findall("item", namespaces=config['xml_ns'])
items = root.findall("item", namespaces=base['xml_ns'])
def get_id_suffix(item):
full_id = item.get(f"{{{config['xml_ns']['android']}}}id")
full_id = item.get(f"{{{base['xml_ns']['android']}}}id")
return full_id.split("tab_")[-1] if full_id else None
items_by_id = {get_id_suffix(item): item for item in items}
existing_order = [get_id_suffix(item) for item in items]
ordered_items = []
for key in config['items']:
for key in config.items:
if key in items_by_id:
ordered_items.append(items_by_id[key])
extra = [i for i in items if get_id_suffix(i) not in config['items']]
extra = [i for i in items if get_id_suffix(i) not in config.items]
if extra:
tqdm.write("⚠Найдены лишние элементы: " + str([get_id_suffix(i) for i in extra]))
ordered_items.extend(extra)
for i in root.findall("item", namespaces=config['xml_ns']):
for i in root.findall("item", namespaces=base['xml_ns']):
root.remove(i)
for item in ordered_items:
Binary file not shown.
+43
View File
@@ -0,0 +1,43 @@
"""
Делает текст в описании аниме копируемым
"selectable_text": {
"enabled": true
}
"""
priority = 0
# imports
from tqdm import tqdm
from lxml import etree
from typing import Dict, Any
from pydantic import Field
from utils.config import PatchConfig
#Config
class Config(PatchConfig): ...
# Patch
def apply(config: Config, base: Dict[str, Any]) -> bool:
file_path = "./decompiled/res/layout/release_info.xml"
parser = etree.XMLParser(remove_blank_text=True)
tree = etree.parse(file_path, parser)
root = tree.getroot()
# Список тегов, к которым нужно добавить атрибут
tags = ["TextView", "at.blogc.android.views.ExpandableTextView"]
for tag in tags:
for element in root.findall(f".//{tag}", namespaces=base["xml_ns"]):
# Проверяем, нет ли уже атрибута
if f"{{{base['xml_ns']['android']}}}textIsSelectable" not in element.attrib:
element.set(f"{{{base['xml_ns']['android']}}}textIsSelectable", "true")
# Сохраняем обратно
tree.write(file_path, encoding="utf-8", xml_declaration=True, pretty_print=True)
return True
+67 -5
View File
@@ -1,7 +1,8 @@
"""
Добавляет в настройки ссылки и доавляет текст к версии приложения
Добавляет в настройки ссылки и добвляет текст к версии приложения
"settings_urls": {
"enabled": true,
"menu": {
"Раздел": [
{
@@ -19,11 +20,60 @@
"version": " by wowlikon"
}
"""
priority = 0
# imports
import shutil
from lxml import etree
from tqdm import tqdm
from typing import Dict, List, Any
from pydantic import Field
from utils.config import PatchConfig
from utils.public import insert_after_public
#Config
DEFAULT_MENU = {
"Мы в социальных сетях": [
{
"title": "wowlikon",
"description": "Разработчик",
"url": "https://t.me/wowlikon",
"icon": "@drawable/ic_custom_telegram",
"icon_space_reserved": "false"
},
{
"title": "Kentai Radiquum",
"description": "Разработчик",
"url": "https://t.me/radiquum",
"icon": "@drawable/ic_custom_telegram",
"icon_space_reserved": "false"
},
{
"title": "Мы в Telegram",
"description": "Подпишитесь на канал, чтобы быть в курсе последних новостей.",
"url": "https://t.me/http_teapod",
"icon": "@drawable/ic_custom_telegram",
"icon_space_reserved": "false"
}
],
"Прочее": [
{
"title": "Помочь проекту",
"description": "Вы можете помочь нам в разработке мода, написании кода или тестировании.",
"url": "https://git.wowlikon.tech/anixart-mod",
"icon": "@drawable/ic_custom_crown",
"icon_space_reserved": "false"
}
]
}
class Config(PatchConfig):
version: str = Field(" by wowlikon", description="Суффикс версии")
menu: Dict[str, List[Dict[str, str]]] = Field(DEFAULT_MENU, description="Меню")
# Patch
def make_category(ns, name, items):
cat = etree.Element("PreferenceCategory", nsmap=ns)
cat.set(f"{{{ns['android']}}}title", name)
@@ -43,7 +93,19 @@ def make_category(ns, name, items):
return cat
def apply(config: dict) -> bool:
def apply(config: Config, base: Dict[str, Any]) -> bool:
shutil.copy(
"./resources/ic_custom_crown.xml",
"./decompiled/res/drawable/ic_custom_crown.xml",
)
insert_after_public("warning_error_counter_background", "ic_custom_crown")
shutil.copy(
"./resources/ic_custom_telegram.xml",
"./decompiled/res/drawable/ic_custom_telegram.xml",
)
insert_after_public("warning_error_counter_background", "ic_custom_telegram")
file_path = "./decompiled/res/xml/preference_main.xml"
parser = etree.XMLParser(remove_blank_text=True)
tree = etree.parse(file_path, parser)
@@ -52,8 +114,8 @@ def apply(config: dict) -> bool:
# Insert new PreferenceCategory before the last element
last = root[-1] # last element
pos = root.index(last)
for section, items in config["menu"].items():
root.insert(pos, make_category(config["xml_ns"], section, items))
for section, items in config.menu.items():
root.insert(pos, make_category(base["xml_ns"], section, items))
pos += 1
# Save back
@@ -68,7 +130,7 @@ def apply(config: dict) -> bool:
with open(filepath, "r", encoding="utf-8") as file:
for line in file.readlines():
if '"\u0412\u0435\u0440\u0441\u0438\u044f'.encode("unicode_escape").decode() in line:
content += line[:line.rindex('"')] + config["version"] + line[line.rindex('"'):]
content += line[:line.rindex('"')] + config.version + line[line.rindex('"'):]
else:
content += line
with open(filepath, "w", encoding="utf-8") as file:
-6
View File
@@ -1,6 +0,0 @@
"""Change application icon"""
priority = 0
def apply(config: dict) -> bool:
return False
+17 -3
View File
@@ -1,23 +1,37 @@
"""
Добавляет пользовательские скорости воспроизведения видео
Добавляет пользовательские скорости воспроизведения видео
"custom_speed": {
"enabled": true,
"speeds": [9.0]
}
"""
priority = 0
# imports
from tqdm import tqdm
from typing import Dict, List, Any
from pydantic import Field
from utils.config import PatchConfig
from utils.smali_parser import float_to_hex
from utils.public import (
insert_after_public,
insert_after_id,
)
def apply(config: dict) -> bool:
#Config
class Config(PatchConfig):
speeds: List[float] = Field([9.0], description="Список пользовательских скоростей воспроизведения")
# Patch
def apply(config: Config, base: Dict[str, Any]) -> bool:
assert float_to_hex(1.5) == "0x3fc00000"
last = "speed75"
for speed in config.get("speeds", []):
for speed in config.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)}"
+49
View File
@@ -0,0 +1,49 @@
"""
Шаблон патча
Здесь вы можете добавить описание патча, его назначение и другие детали.
Каждый патч должен независеть от других патчей и проверять себя при применении. Он не должен вернуть True, если есть проблемы.
На данный момент каждый патч должен иметь функцию `apply`, которая принимает на вход конфигурацию и возвращает True или False.
И модель `Config`, которая наследуется от `PatchConfig` (поле `enabled` добавлять не нужно).
Это позволяет упростить конфигурирование и проверять типы данных, и делать значения по умолчанию.
При успешном применении патча, функция apply должна вернуть True, иначе False.
Ошибка будет интерпретирована как False. С выводом ошибки в консоль.
Ещё патч должен иметь переменную `priority`, которая указывает приоритет патча, чем выше, тем раньше он будет применен.
Коротко о конфигурации. Она состоит из двух частей `config` (на основе модели `Config` и из файла `configs/название_патча.json`).
И постоянной не типизированной переменной `base` из `config.json` и флага `verbose`.
```
python ./main.py build --verbose
```
В конце docstring может быть дополнительное описание конфигурации патча (основное описание получается из модели `Config`).
Это может быть как короткий фрагмент из названия патча и одной опции "enabled", которая обрабатывается в коде патчера.
"todo_template": {
"enabled": true, // Пример описания тк этот текст просто пример
"example": true // Пример кастомного параметра
}
"""
priority = 0 # Приоритет патча, чем выше, тем раньше он будет применен
# imports
from tqdm import tqdm
from typing import Dict, List, Any
from pydantic import Field
from utils.config import PatchConfig
#Config
class Config(PatchConfig):
example: bool = Field(True, description="Пример кастомного параметра")
# Patch
def apply(config: Config, base: Dict[str, Any]) -> bool: # Анотации типов для удобства, читаемости и поддержки IDE
tqdm.write("Вывод информации через tqdm, чтобы не мешать прогресс-бару")
tqdm.write("Пример включен" if config.example else "Пример отключен")
if base["verbose"]:
tqdm.write("Для вывода подробной и отладочной информации используйте флаг --verbose")
return True

Before

Width:  |  Height:  |  Size: 26 MiB

After

Width:  |  Height:  |  Size: 26 MiB

+30
View File
@@ -0,0 +1,30 @@
from pydantic import BaseModel, Field, ValidationError
from rich.console import Console
from typing import Dict, Any
from pathlib import Path
import typer
class ToolsConfig(BaseModel):
apktool_jar_url: str
apktool_wrapper_url: str
class Config(BaseModel):
tools: ToolsConfig
base: Dict[str, Any]
class PatchConfig(BaseModel):
enabled: bool = Field(True, description="Включить или отключить патч")
def load_config(console: Console) -> Config:
try:
return Config.model_validate_json(Path("config.json").read_text())
except FileNotFoundError:
console.print("[red]Файл config.json не найден")
raise typer.Exit(1)
except ValidationError as e:
console.print("[red]Ошибка валидации config.json:", e)
raise typer.Exit(1)
+45
View File
@@ -0,0 +1,45 @@
from plumbum import local, ProcessExecutionError
from rich.progress import Progress
from rich.console import Console
from pathlib import Path
from typing import List
import httpx
import typer
TOOLS = Path("tools")
ORIGINAL = Path("original")
MODIFIED = Path("modified")
DECOMPILED = Path("decompiled")
PATCHES = Path("patches")
CONFIGS = Path("configs")
def ensure_dirs():
for d in [TOOLS, ORIGINAL, MODIFIED, DECOMPILED, PATCHES, CONFIGS]:
d.mkdir(exist_ok=True)
def run(console: Console, cmd: List[str], hide_output=True):
prog = local[cmd[0]][cmd[1:]]
try:
prog() if hide_output else prog & FG # type: ignore [reportUndefinedVariable]
except ProcessExecutionError as e:
console.print(f"[red]Ошибка при выполнении команды: {' '.join(cmd)}")
console.print(e.stderr)
raise typer.Exit(1)
def download(console: Console, url: str, dest: Path):
console.print(f"[cyan]Скачивание {url}{dest.name}")
with httpx.Client(follow_redirects=True, timeout=60.0) as client:
with client.stream("GET", url) as response:
response.raise_for_status()
total = int(response.headers.get("Content-Length", 0))
dest.parent.mkdir(parents=True, exist_ok=True)
with open(dest, "wb") as f, Progress(console=console) as progress:
task = progress.add_task("Загрузка", total=total if total else None)
for chunk in response.iter_bytes(chunk_size=8192):
f.write(chunk)
progress.update(task, advance=len(chunk))