Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 19e1ce2f45 | |||
| fbc8b3e017 | |||
| 5ba590cc31 | |||
| 0a4aa544a2 | |||
| 40f9cf0307 | |||
| 8b8ca63bb1 | |||
| 670c53ba69 | |||
| 5ff882a8d5 | |||
| 66336f3a5c | |||
| 85aef3d997 |
@@ -60,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
|
||||
@@ -73,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: |-
|
||||
|
||||
@@ -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
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
{"enabled":true,"server":"https://anixarty.0x174.su/patch"}
|
||||
@@ -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"}}
|
||||
@@ -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}
|
||||
@@ -0,0 +1 @@
|
||||
{"enabled":true}
|
||||
@@ -0,0 +1 @@
|
||||
{"enabled":true}
|
||||
@@ -0,0 +1 @@
|
||||
{"enabled":true}
|
||||
@@ -0,0 +1 @@
|
||||
{"enabled":true,"package_name":"com.wowlikon.anixart"}
|
||||
@@ -0,0 +1 @@
|
||||
{"enabled":true,"items":["home","discover","feed","bookmarks","profile"]}
|
||||
@@ -0,0 +1 @@
|
||||
{"enabled":true}
|
||||
@@ -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"}]}}
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -2,25 +2,39 @@
|
||||
Заменяет сервер 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)
|
||||
|
||||
|
||||
+51
-22
@@ -2,36 +2,62 @@
|
||||
Изменяет цветовую тему приложения и иконку
|
||||
|
||||
"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:])
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
+20
-6
@@ -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:
|
||||
|
||||
@@ -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"
|
||||
|
||||
+18
-25
@@ -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
|
||||
|
||||
+20
-11
@@ -2,21 +2,33 @@
|
||||
Изменяет имя пакета в 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)"
|
||||
|
||||
@@ -2,41 +2,53 @@
|
||||
Меняет порядок вкладок в панели навигации
|
||||
|
||||
"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.
@@ -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
|
||||
@@ -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:
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
"""Change application icon"""
|
||||
priority = 0
|
||||
|
||||
|
||||
def apply(config: dict) -> bool:
|
||||
return False
|
||||
@@ -2,22 +2,36 @@
|
||||
Добавляет пользовательские скорости воспроизведения видео
|
||||
|
||||
"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)}"
|
||||
|
||||
@@ -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 |
@@ -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)
|
||||
@@ -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))
|
||||
Reference in New Issue
Block a user