5 Commits

Author SHA1 Message Date
8faa67258f Отключение компактного меню по умолчанию
Сборка мода / build (push) Successful in 1m42s
2026-01-01 20:45:04 +03:00
01260d7c16 Добавление лого и изменение описания 2026-01-01 19:30:35 +03:00
2dceb75262 Исправление патчей на beta 9, исправление welcome, добавление компактного меню по умолчанию
Сборка мода / build (push) Successful in 1m12s
2026-01-01 18:32:53 +03:00
70337ee3ec Улучшение cli и удобства создания патчей
Сборка мода / build (push) Successful in 2m16s
2025-12-28 17:47:56 +03:00
ec047cd3a5 Исправление создания релиза
Сборка мода / build (push) Successful in 4m59s
2025-10-11 19:08:31 +03:00
41 changed files with 2318 additions and 1162 deletions
+11 -7
View File
@@ -5,9 +5,6 @@ on:
push: push:
tags: tags:
- 'v*' - 'v*'
#schedule: # раз в 36 часов
# - cron: "0 0 */3 * *" # каждые 3 дня в 00:00
# - cron: "0 12 */3 * *" # каждые 3 дня в 12:00
jobs: jobs:
build: build:
@@ -56,13 +53,20 @@ jobs:
run: | run: |
python ./main.py build -f python ./main.py build -f
- name: Чтение report.log - name: Чтение title из report.md
id: get_title id: get_title
run: | run: |
TITLE=$(head -n 1 modified/report.log) TITLE=$(head -n 1 modified/report.md)
tail -n +2 modified/report.log > modified/report.log.tmp
echo "title=${TITLE}" >> $GITHUB_OUTPUT echo "title=${TITLE}" >> $GITHUB_OUTPUT
- name: Чтение body из report.md
id: get_body
run: |
BODY=$(tail -n +3 modified/report.md)
echo "body<<EOF" >> $GITHUB_OUTPUT
echo "$BODY" >> $GITHUB_OUTPUT
echo "EOF" >> $GITHUB_OUTPUT
- name: Установка go - name: Установка go
if: steps.build.outputs.BUILD_EXIT == '0' if: steps.build.outputs.BUILD_EXIT == '0'
uses: actions/setup-go@v4 uses: actions/setup-go@v4
@@ -74,7 +78,7 @@ jobs:
uses: https://gitea.com/actions/release-action@main uses: https://gitea.com/actions/release-action@main
with: with:
title: ${{ steps.get_title.outputs.title }} title: ${{ steps.get_title.outputs.title }}
body_path: modified/report.log.tmp body: ${{ steps.get_body.outputs.body }}
draft: true draft: true
api_key: '${{secrets.RELEASE_TOKEN}}' api_key: '${{secrets.RELEASE_TOKEN}}'
files: |- files: |-
+14 -9
View File
@@ -1,4 +1,5 @@
# Anixarty patcher ![logo](./logo.png)
# Anixarts patcher
### Описание: ### Описание:
Автоматический патчер для приложения anixart. Автоматический патчер для приложения anixart.
@@ -8,11 +9,15 @@
### Структура проекта: ### Структура проекта:
- `main.py` Главный файл - `main.py` Главный файл
- `config.json` Глобальный конфиг
- `configs` Конфигурации патчей
- `patches` Модули патчей - `patches` Модули патчей
- `resources` Ресурсы, используемые патчами
- `utils` Вспомогательные модули - `utils` Вспомогательные модули
- `tools` Инструменты для модификации - `tools` Инструменты для модификации
- `resources` Ресурсы, используемые патчами - `original` Оригинальные apk файлы
- `todo_drafts` Заметки для новых патчей(можно в любом формате) - `decompiled` Декомпилированные файлы выбраного apk
- `modified` Модифицированные apk файлы и отчёт сборки
### Схема ### Схема
@@ -26,7 +31,7 @@ flowchart TD
B f2@==> p[Декомпиляция] B f2@==> p[Декомпиляция]
subgraph p["Применение патчей по возрастанию приоритета"] subgraph p["Применение патчей по убыванию приоритета"]
C[Патч 1] --> D C[Патч 1] --> D
D[Патч 2] --...--> E[Патч n] D[Патч 2] --...--> E[Патч n]
end end
@@ -49,7 +54,7 @@ flowchart TD
1. Клонируйте репозиторий: 1. Клонируйте репозиторий:
```sh ```sh
git clone https://git.wowlikon.tech/anixart-mod/patcher.git git clone https://git.0x174.su/anixart-mod/patcher.git
``` ```
Требования: Требования:
- Python 3.8+ - Python 3.8+
@@ -66,8 +71,8 @@ flowchart TD
``` ```
Пароль от keystore нужно сохранить в `keystore.pass` для полностью автоматической сборки. Пароль от keystore нужно сохранить в `keystore.pass` для полностью автоматической сборки.
3. Измените настройки мода в файле `patches/config.json`. Если вы развернули свой [сервер](https://git.wowlikon.tech/anixart-mod/server), то измените `"server": "https://new.url"` 3. Измените конфигурацию в файле `configs/change_server.json`. Если вы развернули свой [сервер](https://git.0x174.su/anixart-mod/server), то измените `"server": "https://new.url/patch"` на своё значение
4. Поместите оригинальный apk файла anixart в папку `original` 4. Поместите оригинальный apk файл anixart в папку `original`
5. Запустите `main.py build` и выберите файл apk 5. Запустите `main.py build` и выберите файл apk
6. Установите приложение на ваше устройство. 6. Установите приложение на ваше устройство.
@@ -76,6 +81,6 @@ flowchart TD
Этот проект лицензирован под лицензией MIT. См. [LICENSE](./LICENSE) для получения подробной информации. Этот проект лицензирован под лицензией MIT. См. [LICENSE](./LICENSE) для получения подробной информации.
### Вклад в проект: ### Вклад в проект:
- [Kentai Radiquum](https://git.0x174.su/Radiquum) - Значительный вклад в развитие патчера, разработка [anix](https://github.com/AniX-org/AniX) и помощь с API [[GitHub](https://github.com/adiquum) | [Telegram](https://t.me/radiquum)] - [wowlikon](https://git.0x174.su/wowlikon) - Создание и поддержка проекта [[GitHub](https://github.com/wowlikon) | [Telegram](https://t.me/wowlikon)]
- [Kentai Radiquum](https://git.0x174.su/Radiquum) - Значительный вклад в развитие патчера, разработка [anix](https://github.com/AniX-org/AniX) и помощь с API [[GitHub](https://github.com/radiquum) | [Telegram](https://t.me/radiquum)]
- [Seele](https://git.0x174.su/seele_archive) - Оригинальные патчи в начале разработки основаны на модификации от Seele - [Seele](https://git.0x174.su/seele_archive) - Оригинальные патчи в начале разработки основаны на модификации от Seele
- [ReCode Liner](https://git.0x174.su/ReCodeLiner) - Помощь в изучении моддинга приложения [[Telegram](https://t.me/recodius)]
+4 -1
View File
@@ -1 +1,4 @@
{"enabled":true,"server":"https://anixarty.0x174.su/patch"} {
"enabled": false,
"server": "https://anixarts.0x174.su/patch"
}
+17 -1
View File
@@ -1 +1,17 @@
{"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"}} {
"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"
}
}
+6 -1
View File
@@ -1 +1,6 @@
{"enabled":true,"replace":true,"custom_icons":true,"icon_size":"18.0dip"} {
"enabled": true,
"replace": true,
"custom_icons": true,
"icon_size": "18.0dip"
}
+13 -1
View File
@@ -1 +1,13 @@
{"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} {
"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
}
+3 -1
View File
@@ -1 +1,3 @@
{"enabled":true} {
"enabled": true
}
+3 -1
View File
@@ -1 +1,3 @@
{"enabled":true} {
"enabled": true
}
+4 -1
View File
@@ -1 +1,4 @@
{"enabled":true,"package_name":"com.wowlikon.anixart"} {
"enabled": true,
"package_name": "com.wowlikon.anixart"
}
+5 -1
View File
@@ -1 +1,5 @@
{"enabled":true,"items":["home","discover","feed","bookmarks","profile"]} {
"enabled": true,
"default_compact": false,
"items": ["home", "discover", "feed", "bookmarks", "profile"]
}
+3 -1
View File
@@ -1 +1,3 @@
{"enabled":true} {
"enabled": true
}
+38 -1
View File
@@ -1 +1,38 @@
{"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"}]}} {
"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.0x174.su/anixart-mod",
"icon": "@drawable/ic_custom_crown",
"icon_space_reserved": "false"
}
]
}
}
+9 -1
View File
@@ -1 +1,9 @@
{"enabled":true,"format":{"share_channel_text":"Канал: «%1$s»\n%2$schannel/%3$d","share_collection_text":"Коллекция: «%1$s»\n%2$scollection/%3$d","share_profile_text":"Профиль пользователя «%1$s»\n%2$sprofile/%3$d","share_release_text":"Релиз: «%1$s»\n%2$srelease/%3$d"}} {
"enabled": true,
"format": {
"share_channel_text": "Канал: «%1$s»\n%2$schannel/%3$d",
"share_collection_text": "Коллекция: «%1$s»\n%2$scollection/%3$d",
"share_profile_text": "Профиль пользователя «%1$s»\n%2$sprofile/%3$d",
"share_release_text": "Релиз: «%1$s»\n%2$srelease/%3$d"
}
}
+14 -1
View File
@@ -1 +1,14 @@
{"enabled":true,"title":"Anixarty","description":"Описание","link_text":"МЫ В TELEGRAM","link_url":"https://t.me/http_teapod","skip_text":"Пропустить","title_bg_color":"#FFFFFF"} {
"enabled": true,
"title": "Anixarts",
"title_color": "#FF252525",
"title_bg_color": "#FFCFF04D",
"body_bg_color": "#FF252525",
"description": "Описание",
"description_color": "#FFFFFFFF",
"skip_text": "Пропустить",
"skip_color": "#FFFFFFFF",
"link_text": "МЫ В TELEGRAM",
"link_color": "#FFCFF04D",
"link_url": "https://t.me/http_teapod"
}
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 184 KiB

+434 -201
View File
@@ -1,241 +1,474 @@
from typing import List, Dict, Any __version__ = "1.0.0"
import shutil
from functools import wraps
from pathlib import Path
from typing import Any, Dict, List
import typer import typer
import importlib from plumbum import ProcessExecutionError, local
import traceback
import yaml
from plumbum import local, ProcessExecutionError
from rich.console import Console from rich.console import Console
from rich.progress import Progress from rich.progress import Progress
from rich.prompt import Prompt from rich.prompt import Prompt
from rich.table import Table
from utils.config import * from utils.apk import APKMeta, APKProcessor
from utils.tools import * from utils.config import Config, PatchTemplate, load_config
from utils.info import print_model_fields, print_model_table
from utils.patch_manager import (BuildError, ConfigError, PatcherError,
PatchManager, handle_errors)
from utils.tools import (CONFIGS, DECOMPILED, MODIFIED, ORIGINAL, PATCHES,
TOOLS, download, ensure_dirs, run, select_apk)
console = Console() console = Console()
app = typer.Typer() app = typer.Typer(
name="anixarts-patcher",
help="Инструмент для модификации Anixarty APK",
add_completion=False,
)
# ======================= PATCHING ========================= from datetime import datetime
class Patch:
def __init__(self, name: str, module):
self.name = name def generate_report(
self.module = module apk_path: Path,
self.applied = False meta: APKMeta,
self.priority = getattr(module, "priority", 0) patches: List[PatchTemplate],
manager: PatchManager,
) -> None:
"""Генерирует отчёт о сборке в формате Markdown"""
report_path = MODIFIED / "report.md"
applied_count = sum(1 for p in patches if p.applied)
applied_patches = [p for p in patches if p.applied]
failed_patches = [p for p in patches if not p.applied]
applied_patches.sort(key=lambda p: p.priority, reverse=True)
failed_patches.sort(key=lambda p: p.priority, reverse=True)
def get_patch_info(patch: PatchTemplate) -> Dict[str, str]:
"""Получает описание и автора патча из модуля"""
info = {"doc": "", "author": "-"}
try: try:
self.config = module.Config.model_validate_json((CONFIGS / f"{name}.json").read_text()) patch_module = manager.load_patch_module(patch.name)
except Exception as e: doc = patch_module.__doc__
console.print(f"[red]Ошибка при загрузке конфигурации патча {name}: {e}") if doc:
console.print(f"[yellow]Используются значения по умолчанию") info["doc"] = doc.strip().split("\n")[0]
self.config = module.Config() author = getattr(patch_module, "__author__", "")
if author:
info["author"] = f"`{author}`"
except Exception:
pass
return info
def apply(self, conf: Dict[str, Any]) -> bool: lines = []
try: lines.append(f"Anixarts {meta.version_name} (build {meta.version_code})")
self.applied = bool(self.module.apply(self.config, conf)) lines.append("")
return self.applied
except Exception as e:
console.print(f"[red]Ошибка в патче {self.name}: {e}")
traceback.print_exc()
return False
lines.append("## 📦 Информация о сборке")
lines.append("")
lines.append("| Параметр | Значение |")
lines.append("|----------|----------|")
lines.append(f"| Версия | `{meta.version_name}` |")
lines.append(f"| Код версии | `{meta.version_code}` |")
lines.append(f"| Пакет | `{meta.package}` |")
lines.append(f"| Файл | `{apk_path.name}` |")
lines.append(f"| Дата сборки | {datetime.now().strftime('%Y-%m-%d %H:%M:%S')} |")
lines.append("")
# ========================= INIT ========================= lines.append("## 🔧 Применённые патчи")
@app.command() lines.append("")
def init():
"""Создание директорий и скачивание инструментов"""
ensure_dirs()
conf = load_config(console)
for f in PATCHES.glob("*.py"): if applied_patches:
if f.name.startswith("todo_") or f.name == "__init__.py": lines.append(
continue f"> ✅ Успешно применено: **{applied_count}** из **{len(patches)}**"
patch = Patch(f.stem, __import__(f"patches.{f.stem}", fromlist=[""])) )
json_string = patch.config.model_dump_json() lines.append("")
(CONFIGS / f"{patch.name}.json").write_text(json_string) lines.append("| Патч | Приоритет | Автор | Описание |")
lines.append("|------|:---------:|-------|----------|")
if not (TOOLS / "apktool.jar").exists():
download(console, conf.tools.apktool_jar_url, TOOLS / "apktool.jar")
if not (TOOLS / "apktool").exists():
download(console, conf.tools.apktool_wrapper_url, TOOLS / "apktool")
(TOOLS / "apktool").chmod(0o755)
try:
local["java"]["-version"]()
console.print("[green]Java найдена")
except ProcessExecutionError:
console.print("[red]Java не установлена")
raise typer.Exit(1)
# ========================= 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__}")
console.print(f"[blue]Поля конфигурации")
for field_name, field_info in type(patch.config).model_fields.items():
field_data = {
'type': field_info.annotation.__name__,
'description': field_info.description,
'default': field_info.default,
'json_schema_extra': field_info.json_schema_extra,
}
console.print(f'{field_name} {field_data}')
console.print("\n[blue]" + "="*50 + "\n")
for p in applied_patches:
info = get_patch_info(p)
lines.append(
f"| ✅ `{p.name}` | {p.priority} | {info['author']} | {info['doc']} |"
)
else: else:
conf = load_config(console) lines.append("> ⚠️ Нет применённых патчей")
console.print("[cyan]Список патчей:")
patch_list = []
for f in PATCHES.glob("*.py"):
if f.name == "__init__.py": continue
if f.name.startswith("todo_"):
try: priority = __import__(f"patches.{f.stem}.priority", fromlist=[""])
except: priority = None
patch_list.append((priority, f" [{priority}] [yellow]{f.stem}: [yellow]⚠ в разработке"))
continue
patch = Patch(f.stem, __import__(f"patches.{f.stem}", fromlist=[""]))
if patch.config.enabled: patch_list.append((patch.priority, f" [{patch.priority}] [yellow]{f.stem}: [green]✔ включен"))
else: patch_list.append((patch.priority, f" [{patch.priority}] [yellow]{f.stem}: [red]✘ выключен"))
for _, patch in sorted(patch_list, key=lambda x: (x[0] is None, x[0]), reverse=True): console.print(patch)
lines.append("")
# ========================= UTIL ========================= if failed_patches:
def select_apk() -> Path: lines.append("## ❌ Ошибки")
apks = [f for f in ORIGINAL.glob("*.apk")] lines.append("")
if not apks: lines.append("| Патч | Приоритет | Автор | Описание |")
console.print("[red]Нет apk-файлов в папке original") lines.append("|------|:---------:|-------|----------|")
raise typer.Exit(1)
if len(apks) == 1: for p in failed_patches:
console.print(f"[green]Выбран {apks[0].name}") info = get_patch_info(p)
return apks[0] lines.append(
f"| ❌ `{p.name}` | {p.priority} | {info['author']} | {info['doc']} |"
options = {str(i): apk for i, apk in enumerate(apks, 1)}
for k, v in options.items():
console.print(f"{k}. {v.name}")
choice = Prompt.ask("Выберите номер", choices=list(options.keys()))
return options[choice]
def decompile(apk: Path):
console.print("[yellow]Декомпиляция apk...")
run(
console,
[
"java",
"-jar", str(TOOLS / "apktool.jar"),
"d", "-f",
"-o", str(DECOMPILED),
str(apk),
]
) )
lines.append("")
def compile(apk: Path, patches: List[Patch]): lines.append("---")
console.print("[yellow]Сборка apk...") lines.append("")
lines.append(
with open(DECOMPILED / "apktool.yml", encoding="utf-8") as f: "*Собрано с помощью [anixarts-patcher](https://git.0x174.su/anixart-mod/patcher)*"
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),
]
)
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),
str(aligned),
]
) )
console.print("[green]✔ APK успешно собран и подписан") report_path.write_text("\n".join(lines), encoding="utf-8")
console.print(f"[dim]Отчёт сохранён: {report_path}[/dim]")
with open(MODIFIED / "report.log", "w", encoding="utf-8") as f:
f.write(f"Anixarty mod v {version_name} ({version_code})\n")
for p in patches:
f.write(f"{'' if p.applied else ''} {p.name}\n")
# ========================= BUILD ========================= # ========================= COMMANDS =========================
@app.command() @app.command()
def build( @handle_errors
force: bool = typer.Option(False, "--force", "-f", help="Принудительная сборка"), def init():
verbose: bool = typer.Option(False, "--verbose", "-v", help="Подробный вывод"), """Инициализация: создание директорий и скачивание инструментов"""
): ensure_dirs()
"""Декомпиляция, патчи и сборка apk"""
conf = load_config(console) conf = load_config(console)
apk = select_apk()
decompile(apk)
patch_objs: List[Patch] = [] # Проверка Java
conf.base |= {"verbose": verbose} console.print("[cyan]Проверка Java...")
try:
local["java"]["-version"].run(retcode=None)
console.print("[green]✔ Java найдена")
except ProcessExecutionError:
raise PatcherError("Java не установлена. Установите JDK 11+")
for f in PATCHES.glob("*.py"): # Скачивание apktool
if f.name.startswith("todo_") or f.name == "__init__.py": apktool_jar = TOOLS / "apktool.jar"
continue if not apktool_jar.exists():
name = f.stem download(console, conf.tools.apktool_jar_url, apktool_jar)
module = importlib.import_module(f"patches.{name}") else:
if not module.Config.model_validate_json((CONFIGS / f"{name}.json").read_text()): console.print(f"[dim]✔ {apktool_jar.name} уже существует[/dim]")
console.print(f"[yellow]≫ Пропускаем {name}")
continue
patch_objs.append(Patch(name, module))
patch_objs.sort(key=lambda p: p.priority, reverse=True) # Скачивание apktool wrapper
apktool_wrapper = TOOLS / "apktool"
if not apktool_wrapper.exists():
download(console, conf.tools.apktool_wrapper_url, apktool_wrapper)
apktool_wrapper.chmod(0o755)
else:
console.print(f"[dim]✔ {apktool_wrapper.name} уже существует[/dim]")
console.print("[cyan]Применение патчей") # Проверка zipalign и apksigner
with Progress() as progress: for tool in ["zipalign", "apksigner"]:
task = progress.add_task("Патчи", total=len(patch_objs)) try:
for p in patch_objs: local[tool]["--version"].run(retcode=None)
ok = p.apply(conf.base) console.print(f"[green]✔ {tool} найден")
progress.console.print(f"{'' if ok else ''} {p.name}") except Exception:
console.print(f"[yellow]⚠ {tool} не найден в PATH")
# Проверка keystore
if not Path("keystore.jks").exists():
console.print("[yellow]⚠ keystore.jks не найден. Создайте его командой:")
console.print(
"[dim] keytool -genkey -v -keystore keystore.jks -keyalg RSA "
"-keysize 2048 -validity 10000 -alias key[/dim]"
)
# Инициализация конфигов патчей
console.print("\n[cyan]Инициализация конфигураций патчей...")
manager = PatchManager(console)
for name in manager.discover_patches():
patch = manager.load_patch(name)
config_path = CONFIGS / f"{name}.json"
if not config_path.exists():
patch.save_config()
console.print(f" [green]✔ {name}.json создан")
else:
console.print(f" [dim]✔ {name}.json существует[/dim]")
console.print("\n[green]✔ Инициализация завершена")
@app.command("list")
@handle_errors
def list_patches():
"""Показать список всех патчей"""
manager = PatchManager(console)
all_patches = manager.discover_all()
table = Table(title="Доступные патчи")
table.add_column("Приоритет", justify="center", style="cyan")
table.add_column("Название", style="yellow")
table.add_column("Статус", justify="center")
table.add_column("Автор", style="magenta")
table.add_column("Версия", style="yellow")
table.add_column("Описание")
patch_rows = []
for name in all_patches["ready"]:
try:
patch = manager.load_patch(name)
status = "[green]✔ вкл[/green]" if patch.enabled else "[red]✘ выкл[/red]"
patch_class = manager.load_patch_class(name)
priority = getattr(patch_class, "priority", 0)
patch_module = manager.load_patch_module(name)
author = getattr(patch_module, "__author__", "")
version = getattr(patch_module, "__version__", "")
description = (patch_module.__doc__ or "").strip().split("\n")[0]
patch_rows.append(
(patch.priority, name, status, author, version, description)
)
except Exception as e:
raise e
patch_rows.append((0, name, "[red]⚠ ошибка[/red]", "", "", str(e)[:40]))
for name in all_patches["todo"]:
try:
patch_class = manager.load_patch_class(name)
priority = getattr(patch_class, "priority", 0)
patch_module = manager.load_patch_module(name)
author = getattr(patch_module, "__author__", "")
version = getattr(patch_module, "__version__", "")
description = (patch_module.__doc__ or "").strip().split("\n")[0]
patch_rows.append(
(
priority,
name,
"[yellow]⚠ todo[/yellow]",
author,
version,
description,
)
)
except Exception:
patch_rows.append((0, name, "[yellow]⚠ todo[/yellow]", "", "", ""))
patch_rows.sort(key=lambda x: x[0], reverse=True)
for priority, name, status, author, version, desc in patch_rows:
table.add_row(str(priority), name, status, author, version, desc[:50])
console.print(table)
@app.command()
@handle_errors
def info(
patch_name: str = typer.Argument(..., help="Имя патча"),
tree: bool = typer.Option(False, "--tree", "-t", help="Древовидный вывод полей"),
):
"""Показать подробную информацию о патче"""
manager = PatchManager(console)
all_patches = manager.discover_all()
all_names = all_patches["ready"] + all_patches["todo"]
if patch_name not in all_names:
raise PatcherError(f"Патч '{patch_name}' не найден")
patch_class = manager.load_patch_class(patch_name)
console.print(f"\n[bold cyan]Патч: {patch_name}[/bold cyan]")
console.print("-" * 50)
if patch_class.__doc__:
console.print(f"[white]{patch_class.__doc__.strip()}[/white]\n")
is_todo = patch_name in all_patches["todo"]
if is_todo:
console.print("[yellow]Статус: в разработке[/yellow]\n")
else:
patch = manager.load_patch(patch_name)
status = "[green]включён[/green]" if patch.enabled else "[red]выключен[/red]"
console.print(f"Статус: {status}")
console.print(f"Приоритет: [cyan]{patch.priority}[/cyan]\n")
console.print("[bold]Поля конфигурации:[/bold]")
if tree:
print_model_fields(console, patch_class)
else:
table = print_model_table(console, patch_class)
console.print(table)
table = Table(show_header=True)
table.add_column("Поле", style="yellow")
table.add_column("Тип", style="cyan")
table.add_column("По умолчанию")
table.add_column("Описание")
for field_name, field_info in patch_class.model_fields.items():
field_type = getattr(
field_info.annotation, "__name__", str(field_info.annotation)
)
default = str(field_info.default) if field_info.default is not None else "-"
description = field_info.description or ""
table.add_row(field_name, field_type, default, description)
console.print(table)
if not is_todo:
config_path = CONFIGS / f"{patch_name}.json"
if config_path.exists():
console.print(f"\n[bold]Текущая конфигурация[/bold] ({config_path}):")
console.print(config_path.read_text())
@app.command()
@handle_errors
def enable(patch_name: str = typer.Argument(..., help="Имя патча")):
"""Включить патч"""
manager = PatchManager(console)
if patch_name not in manager.discover_patches():
raise PatcherError(f"Патч '{patch_name}' не найден")
patch = manager.load_patch(patch_name)
patch.enabled = True
patch.save_config()
console.print(f"[green]✔ Патч {patch_name} включён")
@app.command()
@handle_errors
def disable(patch_name: str = typer.Argument(..., help="Имя патча")):
"""Выключить патч"""
manager = PatchManager(console)
if patch_name not in manager.discover_patches():
raise PatcherError(f"Патч '{patch_name}' не найден")
patch = manager.load_patch(patch_name)
patch.enabled = False
patch.save_config()
console.print(f"[yellow]✔ Патч {patch_name} выключен")
@app.command()
@handle_errors
def build(
force: bool = typer.Option(
False, "--force", "-f", help="Принудительная сборка при ошибках"
),
verbose: bool = typer.Option(False, "--verbose", "-v", help="Подробный вывод"),
skip_compile: bool = typer.Option(
False, "--skip-compile", "-s", help="Пропустить компиляцию (только патчи)"
),
):
"""Декомпиляция, применение патчей и сборка APK"""
conf = load_config(console)
apk_processor = APKProcessor(console, TOOLS)
apk = select_apk(console)
apk_processor.decompile(apk, DECOMPILED)
manager = PatchManager(console)
patches = manager.load_enabled_patches()
if not patches:
console.print("[yellow]Нет включённых патчей")
if not force:
raise typer.Exit(0)
base_config = conf.base.copy()
base_config["verbose"] = verbose
base_config["decompiled"] = str(DECOMPILED)
console.print(f"\n[cyan]Применение патчей ({len(patches)})...[/cyan]")
with Progress(console=console) as progress:
task = progress.add_task("Патчи", total=len(patches))
for patch in patches:
success = patch.safe_apply(base_config)
status = "[green]✔[/green]" if success else "[red]✘[/red]"
progress.console.print(f" {status} [{patch.priority:2d}] {patch.name}")
progress.advance(task) progress.advance(task)
successes = sum(p.applied for p in patch_objs) applied = sum(1 for p in patches if p.applied)
if successes == len(patch_objs): failed = len(patches) - applied
compile(apk, patch_objs)
elif successes > 0 and ( console.print()
force or Prompt.ask("Продолжить сборку?", choices=["y", "n"]) == "y" if failed == 0:
): console.print(f"[green]✔ Все патчи применены ({applied}/{len(patches)})")
compile(apk, patch_objs) else:
console.print(
f"[yellow]⚠ Применено: {applied}/{len(patches)}, ошибок: {failed}"
)
if skip_compile:
console.print("[yellow]Компиляция пропущена (--skip-compile)")
return
should_compile = (
failed == 0
or force
or Prompt.ask(
"\nПродолжить сборку несмотря на ошибки?", choices=["y", "n"], default="n"
)
== "y"
)
if should_compile:
console.print()
signed_apk, meta = apk_processor.build_and_sign(
source=DECOMPILED,
output_dir=MODIFIED,
)
generate_report(signed_apk, meta, patches, manager)
else: else:
console.print("[red]Сборка отменена") console.print("[red]Сборка отменена")
raise typer.Exit(1) raise typer.Exit(1)
if __name__ == "__main__": app() @app.command()
@handle_errors
def clean(
all_dirs: bool = typer.Option(
False, "--all", "-a", help="Очистить все директории включая modified и configs"
)
):
"""Очистка временных файлов"""
dirs_to_clean = [DECOMPILED]
if all_dirs:
dirs_to_clean.extend([MODIFIED, CONFIGS])
for d in dirs_to_clean:
if d.exists():
shutil.rmtree(d)
d.mkdir()
console.print(f"[yellow]✔ Очищено: {d}")
else:
console.print(f"[dim]≫ Пропущено (не существует): {d}[/dim]")
console.print("[green]✔ Очистка завершена")
@app.command()
@handle_errors
def config():
"""Показать текущую конфигурацию"""
conf = load_config(console)
console.print("[bold cyan]Конфигурация (config.json):[/bold cyan]\n")
console.print("[yellow]Tools:[/yellow]")
console.print(f" apktool_jar_url: {conf.tools.apktool_jar_url}")
console.print(f" apktool_wrapper_url: {conf.tools.apktool_wrapper_url}")
if conf.base:
console.print("\n[yellow]Base:[/yellow]")
for key, value in conf.base.items():
console.print(f" {key}: {value}")
@app.command()
@handle_errors
def version():
"""Показать версию инструмента"""
console.print(f"[cyan]anixarts-patcher[/cyan] v{__version__}")
if __name__ == "__main__":
app()
View File
+45 -30
View File
@@ -2,62 +2,77 @@
"change_server": { "change_server": {
"enabled": true, "enabled": true,
"server": "https://anixarty.0x174.su/patch" "server": "https://anixarts.0x174.su/patch"
} }
""" """
priority = 0 __author__ = "wowlikon <wowlikon@gmail.com>"
__version__ = "1.0.0"
# imports
import json import json
from typing import Any, Dict
import requests import requests
from tqdm import tqdm
from typing import Dict, Any
from pydantic import Field from pydantic import Field
from tqdm import tqdm
from utils.config import PatchConfig from utils.config import PatchTemplate
#Config
class Config(PatchConfig):
server: str = Field("https://anixarty.0x174.su/patch", description="URL сервера")
# Patch class Patch(PatchTemplate):
def apply(config: Config, base: Dict[str, Any]) -> bool: priority: int = Field(frozen=True, exclude=True, default=0)
response = requests.get(config.server) # Получаем данные для патча server: str = Field("https://anixarts.0x174.su/patch", description="URL сервера")
assert response.status_code == 200, f"Failed to fetch data {response.status_code} {response.text}"
def apply(self, base: Dict[str, Any]) -> bool:
response = requests.get(self.server) # Получаем данные для патча
assert (
response.status_code == 200
), f"Failed to fetch data {response.status_code} {response.text}"
new_api = json.loads(response.text) new_api = json.loads(response.text)
for item in new_api['modifications']: # Применяем замены API for item in new_api["modifications"]: # Применяем замены API
tqdm.write(f"Изменение {item['file']}") tqdm.write(f"Изменение {item['file']}")
filepath = './decompiled/smali_classes2/com/swiftsoft/anixartd/network/api/'+item['file'] filepath = (
"./decompiled/smali_classes2/com/swiftsoft/anixartd/network/api/"
+ item["file"]
)
with open(filepath, 'r') as f: with open(filepath, "r") as f:
content = f.read() content = f.read()
with open(filepath, 'w') as f: with open(filepath, "w") as f:
if content.count(item['src']) == 0: if content.count(item["src"]) == 0:
tqdm.write(f"Не найдено {item['src']}") tqdm.write(f"Не найдено {item['src']}")
f.write(content.replace(item['src'], item['dst'])) f.write(content.replace(item["src"], item["dst"]))
tqdm.write(f"Изменение Github ссылки") # Обновление ссылки на поиск серверов в Github tqdm.write(
filepath = './decompiled/smali_classes2/com/swiftsoft/anixartd/utils/anixnet/GithubPagesNetFetcher.smali' f"Изменение Github ссылки"
) # Обновление ссылки на поиск серверов в Github
filepath = "./decompiled/smali_classes2/com/swiftsoft/anixartd/utils/anixnet/GithubPagesNetFetcher.smali"
with open(filepath, 'r') as f: with open(filepath, "r") as f:
content = f.read() content = f.read()
with open(filepath, 'w') as f: 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"]}"')) f.write(
content.replace(
'const-string v1, "https://anixhelper.github.io/pages/urls.json"',
f'const-string v1, "{new_api["gh"]}"',
)
)
tqdm.write("Удаление динамического выбора сервера") # Отключение автовыбора сервера tqdm.write(
filepath = './decompiled/smali_classes2/com/swiftsoft/anixartd/DaggerApp_HiltComponents_SingletonC$SingletonCImpl$SwitchingProvider.smali' "Удаление динамического выбора сервера"
) # Отключение автовыбора сервера
filepath = "./decompiled/smali_classes2/com/swiftsoft/anixartd/DaggerApp_HiltComponents_SingletonC$SingletonCImpl$SwitchingProvider.smali"
content = "" content = ""
with open(filepath, 'r') as f: with open(filepath, "r") as f:
for line in f.readlines(): for line in f.readlines():
if "addInterceptor" in line: continue if "addInterceptor" in line:
continue
content += line content += line
with open(filepath, 'w') as f: with open(filepath, "w") as f:
f.write(content) f.write(content)
return True return True
+108 -46
View File
@@ -19,52 +19,77 @@
} }
""" """
priority = 0 __author__ = "wowlikon <wowlikon@gmail.com>"
__version__ = "1.0.0"
from typing import Any, Dict
# imports
from lxml import etree from lxml import etree
from typing import Dict, Any from pydantic import BaseModel, Field, model_validator
from pydantic import Field, BaseModel
from utils.config import PatchTemplate
from utils.public import change_color, insert_after_color, insert_after_public
from utils.config import PatchConfig
from utils.public import (
insert_after_public,
insert_after_color,
change_color,
)
#Config
class Gradient(BaseModel): class Gradient(BaseModel):
priority: int = Field(frozen=True, exclude=True, default=0)
angle: float = Field(0.0, description="Угол градиента") angle: float = Field(0.0, description="Угол градиента")
start_color: str = Field("#ffccff00", description="Начальный цвет градиента") start_color: str = Field("#ffccff00", description="Начальный цвет градиента")
end_color: str = Field("#ffcccc00", description="Конечный цвет градиента") end_color: str = Field("#ffcccc00", description="Конечный цвет градиента")
class Logo(BaseModel): class Logo(BaseModel):
gradient: Gradient = Field(Gradient(), description="Настройки градиента") # type: ignore [reportCallIssue] gradient: Gradient = Field(
default_factory=Gradient, description="Настройки градиента"
)
ears_color: str = Field("#ffd0d0d0", description="Цвет ушей логотипа") ears_color: str = Field("#ffd0d0d0", description="Цвет ушей логотипа")
class Colors(BaseModel): class Colors(BaseModel):
primary: str = Field("#ccff00", description="Основной цвет") primary: str = Field("#ccff00", description="Основной цвет")
secondary: str = Field("#ffcccc00", description="Вторичный цвет") secondary: str = Field("#ffcccc00", description="Вторичный цвет")
background: str = Field("#ffffff", description="Фоновый цвет") background: str = Field("#ffffff", description="Фоновый цвет")
text: str = Field("#000000", 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 class Patch(PatchTemplate):
def apply(config: Config, base: Dict[str, Any]) -> bool: priority: int = Field(frozen=True, exclude=True, default=0)
main_color = config.colors.primary logo: Logo = Field(default_factory=Logo, description="Настройки цветов логотипа")
splash_color = config.colors.secondary colors: Colors = Field(default_factory=Colors, description="Настройки цветов")
@model_validator(mode="before")
@classmethod
def validate_nested(cls, data):
if isinstance(data, dict):
if "logo" in data and isinstance(data["logo"], dict):
data["logo"] = Logo(**data["logo"])
if "colors" in data and isinstance(data["colors"], dict):
data["colors"] = Colors(**data["colors"])
return data
def hex_to_lottie(hex_color: str) -> tuple[float, float, float]:
hex_color = hex_color.lstrip("#")
hex_color = hex_color[2:] if len(hex_color) == 8 else hex_color
return (
int(hex_color[:2], 16) / 255.0,
int(hex_color[2:4], 16) / 255.0,
int(hex_color[4:6], 16) / 255.0,
)
def apply(self, base: Dict[str, Any]) -> bool:
main_color = self.colors.primary
splash_color = self.colors.secondary
# Обновление сообщения об отсутствии подключения # Обновление сообщения об отсутствии подключения
with open("./decompiled/assets/no_connection.html", "r", encoding="utf-8") as file: with open(
"./decompiled/assets/no_connection.html", "r", encoding="utf-8"
) as file:
file_contents = file.read() file_contents = file.read()
new_contents = file_contents.replace("#f04e4e", main_color) new_contents = file_contents.replace("#f04e4e", main_color)
with open("./decompiled/assets/no_connection.html", "w", encoding="utf-8") as file: with open(
"./decompiled/assets/no_connection.html", "w", encoding="utf-8"
) as file:
file.write(new_contents) file.write(new_contents)
# Суффиксы лого # Суффиксы лого
@@ -79,15 +104,26 @@ def apply(config: Config, base: Dict[str, Any]) -> bool:
root = tree.getroot() root = tree.getroot()
# Замена атрибутов значениями из конфигурации # Замена атрибутов значениями из конфигурации
root.set(f"{{{base['xml_ns']['android']}}}angle", str(config.logo.gradient.angle)) root.set(
root.set(f"{{{base['xml_ns']['android']}}}startColor", config.logo.gradient.start_color) f"{{{base['xml_ns']['android']}}}angle", str(self.logo.gradient.angle)
root.set(f"{{{base['xml_ns']['android']}}}endColor", config.logo.gradient.end_color) )
root.set(
f"{{{base['xml_ns']['android']}}}startColor",
self.logo.gradient.start_color,
)
root.set(
f"{{{base['xml_ns']['android']}}}endColor", self.logo.gradient.end_color
)
# Сохранение # Сохранение
tree.write(file_path, pretty_print=True, xml_declaration=True, encoding="utf-8") tree.write(
file_path, pretty_print=True, xml_declaration=True, encoding="utf-8"
)
# Замена анимации лого # Замена анимации лого
file_path = f"./decompiled/res/drawable{drawable_type}/$logo_splash_anim__0.xml" file_path = (
f"./decompiled/res/drawable{drawable_type}/$logo_splash_anim__0.xml"
)
parser = etree.XMLParser(remove_blank_text=True) parser = etree.XMLParser(remove_blank_text=True)
tree = etree.parse(file_path, parser) tree = etree.parse(file_path, parser)
@@ -96,12 +132,20 @@ def apply(config: Config, base: Dict[str, Any]) -> bool:
for el in root.findall("path", namespaces=base["xml_ns"]): for el in root.findall("path", namespaces=base["xml_ns"]):
name = el.get(f"{{{base['xml_ns']['android']}}}name") name = el.get(f"{{{base['xml_ns']['android']}}}name")
if name == "path": if name == "path":
el.set(f"{{{base['xml_ns']['android']}}}fillColor", config.colors.secondary) el.set(
f"{{{base['xml_ns']['android']}}}fillColor",
self.colors.secondary,
)
elif name in ["path_1", "path_2"]: elif name in ["path_1", "path_2"]:
el.set(f"{{{base['xml_ns']['android']}}}fillColor", config.logo.ears_color) el.set(
f"{{{base['xml_ns']['android']}}}fillColor",
self.logo.ears_color,
)
# Сохранение # Сохранение
tree.write(file_path, pretty_print=True, xml_declaration=True, encoding="utf-8") tree.write(
file_path, pretty_print=True, xml_declaration=True, encoding="utf-8"
)
for filename in ["$ic_launcher_foreground__0", "$ic_banner_foreground__0"]: for filename in ["$ic_launcher_foreground__0", "$ic_banner_foreground__0"]:
file_path = f"./decompiled/res/drawable-v24/{filename}.xml" file_path = f"./decompiled/res/drawable-v24/{filename}.xml"
@@ -111,35 +155,53 @@ def apply(config: Config, base: Dict[str, Any]) -> bool:
root = tree.getroot() root = tree.getroot()
# Замена атрибутов значениями из конфигурации # Замена атрибутов значениями из конфигурации
root.set(f"{{{base['xml_ns']['android']}}}angle", str(config.logo.gradient.angle)) root.set(
items = root.findall("item", namespaces=base['xml_ns']) f"{{{base['xml_ns']['android']}}}angle", str(self.logo.gradient.angle)
)
items = root.findall("item", namespaces=base["xml_ns"])
assert len(items) == 2 assert len(items) == 2
items[0].set(f"{{{base['xml_ns']['android']}}}color", config.logo.gradient.start_color) items[0].set(
items[1].set(f"{{{base['xml_ns']['android']}}}color", config.logo.gradient.end_color) f"{{{base['xml_ns']['android']}}}color", self.logo.gradient.start_color
)
items[1].set(
f"{{{base['xml_ns']['android']}}}color", self.logo.gradient.end_color
)
# Сохранение # Сохранение
tree.write(file_path, pretty_print=True, xml_declaration=True, encoding="utf-8") tree.write(
file_path, pretty_print=True, xml_declaration=True, encoding="utf-8"
)
# Добаление новых цветов для темы # Добаление новых цветов для темы
insert_after_public("carmine", "custom_color") insert_after_public("carmine", "custom_color")
insert_after_public("carmine_alpha_10", "custom_color_alpha_10") insert_after_public("carmine_alpha_10", "custom_color_alpha_10")
insert_after_color("carmine", "custom_color", main_color[0]+'ff'+main_color[1:]) insert_after_color(
insert_after_color("carmine_alpha_10", "custom_color_alpha_10", main_color[0]+'1a'+main_color[1:]) "carmine", "custom_color", main_color[0] + "ff" + main_color[1:]
)
insert_after_color(
"carmine_alpha_10",
"custom_color_alpha_10",
main_color[0] + "1a" + main_color[1:],
)
# Замена цветов # Замена цветов
change_color("accent_alpha_10", main_color[0]+'1a'+main_color[1:]) change_color("accent_alpha_10", main_color[0] + "1a" + main_color[1:])
change_color("accent_alpha_20", main_color[0]+'33'+main_color[1:]) change_color("accent_alpha_20", main_color[0] + "33" + main_color[1:])
change_color("accent_alpha_50", main_color[0]+'80'+main_color[1:]) change_color("accent_alpha_50", main_color[0] + "80" + main_color[1:])
change_color("accent_alpha_70", main_color[0]+'b3'+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("colorAccent", main_color[0] + "ff" + main_color[1:])
change_color("link_color", main_color[0]+'ff'+main_color[1:]) change_color("link_color", main_color[0] + "ff" + main_color[1:])
change_color("link_color_alpha_70", main_color[0]+'b3'+main_color[1:]) change_color("link_color_alpha_70", main_color[0] + "b3" + main_color[1:])
change_color("refresh_progress", main_color[0]+'ff'+main_color[1:]) change_color("refresh_progress", main_color[0] + "ff" + main_color[1:])
change_color("ic_launcher_background", "#ff000000") change_color("ic_launcher_background", "#ff000000")
change_color("bottom_nav_indicator_active", "#ffffffff") change_color("bottom_nav_indicator_active", "#ffffffff")
change_color("bottom_nav_indicator_icon_checked", main_color[0]+'ff'+main_color[1:]) change_color(
change_color("bottom_nav_indicator_label_checked", main_color[0]+'ff'+main_color[1:]) "bottom_nav_indicator_icon_checked", main_color[0] + "ff" + main_color[1:]
)
change_color(
"bottom_nav_indicator_label_checked", main_color[0] + "ff" + main_color[1:]
)
return True return True
+26 -18
View File
@@ -8,25 +8,26 @@
} }
""" """
priority = 0 __author__ = "wowlikon <wowlikon@gmail.com>"
__version__ = "1.0.0"
# imports
import os import os
import shutil import shutil
from tqdm import tqdm from typing import Any, Dict
from lxml import etree from lxml import etree
from pydantic import Field from pydantic import Field
from typing import Dict, Any from tqdm import tqdm
from utils.config import PatchConfig
#Config from utils.config import PatchTemplate
class Config(PatchConfig):
class Patch(PatchTemplate):
priority: int = Field(frozen=True, exclude=True, default=0)
replace: bool = Field(True, description="Менять местами лайк/дизлайк") replace: bool = Field(True, description="Менять местами лайк/дизлайк")
custom_icons: bool = Field(True, description="Кастомные иконки") custom_icons: bool = Field(True, description="Кастомные иконки")
icon_size: str = Field("18.0dip", description="Размер иконки") icon_size: str = Field("18.0dip", description="Размер иконки")
# Patch def apply(self, base: Dict[str, Any]) -> bool:
def apply(config, base: Dict[str, Any]) -> bool:
file_path = "./decompiled/res/layout/item_comment.xml" file_path = "./decompiled/res/layout/item_comment.xml"
parser = etree.XMLParser(remove_blank_text=True) parser = etree.XMLParser(remove_blank_text=True)
tree = etree.parse(file_path, parser) tree = etree.parse(file_path, parser)
@@ -38,12 +39,12 @@ def apply(config, base: Dict[str, Any]) -> bool:
".//*[@android:id='@id/votePlusActive']//ImageView | " ".//*[@android:id='@id/votePlusActive']//ImageView | "
".//*[@android:id='@id/voteMinusInactive']//ImageView | " ".//*[@android:id='@id/voteMinusInactive']//ImageView | "
".//*[@android:id='@id/voteMinusActive']//ImageView", ".//*[@android:id='@id/voteMinusActive']//ImageView",
namespaces=base['xml_ns'], namespaces=base["xml_ns"],
): ):
icon.set(f"{{{base['xml_ns']['android']}}}layout_width", config.icon_size) icon.set(f"{{{base['xml_ns']['android']}}}layout_width", self.icon_size)
# icon.set(f"{{{base['xml_ns']['android']}}}layout_height", config.icon_size) # icon.set(f"{{{base['xml_ns']['android']}}}layout_height", self.icon_size)
if config.replace: if self.replace:
tqdm.write("Меняем местами лайк и дизлайк комментария...") tqdm.write("Меняем местами лайк и дизлайк комментария...")
containers = root.xpath( containers = root.xpath(
@@ -68,15 +69,20 @@ def apply(config, base: Dict[str, Any]) -> bool:
found = True found = True
i_plus = children.index(vote_plus) i_plus = children.index(vote_plus)
i_minus = children.index(vote_minus) i_minus = children.index(vote_minus)
children[i_plus], children[i_minus] = children[i_minus], children[i_plus] children[i_plus], children[i_minus] = (
children[i_minus],
children[i_plus],
)
container[:] = children container[:] = children
tqdm.write("Кнопки лайк и дизлайк поменялись местами.") tqdm.write("Кнопки лайк и дизлайк поменялись местами.")
break break
if not found: if not found:
tqdm.write("Не удалось найти оба узла votePlus/voteMinus даже в общих LinearLayout.") tqdm.write(
"Не удалось найти оба узла votePlus/voteMinus даже в общих LinearLayout."
)
if config.custom_icons: if self.custom_icons:
tqdm.write("Заменяем иконки лайка и дизлайка на кастомные...") tqdm.write("Заменяем иконки лайка и дизлайка на кастомные...")
for suffix in ["up", "up_40", "down", "down_40"]: for suffix in ["up", "up_40", "down", "down_40"]:
shutil.copy( shutil.copy(
@@ -88,7 +94,9 @@ def apply(config, base: Dict[str, Any]) -> bool:
".//*[@android:id='@id/votePlusInactive'] | .//*[@android:id='@id/voteMinusInactive']", ".//*[@android:id='@id/votePlusInactive'] | .//*[@android:id='@id/voteMinusInactive']",
namespaces=base["xml_ns"], namespaces=base["xml_ns"],
): ):
for img in inactive.xpath(".//ImageView[@android:src]", namespaces=base["xml_ns"]): for img in inactive.xpath(
".//ImageView[@android:src]", namespaces=base["xml_ns"]
):
src = img.get(f'{{{base["xml_ns"]["android"]}}}src', "") src = img.get(f'{{{base["xml_ns"]["android"]}}}src', "")
if src.startswith("@drawable/") and not src.endswith("_40"): if src.startswith("@drawable/") and not src.endswith("_40"):
img.set(f'{{{base["xml_ns"]["android"]}}}src', src + "_40") img.set(f'{{{base["xml_ns"]["android"]}}}src', src + "_40")
+82 -57
View File
@@ -26,31 +26,48 @@
} }
""" """
priority = -1 __author__ = "Kentai Radiquum <radiquum@gmail.com>"
__version__ = "1.0.0"
# imports
import os import os
import shutil import shutil
import subprocess import subprocess
from tqdm import tqdm from typing import Any, Dict, List
from pydantic import Field
from typing import Dict, List, Any
from utils.config import PatchConfig from pydantic import Field
from tqdm import tqdm
from utils.config import PatchTemplate
from utils.smali_parser import get_smali_lines, save_smali_lines 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 class Patch(PatchTemplate):
def remove_unknown_files(config: Config, base: Dict[str, Any]): priority: int = Field(frozen=True, exclude=True, default=-1)
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"
)
def do_remove_unknown_files(self, base: Dict[str, Any]):
path = "./decompiled/unknown" path = "./decompiled/unknown"
items = os.listdir(path) items = os.listdir(path)
for item in items: for item in items:
@@ -60,14 +77,13 @@ def remove_unknown_files(config: Config, base: Dict[str, Any]):
if base.get("verbose", False): if base.get("verbose", False):
tqdm.write(f"Удалён файл: {item_path}") tqdm.write(f"Удалён файл: {item_path}")
elif os.path.isdir(item_path): elif os.path.isdir(item_path):
if item not in config.remove_unknown_files_keep_dirs: if item not in self.remove_unknown_files_keep_dirs:
shutil.rmtree(item_path) shutil.rmtree(item_path)
if base.get("verbose", False): if base.get("verbose", False):
tqdm.write(f"Удалёна директория: {item_path}") tqdm.write(f"Удалёна директория: {item_path}")
return True return True
def do_remove_debug_lines(self, config: Dict[str, Any]):
def remove_debug_lines(config: Dict[str, Any]):
for root, _, files in os.walk("./decompiled"): for root, _, files in os.walk("./decompiled"):
for filename in files: for filename in files:
file_path = os.path.join(root, filename) file_path = os.path.join(root, filename)
@@ -83,14 +99,14 @@ def remove_debug_lines(config: Dict[str, Any]):
tqdm.write(f"Удалены дебаг линии из: {file_path}") tqdm.write(f"Удалены дебаг линии из: {file_path}")
return True return True
def compress_png(self, config: Dict[str, Any], png_path: str):
def compress_png(config: Dict[str, Any], png_path: str):
try: try:
assert subprocess.run( assert subprocess.run(
[ [
"pngquant", "pngquant",
"--force", "--force",
"--ext", ".png", "--ext",
".png",
"--quality=65-90", "--quality=65-90",
png_path, png_path,
], ],
@@ -103,18 +119,16 @@ def compress_png(config: Dict[str, Any], png_path: str):
tqdm.write(f"Ошибка при сжатии {png_path}: {e}") tqdm.write(f"Ошибка при сжатии {png_path}: {e}")
return False return False
def do_compress_png_files(self, config: Dict[str, Any]):
def compress_png_files(config: Dict[str, Any]):
compressed = [] compressed = []
for root, _, files in os.walk("./decompiled"): for root, _, files in os.walk("./decompiled"):
for file in files: for file in files:
if file.lower().endswith(".png"): if file.lower().endswith(".png"):
compress_png(config, f"{root}/{file}") self.compress_png(config, f"{root}/{file}")
compressed.append(f"{root}/{file}") compressed.append(f"{root}/{file}")
return len(compressed) > 0 and any(compressed) return len(compressed) > 0 and any(compressed)
def do_remove_AI_voiceover(self, config: Dict[str, Any]):
def remove_AI_voiceover(config: Dict[str, Any]):
blank = "./resources/blank.mp3" blank = "./resources/blank.mp3"
path = "./decompiled/res/raw" path = "./decompiled/res/raw"
files = [ files = [
@@ -145,8 +159,7 @@ def remove_AI_voiceover(config: Dict[str, Any]):
return True return True
def do_remove_language_files(self, config: Dict[str, Any]):
def remove_language_files(config: Dict[str, Any]):
path = "./decompiled/res" path = "./decompiled/res"
folders = [ folders = [
"values-af", "values-af",
@@ -246,8 +259,7 @@ def remove_language_files(config: Dict[str, Any]):
tqdm.write(f"Удалена директория: {path}/{folder}") tqdm.write(f"Удалена директория: {path}/{folder}")
return True return True
def do_remove_drawable_files(self, config: Dict[str, Any]):
def remove_drawable_files(config: Dict[str, Any]):
path = "./decompiled/res" path = "./decompiled/res"
folders = [ folders = [
"drawable-en-hdpi", "drawable-en-hdpi",
@@ -279,30 +291,43 @@ def remove_drawable_files(config: Dict[str, Any]):
tqdm.write(f"Удалена директория: {path}/{folder}") tqdm.write(f"Удалена директория: {path}/{folder}")
return True return True
def apply(self, base: Dict[str, Any]) -> bool:
actions = [
(
self.remove_unknown_files,
"Удаление неизвестных файлов...",
self.do_remove_unknown_files,
),
(
self.remove_drawable_files,
"Удаление директорий drawable-xx...",
self.do_remove_drawable_files,
),
(
self.compress_png_files,
"Сжатие PNG файлов...",
self.do_compress_png_files,
),
(
self.remove_language_files,
"Удаление языков...",
self.do_remove_language_files,
),
(
self.remove_AI_voiceover,
"Удаление ИИ озвучки...",
self.do_remove_AI_voiceover,
),
(
self.remove_debug_lines,
"Удаление дебаг линий...",
self.do_remove_debug_lines,
),
]
def apply(config: Config, base: Dict[str, Any]) -> bool: for enabled, message, action in actions:
if config.remove_unknown_files: if enabled:
tqdm.write(f"Удаление неизвестных файлов...") tqdm.write(message)
remove_unknown_files(config, base) action(base)
if config.remove_drawable_files:
tqdm.write(f"Удаление директорий drawable-xx...")
remove_drawable_files(base)
if config.compress_png_files:
tqdm.write(f"Сжатие PNG файлов...")
compress_png_files(base)
if config.remove_language_files:
tqdm.write(f"Удаление языков...")
remove_language_files(base)
if config.remove_AI_voiceover:
tqdm.write(f"Удаление ИИ озвучки...")
remove_AI_voiceover(base)
if config.remove_debug_lines:
tqdm.write(f"Удаление дебаг линий...")
remove_debug_lines(base)
return True return True
+19 -20
View File
@@ -5,35 +5,34 @@
} }
""" """
priority = 0 __author__ = "Kentai Radiquum <radiquum@gmail.com>"
__version__ = "1.0.0"
# imports
import textwrap import textwrap
from tqdm import tqdm from typing import Any, Dict
from typing import Dict, Any
from utils.config import PatchConfig from pydantic import Field
from utils.smali_parser import (
find_smali_method_end, from utils.config import PatchTemplate
find_smali_method_start, from utils.smali_parser import (find_smali_method_end, find_smali_method_start,
get_smali_lines, get_smali_lines, replace_smali_method_body)
replace_smali_method_body,
)
#Config class Patch(PatchTemplate):
class Config(PatchConfig): ... priority: int = Field(frozen=True, exclude=True, default=0)
def apply(self, base: Dict[str, Any]) -> bool:
# Patch path = "./decompiled/smali_classes3/com/swiftsoft/anixartd/Prefs.smali"
def apply(config: Config, base: Dict[str, Any]) -> bool: replacement = [
replacement = [f'\t{line}\n' for line in textwrap.dedent("""\ f"\t{line}\n"
for line in textwrap.dedent(
"""\
.locals 0 .locals 0
const/4 p0, 0x1 const/4 p0, 0x1
return p0 return p0
""").splitlines()] """
).splitlines()
]
path = "./decompiled/smali_classes2/com/swiftsoft/anixartd/Prefs.smali"
lines = get_smali_lines(path) lines = get_smali_lines(path)
for index, line in enumerate(lines): for index, line in enumerate(lines):
if line.find("IS_SPONSOR") >= 0: if line.find("IS_SPONSOR") >= 0:
+17 -13
View File
@@ -5,22 +5,24 @@
} }
""" """
priority = 0 __author__ = "Kentai Radiquum <radiquum@gmail.com>"
__version__ = "1.0.0"
# imports
import os import os
from tqdm import tqdm from typing import Any, Dict
from lxml import etree
from typing import Dict, Any
from utils.config import PatchConfig from lxml import etree
from pydantic import Field
from tqdm import tqdm
from utils.config import PatchTemplate
from utils.smali_parser import get_smali_lines, save_smali_lines from utils.smali_parser import get_smali_lines, save_smali_lines
#Config
class Config(PatchConfig): ...
# Patch class Patch(PatchTemplate):
def apply(config: Config, base: Dict[str, Any]) -> bool: priority: int = Field(frozen=True, exclude=True, default=0)
def apply(self, base: Dict[str, Any]) -> bool:
beta_banner_xml = "./decompiled/res/layout/item_beta.xml"
attributes = [ attributes = [
"paddingTop", "paddingTop",
"paddingBottom", "paddingBottom",
@@ -34,7 +36,6 @@ def apply(config: Config, base: Dict[str, Any]) -> bool:
"layout_marginEnd", "layout_marginEnd",
] ]
beta_banner_xml = "./decompiled/res/layout/item_beta.xml"
if os.path.exists(beta_banner_xml): if os.path.exists(beta_banner_xml):
parser = etree.XMLParser(remove_blank_text=True) parser = etree.XMLParser(remove_blank_text=True)
tree = etree.parse(beta_banner_xml, parser) tree = etree.parse(beta_banner_xml, parser)
@@ -46,7 +47,10 @@ def apply(config: Config, base: Dict[str, Any]) -> bool:
root.set(f"{{{base['xml_ns']['android']}}}{attr}", "0.0dip") root.set(f"{{{base['xml_ns']['android']}}}{attr}", "0.0dip")
tree.write( tree.write(
beta_banner_xml, pretty_print=True, xml_declaration=True, encoding="utf-8" beta_banner_xml,
pretty_print=True,
xml_declaration=True,
encoding="utf-8",
) )
return True return True
+20 -22
View File
@@ -6,28 +6,26 @@
} }
""" """
priority = -1 __author__ = "wowlikon <wowlikon@gmail.com>"
__version__ = "1.0.0"
# imports
import os import os
from tqdm import tqdm from typing import Any, Dict
from lxml import etree from lxml import etree
from typing import Dict, Any
from pydantic import Field from pydantic import Field
from utils.config import PatchConfig from utils.config import PatchTemplate
#Config
class Config(PatchConfig): class Patch(PatchTemplate):
priority: int = Field(frozen=True, exclude=True, default=-1)
package_name: str = Field("com.wowlikon.anixart", description="Название пакета") package_name: str = Field("com.wowlikon.anixart", description="Название пакета")
# Patch def rename_dir(self, src, dst):
def rename_dir(src, dst):
os.makedirs(os.path.dirname(dst), exist_ok=True) os.makedirs(os.path.dirname(dst), exist_ok=True)
os.rename(src, dst) os.rename(src, dst)
def apply(self, base: Dict[str, Any]) -> bool:
def apply(config: Config, base: Dict[str, Any]) -> bool:
for root, dirs, files in os.walk("./decompiled"): for root, dirs, files in os.walk("./decompiled"):
for filename in files: for filename in files:
file_path = os.path.join(root, filename) file_path = os.path.join(root, filename)
@@ -38,14 +36,14 @@ def apply(config: Config, base: Dict[str, Any]) -> bool:
file_contents = file.read() file_contents = file.read()
new_contents = file_contents.replace( new_contents = file_contents.replace(
"com.swiftsoft.anixartd", config.package_name "com.swiftsoft.anixartd", self.package_name
) )
new_contents = new_contents.replace( new_contents = new_contents.replace(
"com/swiftsoft/anixartd", "com/swiftsoft/anixartd",
config.package_name.replace(".", "/"), self.package_name.replace(".", "/"),
).replace( ).replace(
"com/swiftsoft", "com/swiftsoft",
"/".join(config.package_name.split(".")[:2]), "/".join(self.package_name.split(".")[:2]),
) )
with open(file_path, "w", encoding="utf-8") as file: with open(file_path, "w", encoding="utf-8") as file:
file.write(new_contents) file.write(new_contents)
@@ -54,28 +52,28 @@ def apply(config: Config, base: Dict[str, Any]) -> bool:
# Изменяем названия папок # Изменяем названия папок
if os.path.exists("./decompiled/smali/com/swiftsoft/anixartd"): if os.path.exists("./decompiled/smali/com/swiftsoft/anixartd"):
rename_dir( self.rename_dir(
"./decompiled/smali/com/swiftsoft/anixartd", "./decompiled/smali/com/swiftsoft/anixartd",
os.path.join( os.path.join(
"./decompiled", "smali", config.package_name.replace(".", "/") "./decompiled", "smali", self.package_name.replace(".", "/")
), ),
) )
if os.path.exists("./decompiled/smali_classes2/com/swiftsoft/anixartd"): if os.path.exists("./decompiled/smali_classes2/com/swiftsoft/anixartd"):
rename_dir( self.rename_dir(
"./decompiled/smali_classes2/com/swiftsoft/anixartd", "./decompiled/smali_classes2/com/swiftsoft/anixartd",
os.path.join( os.path.join(
"./decompiled", "./decompiled",
"smali_classes2", "smali_classes2",
config.package_name.replace(".", "/"), self.package_name.replace(".", "/"),
), ),
) )
if os.path.exists("./decompiled/smali_classes4/com/swiftsoft"): if os.path.exists("./decompiled/smali_classes4/com/swiftsoft"):
rename_dir( self.rename_dir(
"./decompiled/smali_classes4/com/swiftsoft", "./decompiled/smali_classes4/com/swiftsoft",
os.path.join( os.path.join(
"./decompiled", "./decompiled",
"smali_classes4", "smali_classes4",
"/".join(config.package_name.split(".")[:2]), "/".join(self.package_name.split(".")[:2]),
), ),
) )
@@ -100,7 +98,7 @@ def apply(config: Config, base: Dict[str, Any]) -> bool:
new_contents = file_contents.replace( new_contents = file_contents.replace(
"com/swiftsoft", "com/swiftsoft",
"/".join(config.package_name.split(".")[:-1]), "/".join(self.package_name.split(".")[:-1]),
) )
with open(file_path, "w", encoding="utf-8") as file: with open(file_path, "w", encoding="utf-8") as file:
file.write(new_contents) file.write(new_contents)
+49 -15
View File
@@ -7,22 +7,26 @@
} }
""" """
priority = 0 __author__ = "wowlikon <wowlikon@gmail.com>"
__version__ = "1.0.0"
from typing import Any, Dict, List
# imports
from lxml import etree from lxml import etree
from tqdm import tqdm
from typing import Dict, List, Any
from pydantic import Field from pydantic import Field
from tqdm import tqdm
from utils.config import PatchConfig from utils.config import PatchTemplate
from utils.smali_parser import get_smali_lines, save_smali_lines, find_smali_line
#Config class Patch(PatchTemplate):
class Config(PatchConfig): priority: int = Field(frozen=True, exclude=True, default=0)
items: List[str] = Field(["home", "discover", "feed", "bookmarks", "profile"], description="Список элементов в панели навигации") default_compact: bool = Field(False, description="Компактный вид по умолчанию")
items: List[str] = Field(
["home", "discover", "feed", "bookmarks", "profile"],
description="Список элементов в панели навигации",
)
# Patch def apply(self, base: Dict[str, Any]) -> bool:
def apply(config: Config, base: Dict[str, Any]) -> bool:
file_path = "./decompiled/res/menu/bottom.xml" file_path = "./decompiled/res/menu/bottom.xml"
parser = etree.XMLParser(remove_blank_text=True) parser = etree.XMLParser(remove_blank_text=True)
@@ -30,7 +34,7 @@ def apply(config: Config, base: Dict[str, Any]) -> bool:
root = tree.getroot() root = tree.getroot()
# Получение элементов панели навигации # Получение элементов панели навигации
items = root.findall("item", namespaces=base['xml_ns']) items = root.findall("item", namespaces=base["xml_ns"])
def get_id_suffix(item): def get_id_suffix(item):
full_id = item.get(f"{{{base['xml_ns']['android']}}}id") full_id = item.get(f"{{{base['xml_ns']['android']}}}id")
@@ -41,17 +45,19 @@ def apply(config: Config, base: Dict[str, Any]) -> bool:
# Размещение в новом порядке # Размещение в новом порядке
ordered_items = [] ordered_items = []
for key in config.items: for key in self.items:
if key in items_by_id: if key in items_by_id:
ordered_items.append(items_by_id[key]) 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 self.items]
if extra: if extra:
tqdm.write("⚠Найдены лишние элементы: " + str([get_id_suffix(i) for i in extra])) tqdm.write(
"⚠Найдены лишние элементы: " + str([get_id_suffix(i) for i in extra])
)
ordered_items.extend(extra) ordered_items.extend(extra)
for i in root.findall("item", namespaces=base['xml_ns']): for i in root.findall("item", namespaces=base["xml_ns"]):
root.remove(i) root.remove(i)
for item in ordered_items: for item in ordered_items:
@@ -59,4 +65,32 @@ def apply(config: Config, base: Dict[str, Any]) -> bool:
tree.write(file_path, pretty_print=True, xml_declaration=True, encoding="utf-8") tree.write(file_path, pretty_print=True, xml_declaration=True, encoding="utf-8")
# Изменение компактного вида
if self.default_compact:
main_file_path = "./decompiled/smali_classes3/com/swiftsoft/anixartd/ui/activity/MainActivity.smali"
main_lines = get_smali_lines(main_file_path)
preference_file_path = "./decompiled/smali_classes3/com/swiftsoft/anixartd/ui/fragment/main/preference/AppearancePreferenceFragment.smali"
preference_lines = get_smali_lines(preference_file_path)
main_const = find_smali_line(main_lines, "BOTTOM_NAVIGATION_COMPACT")[-1]
preference_const = find_smali_line(preference_lines, "BOTTOM_NAVIGATION_COMPACT")[-1]
main_invoke = find_smali_line(main_lines, "Landroid/content/SharedPreferences;->getBoolean(Ljava/lang/String;Z)Z")[0]
preference_invoke = find_smali_line(preference_lines, "Landroid/content/SharedPreferences;->getBoolean(Ljava/lang/String;Z)Z")[0]
main_value = set(find_smali_line(main_lines, "const/4 v7, 0x0"))
preference_value = set(find_smali_line(preference_lines, "const/4 v7, 0x0"))
main_target_line = main_value & set(range(main_const, main_invoke))
preference_tartget_line = preference_value & set(range(preference_const, preference_invoke))
assert len(main_target_line) == 1 and len(preference_tartget_line) == 1
main_lines[main_target_line.pop()] = "const/4 v7, 0x1"
preference_lines[preference_tartget_line.pop()] = "const/4 v7, 0x1"
save_smali_lines(main_file_path, main_lines)
save_smali_lines(preference_file_path, preference_lines)
return True return True
+14 -11
View File
@@ -5,22 +5,20 @@
} }
""" """
priority = 0 __author__ = "wowlikon <wowlikon@gmail.com>"
__version__ = "1.0.0"
from typing import Any, Dict
# imports
from tqdm import tqdm
from lxml import etree from lxml import etree
from typing import Dict, Any
from pydantic import Field from pydantic import Field
from utils.config import PatchConfig from utils.config import PatchTemplate
#Config
class Config(PatchConfig): ...
# Patch class Patch(PatchTemplate):
def apply(config: Config, base: Dict[str, Any]) -> bool: priority: int = Field(frozen=True, exclude=True, default=0)
def apply(self, base: Dict[str, Any]) -> bool:
file_path = "./decompiled/res/layout/release_info.xml" file_path = "./decompiled/res/layout/release_info.xml"
parser = etree.XMLParser(remove_blank_text=True) parser = etree.XMLParser(remove_blank_text=True)
@@ -33,8 +31,13 @@ def apply(config: Config, base: Dict[str, Any]) -> bool:
for tag in tags: for tag in tags:
for element in root.findall(f".//{tag}", namespaces=base["xml_ns"]): for element in root.findall(f".//{tag}", namespaces=base["xml_ns"]):
# Проверяем, нет ли уже атрибута # Проверяем, нет ли уже атрибута
if f"{{{base['xml_ns']['android']}}}textIsSelectable" not in element.attrib: if (
element.set(f"{{{base['xml_ns']['android']}}}textIsSelectable", "true") 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) tree.write(file_path, encoding="utf-8", xml_declaration=True, pretty_print=True)
+38 -29
View File
@@ -20,60 +20,60 @@
} }
""" """
priority = 0 __author__ = "wowlikon <wowlikon@gmail.com>"
__version__ = "1.0.0"
# imports
import shutil import shutil
from typing import Any, Dict, List
from lxml import etree from lxml import etree
from tqdm import tqdm
from typing import Dict, List, Any
from pydantic import Field from pydantic import Field
from utils.config import PatchConfig from utils.config import PatchTemplate
from utils.public import insert_after_public from utils.public import insert_after_public
#Config # Config
DEFAULT_MENU = { DEFAULT_MENU = {
"Мы в социальных сетях": [ "Мы в социальных сетях": [
{
"title": "Мы в Telegram",
"description": "Подпишитесь на канал, чтобы быть в курсе последних новостей.",
"url": "https://t.me/http_teapod",
"icon": "@drawable/ic_custom_telegram",
"icon_space_reserved": "false",
},
{ {
"title": "wowlikon", "title": "wowlikon",
"description": "Разработчик", "description": "Разработчик",
"url": "https://t.me/wowlikon", "url": "https://t.me/wowlikon",
"icon": "@drawable/ic_custom_telegram", "icon": "@drawable/ic_custom_telegram",
"icon_space_reserved": "false" "icon_space_reserved": "false",
}, },
{ {
"title": "Kentai Radiquum", "title": "Kentai Radiquum",
"description": "Разработчик", "description": "Разработчик",
"url": "https://t.me/radiquum", "url": "https://t.me/radiquum",
"icon": "@drawable/ic_custom_telegram", "icon": "@drawable/ic_custom_telegram",
"icon_space_reserved": "false" "icon_space_reserved": "false",
}, },
{
"title": "Мы в Telegram",
"description": "Подпишитесь на канал, чтобы быть в курсе последних новостей.",
"url": "https://t.me/http_teapod",
"icon": "@drawable/ic_custom_telegram",
"icon_space_reserved": "false"
}
], ],
"Прочее": [ "Прочее": [
{ {
"title": "Помочь проекту", "title": "Помочь проекту",
"description": "Вы можете помочь нам с идеями, написанием кода или тестированием.", "description": "Вы можете помочь нам с идеями, написанием кода или тестированием.",
"url": "https://git.wowlikon.tech/anixart-mod", "url": "https://git.0x174.su/anixart-mod",
"icon": "@drawable/ic_custom_crown", "icon": "@drawable/ic_custom_crown",
"icon_space_reserved": "false" "icon_space_reserved": "false",
} }
] ],
} }
class Config(PatchConfig):
class Patch(PatchTemplate):
priority: int = Field(frozen=True, exclude=True, default=0)
version: str = Field(" by wowlikon", description="Суффикс версии") version: str = Field(" by wowlikon", description="Суффикс версии")
menu: Dict[str, List[Dict[str, str]]] = Field(DEFAULT_MENU, description="Меню") menu: Dict[str, List[Dict[str, str]]] = Field(DEFAULT_MENU, description="Меню")
# Patch def make_category(self, ns, name, items):
def make_category(ns, name, items):
cat = etree.Element("PreferenceCategory", nsmap=ns) cat = etree.Element("PreferenceCategory", nsmap=ns)
cat.set(f"{{{ns['android']}}}title", name) cat.set(f"{{{ns['android']}}}title", name)
cat.set(f"{{{ns['app']}}}iconSpaceReserved", "false") cat.set(f"{{{ns['app']}}}iconSpaceReserved", "false")
@@ -92,7 +92,7 @@ def make_category(ns, name, items):
return cat return cat
def apply(config: Config, base: Dict[str, Any]) -> bool: def apply(self, base: Dict[str, Any]) -> bool:
# Добавление кастомных иконок # Добавление кастомных иконок
shutil.copy( shutil.copy(
"./resources/ic_custom_crown.xml", "./resources/ic_custom_crown.xml",
@@ -113,8 +113,8 @@ def apply(config: Config, base: Dict[str, Any]) -> bool:
# Вставка новых пунктов перед последним # Вставка новых пунктов перед последним
pos = root.index(root[-1]) pos = root.index(root[-1])
for section, items in config.menu.items(): for section, items in self.menu.items():
root.insert(pos, make_category(base["xml_ns"], section, items)) root.insert(pos, self.make_category(base["xml_ns"], section, items))
pos += 1 pos += 1
# Сохранение # Сохранение
@@ -122,15 +122,24 @@ def apply(config: Config, base: Dict[str, Any]) -> bool:
# Добавление суффикса версии # Добавление суффикса версии
filepaths = [ filepaths = [
"./decompiled/smali_classes2/com/swiftsoft/anixartd/ui/activity/UpdateActivity.smali", "./decompiled/smali_classes3/com/swiftsoft/anixartd/ui/activity/UpdateActivity.smali",
"./decompiled/smali_classes2/com/swiftsoft/anixartd/ui/fragment/main/preference/MainPreferenceFragment.smali", "./decompiled/smali_classes3/com/swiftsoft/anixartd/ui/fragment/main/preference/MainPreferenceFragment.smali",
] ]
for filepath in filepaths: for filepath in filepaths:
content = "" content = ""
with open(filepath, "r", encoding="utf-8") as file: with open(filepath, "r", encoding="utf-8") as file:
for line in file.readlines(): for line in file.readlines():
if '"\u0412\u0435\u0440\u0441\u0438\u044f'.encode("unicode_escape").decode() in line: if (
content += line[:line.rindex('"')] + config.version + line[line.rindex('"'):] '"\u0412\u0435\u0440\u0441\u0438\u044f'.encode(
"unicode_escape"
).decode()
in line
):
content += (
line[: line.rindex('"')]
+ self.version
+ line[line.rindex('"') :]
)
else: else:
content += line content += line
with open(filepath, "w", encoding="utf-8") as file: with open(filepath, "w", encoding="utf-8") as file:
+13 -13
View File
@@ -11,30 +11,30 @@
} }
""" """
priority = 0 __author__ = "wowlikon <wowlikon@gmail.com>"
__version__ = "1.0.0"
from typing import Any, Dict
# imports
from tqdm import tqdm
from lxml import etree from lxml import etree
from typing import Dict, Any
from pydantic import Field from pydantic import Field
from utils.config import PatchConfig from utils.config import PatchTemplate
#Config
DEFAULT_FORMATS = { DEFAULT_FORMATS = {
"share_channel_text": "Канал: «%1$s»\n%2$schannel/%3$d", "share_channel_text": "Канал: «%1$s»\n%2$schannel/%3$d",
"share_collection_text": "Коллекция: «%1$s»\n%2$scollection/%3$d", "share_collection_text": "Коллекция: «%1$s»\n%2$scollection/%3$d",
"share_profile_text": "Профиль пользователя «%1$s»\n%2$sprofile/%3$d", "share_profile_text": "Профиль пользователя «%1$s»\n%2$sprofile/%3$d",
"share_release_text": "Релиз: «%1$s»\n%2$srelease/%3$d" "share_release_text": "Релиз: «%1$s»\n%2$srelease/%3$d",
} }
class Config(PatchConfig):
format: Dict[str, str] = Field(DEFAULT_FORMATS, description="Строки для замены в `strings.xml`")
# Patch class Patch(PatchTemplate):
def apply(config: Config, base: Dict[str, Any]) -> bool: priority: int = Field(frozen=True, exclude=True, default=0)
format: Dict[str, str] = Field(
DEFAULT_FORMATS, description="Строки для замены в `strings.xml`"
)
def apply(self, base: Dict[str, Any]) -> bool:
file_path = "./decompiled/res/values/strings.xml" file_path = "./decompiled/res/values/strings.xml"
parser = etree.XMLParser(remove_blank_text=True) parser = etree.XMLParser(remove_blank_text=True)
@@ -44,8 +44,8 @@ def apply(config: Config, base: Dict[str, Any]) -> bool:
# Обновляем значения # Обновляем значения
for string in root.findall("string"): for string in root.findall("string"):
name = string.get("name") name = string.get("name")
if name in config.format: if name in self.format:
string.text = config.format[name] string.text = self.format[name]
# Сохраняем обратно # Сохраняем обратно
tree.write(file_path, encoding="utf-8", xml_declaration=True, pretty_print=True) tree.write(file_path, encoding="utf-8", xml_declaration=True, pretty_print=True)
+13 -16
View File
@@ -6,31 +6,28 @@
} }
""" """
priority = 0 __author__ = "wowlikon <wowlikon@gmail.com>"
__version__ = "1.0.0"
from typing import Any, Dict, List
# imports
from tqdm import tqdm
from typing import Dict, List, Any
from pydantic import Field from pydantic import Field
from utils.config import PatchConfig from utils.config import PatchTemplate
from utils.public import insert_after_id, insert_after_public
from utils.smali_parser import float_to_hex from utils.smali_parser import float_to_hex
from utils.public import (
insert_after_public,
insert_after_id,
)
#Config
class Config(PatchConfig):
speeds: List[float] = Field([9.0], description="Список пользовательских скоростей воспроизведения")
# Patch class Patch(PatchTemplate):
def apply(config: Config, base: Dict[str, Any]) -> bool: priority: int = Field(frozen=True, exclude=True, default=0)
speeds: List[float] = Field(
[9.0], description="Список пользовательских скоростей воспроизведения"
)
def apply(self, base: Dict[str, Any]) -> bool:
assert float_to_hex(1.5) == "0x3fc00000" assert float_to_hex(1.5) == "0x3fc00000"
last = "speed75" last = "speed75"
for speed in config.speeds: for speed in self.speeds:
insert_after_public(last, f"speed{int(float(speed)*10)}") insert_after_public(last, f"speed{int(float(speed)*10)}")
insert_after_id(last, f"speed{int(float(speed)*10)}") insert_after_id(last, f"speed{int(float(speed)*10)}")
last = f"speed{int(float(speed)*10)}" last = f"speed{int(float(speed)*10)}"
+18 -13
View File
@@ -4,7 +4,7 @@
Каждый патч должен независеть от других патчей и проверять себя при применении. Он не должен вернуть True, если есть проблемы. Каждый патч должен независеть от других патчей и проверять себя при применении. Он не должен вернуть True, если есть проблемы.
На данный момент каждый патч должен иметь функцию `apply`, которая принимает на вход конфигурацию и возвращает True или False. На данный момент каждый патч должен иметь функцию `apply`, которая принимает на вход конфигурацию и возвращает True или False.
И модель `Config`, которая наследуется от `PatchConfig` (поле `enabled` добавлять не нужно). И модель `Config`, которая наследуется от `PatchTemplate` (поле `enabled` добавлять не нужно).
Это позволяет упростить конфигурирование и проверять типы данных, и делать значения по умолчанию. Это позволяет упростить конфигурирование и проверять типы данных, и делать значения по умолчанию.
При успешном применении патча, функция apply должна вернуть True, иначе False. При успешном применении патча, функция apply должна вернуть True, иначе False.
Ошибка будет интерпретирована как False. С выводом ошибки в консоль. Ошибка будет интерпретирована как False. С выводом ошибки в консоль.
@@ -25,24 +25,29 @@ python ./main.py build --verbose
} }
""" """
priority = 0 # Приоритет патча, чем выше, тем раньше он будет применен __author__ = "wowlikon <wowlikon@gmail.com>"
__version__ = "1.0.0"
from typing import Any, Dict, List
# imports
from tqdm import tqdm
from typing import Dict, List, Any
from pydantic import Field from pydantic import Field
from tqdm import tqdm
from utils.config import PatchConfig from utils.config import PatchTemplate
#Config
class Config(PatchConfig): class Patch(PatchTemplate):
example: bool = Field(True, description="Пример кастомного параметра") example: bool = Field(True, description="Пример кастомного параметра")
def apply(
# Patch self, base: Dict[str, Any]
def apply(config: Config, base: Dict[str, Any]) -> bool: # Анотации типов для удобства, читаемости и поддержки IDE ) -> bool: # Анотации типов для удобства, читаемости и поддержки IDE
priority: int = Field(
frozen=True, exclude=True, default=0
) # Приоритет патча, чем выше, тем раньше он будет применен
tqdm.write("Вывод информации через tqdm, чтобы не мешать прогресс-бару") tqdm.write("Вывод информации через tqdm, чтобы не мешать прогресс-бару")
tqdm.write("Пример включен" if config.example else "Пример отключен") tqdm.write("Пример включен" if self.example else "Пример отключен")
if base["verbose"]: if base["verbose"]:
tqdm.write("Для вывода подробной и отладочной информации используйте флаг --verbose") tqdm.write(
"Для вывода подробной и отладочной информации используйте флаг --verbose"
)
return True return True
+66 -24
View File
@@ -2,7 +2,7 @@
"welcome": { "welcome": {
"enabled": true, "enabled": true,
"title": "Anixarty", "title": "Anixarts",
"description": "Описание", "description": "Описание",
"link_text": "МЫ В TELEGRAM", "link_text": "МЫ В TELEGRAM",
"link_url": "https://t.me/http_teapod", "link_url": "https://t.me/http_teapod",
@@ -11,46 +11,88 @@
} }
""" """
priority = 0 __author__ = "wowlikon <wowlikon@gmail.com>"
__version__ = "1.0.0"
# imports
import os
import shutil import shutil
from typing import Any, Dict
from urllib import parse
from pydantic import Field from pydantic import Field
from typing import Dict, Any
from utils.config import PatchConfig
from utils.smali_parser import (
find_and_replace_smali_line,
get_smali_lines,
save_smali_lines
)
#Config from utils.config import PatchTemplate
class Config(PatchConfig): from utils.smali_parser import (find_and_replace_smali_line, get_smali_lines,
title: str = Field("Anixarty", description="Заголовок") save_smali_lines)
class Patch(PatchTemplate):
priority: int = Field(frozen=True, exclude=True, default=0)
title: str = Field("Anixarts", description="Заголовок")
title_color: str = Field("#FF252525", description="Цвет заголовка")
title_bg_color: str = Field("#FFCFF04D", description="Цвет фона заголовка")
body_bg_color: str = Field("#FF252525", description="Цвет фона окна")
description: str = Field("Описание", description="Описание") description: str = Field("Описание", description="Описание")
description_color: str = Field("#FFFFFFFF", description="Цвет описания")
skip_text: str = Field("Пропустить", description="Текст кнопки пропустить")
skip_color: str = Field("#FFFFFFFF", description="Цвет кнопки пропустить")
link_text: str = Field("МЫ В TELEGRAM", description="Текст ссылки") link_text: str = Field("МЫ В TELEGRAM", description="Текст ссылки")
link_color: str = Field("#FFCFF04D", description="Цвет ссылки")
link_url: str = Field("https://t.me/http_teapod", description="Ссылка") link_url: str = Field("https://t.me/http_teapod", description="Ссылка")
skip_text: str = Field("Пропустить", description="Текст кнопки пропуска")
title_bg_color: str = Field("#FFFFFF", description="Цвет фона заголовка")
def encode_text(self, text: str) -> str:
return '+'.join([parse.quote(i) for i in text.split(' ')])
# Patch def apply(self, base: Dict[str, Any]) -> bool:
def apply(config: Config, base: Dict[str, Any]) -> bool:
# Добавление ресурсов окна первого входа # Добавление ресурсов окна первого входа
shutil.copy("./resources/avatar.png", "./decompiled/assets/avatar.png") shutil.copy("./resources/avatar.png", "./decompiled/assets/avatar.png")
shutil.copy( shutil.copy(
"./resources/OpenSans-Regular.ttf", "./resources/OpenSans-Regular.ttf",
"./decompiled/assets/OpenSans-Regular.ttf", "./decompiled/assets/OpenSans-Regular.ttf",
) )
shutil.copytree( for subdir in ["about/", "authorization/"]:
"./resources/smali_classes4/", "./decompiled/smali_classes4/" shutil.copytree("./resources/smali_classes4/com/swiftsoft/"+subdir, "./decompiled/smali_classes4/com/swiftsoft/"+subdir)
)
file_path = "./decompiled/smali_classes2/com/swiftsoft/anixartd/ui/activity/MainActivity.smali" # Привязка к первому запуску
file_path = "./decompiled/smali_classes3/com/swiftsoft/anixartd/ui/activity/MainActivity.smali"
method = "invoke-super {p0}, Lmoxy/MvpAppCompatActivity;->onResume()V" method = "invoke-super {p0}, Lmoxy/MvpAppCompatActivity;->onResume()V"
lines = get_smali_lines(file_path) lines = get_smali_lines(file_path)
lines = find_and_replace_smali_line(lines, method, method+"\ninvoke-static {p0}, Lcom/swiftsoft/about/$2;->oooooo(Landroid/content/Context;)Ljava/lang/Object;") lines = find_and_replace_smali_line(
lines,
method,
method
+ "\ninvoke-static {p0}, Lcom/swiftsoft/about/$2;->oooooo(Landroid/content/Context;)Ljava/lang/Object;",
)
save_smali_lines(file_path, lines)
# Замена ссылки
file_path = "./decompiled/smali_classes4/com/swiftsoft/about/$4.smali"
lines = get_smali_lines(file_path)
lines = find_and_replace_smali_line(
lines,
"const-string v0, \"https://example.com\"",
'const-string v0, "' + self.link_url + '"',
)
save_smali_lines(file_path, lines)
# Настройка всплывающго окна
file_path = "./decompiled/smali_classes4/com/swiftsoft/about/$2.smali"
lines = get_smali_lines(file_path)
for replacement in [
('const-string v5, "#FF252525" # Title color', f'const-string v5, "{self.title_color}"'),
('const-string v7, "#FFFFFFFF" # Description color', f'const-string v7, "{self.description_color}"'),
('const-string v8, "#FFCFF04D" # Link color', f'const-string v8, "{self.link_color}"'),
('const-string v9, "#FFFFFFFF" # Skip color', f'const-string v9, "{self.skip_color}"'),
('const-string v5, "#FF252525" # Body background', f'const-string v5, "{self.body_bg_color}"'),
('const-string v10, "#FFCFF04D" # Title background', f'const-string v10, "{self.title_bg_color}"'),
('const-string v12, "Title"', f'const-string v12, "{self.encode_text(self.title)}"'),
('const-string v11, "Description"', f'const-string v11, "{self.encode_text(self.description)}"'),
('const-string v12, "URL"', f'const-string v12, "{self.link_text.encode('unicode-escape').decode()}"'),
('const-string v12, "Skip"', f'const-string v12, "{self.skip_text.encode('unicode-escape').decode()}"')
]: lines = find_and_replace_smali_line(lines, *replacement)
save_smali_lines(file_path, lines) save_smali_lines(file_path, lines)
return True return True
@@ -95,28 +95,28 @@
move-result-object v2 move-result-object v2
const-string v5, "#FF252525" const-string v5, "#FF252525" # Title color
.line 43 .line 43
invoke-static {v5}, Landroid/graphics/Color;->parseColor(Ljava/lang/String;)I invoke-static {v5}, Landroid/graphics/Color;->parseColor(Ljava/lang/String;)I
move-result v6 move-result v6
const-string v7, "#FFFFFFFF" const-string v7, "#FFFFFFFF" # Description color
.line 44 .line 44
invoke-static {v7}, Landroid/graphics/Color;->parseColor(Ljava/lang/String;)I invoke-static {v7}, Landroid/graphics/Color;->parseColor(Ljava/lang/String;)I
move-result v7 move-result v7
const-string v8, "#FFCFF04D" const-string v8, "#FFCFF04D" # Link color
.line 45 .line 45
invoke-static {v8}, Landroid/graphics/Color;->parseColor(Ljava/lang/String;)I invoke-static {v8}, Landroid/graphics/Color;->parseColor(Ljava/lang/String;)I
move-result v8 move-result v8
const-string v9, "#FFFFFFFF" const-string v9, "#FFFFFFFF" # Skip color
.line 46 .line 46
invoke-static {v9}, Landroid/graphics/Color;->parseColor(Ljava/lang/String;)I invoke-static {v9}, Landroid/graphics/Color;->parseColor(Ljava/lang/String;)I
@@ -124,13 +124,13 @@
move-result v9 move-result v9
.line 47 .line 47
const-string v5, "#FF252525" const-string v5, "#FF252525" # Body background
invoke-static {v5}, Landroid/graphics/Color;->parseColor(Ljava/lang/String;)I invoke-static {v5}, Landroid/graphics/Color;->parseColor(Ljava/lang/String;)I
move-result v5 move-result v5
const-string v10, "#FFCFF04D" const-string v10, "#FFCFF04D" # Title background
.line 48 .line 48
invoke-static {v10}, Landroid/graphics/Color;->parseColor(Ljava/lang/String;)I invoke-static {v10}, Landroid/graphics/Color;->parseColor(Ljava/lang/String;)I
@@ -177,7 +177,7 @@
invoke-direct {v11, v0, v12}, Landroid/app/AlertDialog$Builder;-><init>(Landroid/content/Context;I)V invoke-direct {v11, v0, v12}, Landroid/app/AlertDialog$Builder;-><init>(Landroid/content/Context;I)V
const-string v12, "wowlikon+ID" const-string v12, "Title"
.line 67 .line 67
invoke-virtual {v11, v12}, Landroid/app/AlertDialog$Builder;->setTitle(Ljava/lang/CharSequence;)Landroid/app/AlertDialog$Builder; invoke-virtual {v11, v12}, Landroid/app/AlertDialog$Builder;->setTitle(Ljava/lang/CharSequence;)Landroid/app/AlertDialog$Builder;
@@ -189,7 +189,7 @@
move-result-object v3 move-result-object v3
const-string v11, "%D0%9C%D0%BE%D0%B4+%D1%81%D0%B4%D0%B5%D0%BB%D0%B0%D0%BD+wowlikon+%D1%81+%D0%BD%D0%BE%D0%B2%D1%8B%D1%8B%D0%BC%D0%B8+%D1%84%D1%83%D0%BD%D0%BA%D1%86%D0%B8%D1%8F%D0%BC%D0%B8%21%0A%0A%D0%A1%D0%B4%D0%B5%D0%BB%D0%B0%D0%BD%D0%BE+%D1%81+%E2%9D%A4%EF%B8%8F+%D0%BE%D1%82+swiftsoft" const-string v11, "Description"
.line 69 .line 69
invoke-virtual {v3, v11}, Landroid/app/AlertDialog$Builder;->setMessage(Ljava/lang/CharSequence;)Landroid/app/AlertDialog$Builder; invoke-virtual {v3, v11}, Landroid/app/AlertDialog$Builder;->setMessage(Ljava/lang/CharSequence;)Landroid/app/AlertDialog$Builder;
@@ -200,7 +200,7 @@
invoke-direct {v11}, Lcom/swiftsoft/about/$4;-><init>()V invoke-direct {v11}, Lcom/swiftsoft/about/$4;-><init>()V
const-string v12, "\u041c\u044b \u0432 Telegram" const-string v12, "URL"
.line 70 .line 70
invoke-virtual {v3, v12, v11}, Landroid/app/AlertDialog$Builder;->setPositiveButton(Ljava/lang/CharSequence;Landroid/content/DialogInterface$OnClickListener;)Landroid/app/AlertDialog$Builder; invoke-virtual {v3, v12, v11}, Landroid/app/AlertDialog$Builder;->setPositiveButton(Ljava/lang/CharSequence;Landroid/content/DialogInterface$OnClickListener;)Landroid/app/AlertDialog$Builder;
@@ -211,7 +211,7 @@
invoke-direct {v11}, Lcom/swiftsoft/about/$3;-><init>()V invoke-direct {v11}, Lcom/swiftsoft/about/$3;-><init>()V
const-string v12, "\u041f\u043e\u043d\u044f\u0442\u043d\u043e" const-string v12, "Skip"
.line 71 .line 71
invoke-virtual {v3, v12, v11}, Landroid/app/AlertDialog$Builder;->setNeutralButton(Ljava/lang/CharSequence;Landroid/content/DialogInterface$OnClickListener;)Landroid/app/AlertDialog$Builder; invoke-virtual {v3, v12, v11}, Landroid/app/AlertDialog$Builder;->setNeutralButton(Ljava/lang/CharSequence;Landroid/content/DialogInterface$OnClickListener;)Landroid/app/AlertDialog$Builder;
@@ -32,7 +32,7 @@
new-instance p2, Landroid/content/Intent; new-instance p2, Landroid/content/Intent;
const-string v0, "https://t.me/wowlikon" const-string v0, "https://example.com"
invoke-static {v0}, Landroid/net/Uri;->parse(Ljava/lang/String;)Landroid/net/Uri; invoke-static {v0}, Landroid/net/Uri;->parse(Ljava/lang/String;)Landroid/net/Uri;
-27
View File
@@ -1,27 +0,0 @@
/res/layout/release_info.xml
<TextView android:textColor="@color/light_md_blue_500" android:id="@id/note" android:background="@drawable/bg_release_note" android:paddingTop="12.0dip" android:paddingBottom="12.0dip" android:layout_width="fill_parent" android:layout_height="wrap_content" android:textIsSelectable="true" android:paddingStart="16.0dip" android:paddingEnd="16.0dip" style="?textAppearanceBodyMedium" />
</FrameLayout>
<LinearLayout android:orientation="vertical" android:layout_width="fill_parent" android:layout_height="fill_parent">
<LinearLayout android:gravity="center_vertical" android:orientation="horizontal" android:id="@id/countryLayout" android:visibility="gone" android:layout_width="fill_parent" android:layout_height="fill_parent">
<androidx.appcompat.widget.AppCompatImageView android:id="@id/iconCountry" android:layout_width="wrap_content" android:layout_height="wrap_content" app:srcCompat="@drawable/ic_flag_japan" />
<TextView android:textColor="?primaryTextColor" android:textColorLink="?primaryTextColor" android:gravity="center_vertical" android:linksClickable="true" android:id="@id/tvCountry" android:layout_width="fill_parent" android:layout_height="wrap_content" android:textIsSelectable="true" android:layout_marginStart="8.0dip" style="?textAppearanceBodyMedium" />
</LinearLayout>
<LinearLayout android:gravity="center_vertical" android:orientation="horizontal" android:id="@id/episodesLayout" android:visibility="gone" android:layout_width="fill_parent" android:layout_height="fill_parent" android:layout_marginTop="2.0dip">
<androidx.appcompat.widget.AppCompatImageView android:layout_width="wrap_content" android:layout_height="wrap_content" app:srcCompat="@drawable/ic_episodes" />
<TextView android:textColor="?primaryTextColor" android:textColorLink="?primaryTextColor" android:gravity="center_vertical" android:linksClickable="true" android:id="@id/tvEpisodes" android:layout_width="fill_parent" android:layout_height="wrap_content" android:textIsSelectable="true" android:layout_marginStart="8.0dip" style="?textAppearanceBodyMedium" />
</LinearLayout>
<LinearLayout android:gravity="center_vertical" android:orientation="horizontal" android:id="@id/scheduleLayout" android:visibility="gone" android:layout_width="fill_parent" android:layout_height="fill_parent" android:layout_marginTop="2.0dip">
<androidx.appcompat.widget.AppCompatImageView android:layout_width="wrap_content" android:layout_height="wrap_content" app:srcCompat="@drawable/ic_schedule" />
<TextView android:textColor="?primaryTextColor" android:textColorLink="?primaryTextColor" android:gravity="center_vertical" android:linksClickable="true" android:id="@id/tvSchedule" android:layout_width="fill_parent" android:layout_height="wrap_content" android:textIsSelectable="true" android:layout_marginStart="8.0dip" style="?textAppearanceBodyMedium" />
</LinearLayout>
<LinearLayout android:gravity="center_vertical" android:orientation="horizontal" android:id="@id/sourceLayout" android:visibility="gone" android:layout_width="fill_parent" android:layout_height="fill_parent" android:layout_marginTop="2.0dip">
<androidx.appcompat.widget.AppCompatImageView android:layout_width="wrap_content" android:layout_height="wrap_content" app:srcCompat="@drawable/ic_source" />
<TextView android:textColor="?primaryTextColor" android:textColorLink="?primaryTextColor" android:gravity="center_vertical" android:linksClickable="true" android:id="@id/tvSource" android:layout_width="fill_parent" android:layout_height="wrap_content" android:lineSpacingExtra="4.0sp" android:textIsSelectable="true" android:layout_marginStart="8.0dip" style="?textAppearanceBodyMedium" />
</LinearLayout>
<LinearLayout android:orientation="horizontal" android:id="@id/studioLayout" android:visibility="gone" android:layout_width="fill_parent" android:layout_height="wrap_content" android:layout_marginTop="2.0dip">
<androidx.appcompat.widget.AppCompatImageView android:layout_width="wrap_content" android:layout_height="wrap_content" app:srcCompat="@drawable/ic_studio" />
<TextView android:textColor="?primaryTextColor" android:textColorLink="?primaryTextColor" android:gravity="center_vertical" android:linksClickable="true" android:id="@id/tvStudio" android:layout_width="fill_parent" android:layout_height="wrap_content" android:lineSpacingExtra="4.0sp" android:textIsSelectable="true" android:layout_marginStart="8.0dip" style="?textAppearanceBodyMedium" />
</LinearLayout>
</LinearLayout>
<TextView android:textColor="?primaryTextColor" android:textColorLink="?primaryTextColor" android:linksClickable="true" android:id="@id/tvGenres" android:visibility="gone" android:layout_width="fill_parent" android:layout_height="wrap_content" android:layout_marginTop="16.0dip" android:layout_marginBottom="2.0dip" android:lineSpacingExtra="4.0sp" android:textIsSelectable="true" style="?textAppearanceBodyMedium" />
<at.blogc.android.views.ExpandableTextView android:textColor="?primaryTextColor" android:id="@id/description" android:layout_width="fill_parent" android:layout_height="wrap_content" android:layout_marginTop="8.0dip" android:layout_marginBottom="8.0dip" android:maxLines="5" android:lineSpacingExtra="4.0sp" android:textIsSelectable="true" app:animation_duration="225" style="?textAppearanceBodyMedium" />
+228
View File
@@ -0,0 +1,228 @@
import xml.etree.ElementTree as ET
from pathlib import Path
from typing import Optional
import yaml
from pydantic import BaseModel, Field, computed_field
from rich.console import Console
from utils.tools import DECOMPILED, MODIFIED, TOOLS, run
class APKMeta(BaseModel):
"""Метаданные APK файла"""
version_code: int = Field(default=0)
version_name: str = Field(default="unknown")
package: str = Field(default="unknown")
path: Path
@computed_field
@property
def safe_version(self) -> str:
"""Версия, безопасная для использования в именах файлов"""
return self.version_name.lower().replace(" ", "-").replace(".", "-")
@computed_field
@property
def output_name(self) -> str:
"""Имя выходного файла"""
return f"Anixarts-v{self.safe_version}.apk"
@computed_field
@property
def aligned_name(self) -> str:
"""Имя выровненного файла"""
return f"Anixarts-v{self.safe_version}-aligned.apk"
@computed_field
@property
def signed_name(self) -> str:
"""Имя подписанного файла"""
return f"Anixarts-v{self.safe_version}-mod.apk"
class SigningConfig(BaseModel):
"""Конфигурация подписи APK"""
keystore: Path = Field(default=Path("keystore.jks"))
keystore_pass_file: Path = Field(default=Path("keystore.pass"))
v1_signing: bool = Field(default=False)
v2_signing: bool = Field(default=True)
v3_signing: bool = Field(default=True)
class APKProcessor:
"""Класс для работы с APK файлами"""
def __init__(self, console: Console, tools_dir: Path = TOOLS):
self.console = console
self.tools_dir = tools_dir
self.apktool_jar = tools_dir / "apktool.jar"
def decompile(self, apk: Path, output: Path = DECOMPILED) -> None:
"""Декомпилирует APK файл"""
self.console.print("[yellow]Декомпиляция APK...")
run(
self.console,
[
"java",
"-jar",
str(self.apktool_jar),
"d",
"-f",
"-o",
str(output),
str(apk),
],
)
self.console.print("[green]✔ Декомпиляция завершена")
def compile(self, source: Path, output: Path) -> None:
"""Компилирует APK из исходников"""
self.console.print("[yellow]Сборка APK...")
run(
self.console,
[
"java",
"-jar",
str(self.apktool_jar),
"b",
str(source),
"-o",
str(output),
],
)
self.console.print("[green]✔ Сборка завершена")
def align(self, input_apk: Path, output_apk: Path) -> None:
"""Выравнивает APK с помощью zipalign"""
self.console.print("[yellow]Выравнивание APK...")
run(
self.console, ["zipalign", "-f", "-v", "4", str(input_apk), str(output_apk)]
)
self.console.print("[green]✔ Выравнивание завершено")
def sign(
self,
input_apk: Path,
output_apk: Path,
config: Optional[SigningConfig] = None,
) -> None:
"""Подписывает APK"""
if config is None:
config = SigningConfig()
self.console.print("[yellow]Подпись APK...")
run(
self.console,
[
"apksigner",
"sign",
"--v1-signing-enabled",
str(config.v1_signing).lower(),
"--v2-signing-enabled",
str(config.v2_signing).lower(),
"--v3-signing-enabled",
str(config.v3_signing).lower(),
"--ks",
str(config.keystore),
"--ks-pass",
f"file:{config.keystore_pass_file}",
"--out",
str(output_apk),
str(input_apk),
],
)
self.console.print("[green]✔ Подпись завершена")
def _get_package_name_from_manifest(self, decompiled_path: Path) -> str:
"""Читает имя пакета напрямую из AndroidManifest.xml"""
manifest_path = decompiled_path / "AndroidManifest.xml"
if not manifest_path.exists():
return "unknown"
try:
tree = ET.parse(manifest_path)
root = tree.getroot()
return root.get("package", "unknown")
except Exception:
return "unknown"
def get_meta(self, decompiled: Path = DECOMPILED) -> APKMeta:
"""Извлекает метаданные из декомпилированного APK"""
apktool_yml = decompiled / "apktool.yml"
if not apktool_yml.exists():
raise FileNotFoundError(f"Файл {apktool_yml} не найден")
with open(apktool_yml, encoding="utf-8") as f:
meta = yaml.safe_load(f)
version_info = meta.get("versionInfo", {})
package_name = self._get_package_name_from_manifest(decompiled)
if package_name == "unknown":
package_info = meta_yaml.get("packageInfo", {})
package_name = package_info.get("renameManifestPackage") or "unknown"
return APKMeta(
version_code=version_info.get("versionCode", 0),
version_name=version_info.get("versionName", "unknown"),
package=package_name,
path=decompiled,
)
def _extract_package_from_manifest(self, decompiled: Path) -> str | None:
"""Извлекает имя пакета из AndroidManifest.xml"""
manifest = decompiled / "AndroidManifest.xml"
if not manifest.exists():
return None
try:
import re
content = manifest.read_text(encoding="utf-8")
match = re.search(r'package="([^"]+)"', content)
if match:
return match.group(1)
except Exception:
pass
return None
def build_and_sign(
self,
source: Path = DECOMPILED,
output_dir: Path = MODIFIED,
signing_config: Optional[SigningConfig] = None,
cleanup: bool = True,
) -> tuple[Path, APKMeta]:
"""
Полный цикл сборки: компиляция, выравнивание, подпись.
Возвращает путь к подписанному APK и метаданные.
"""
meta = self.get_meta(source)
out_apk = output_dir / meta.output_name
aligned_apk = output_dir / meta.aligned_name
signed_apk = output_dir / meta.signed_name
for f in [out_apk, aligned_apk, signed_apk]:
f.unlink(missing_ok=True)
self.compile(source, out_apk)
self.align(out_apk, aligned_apk)
self.sign(aligned_apk, signed_apk, signing_config)
if cleanup:
out_apk.unlink(missing_ok=True)
aligned_apk.unlink(missing_ok=True)
idsig = signed_apk.with_suffix(".apk.idsig")
idsig.unlink(missing_ok=True)
self.console.print(f"[green]✔ APK готов: {signed_apk.name}")
return signed_apk, meta
+120 -13
View File
@@ -1,30 +1,137 @@
from pydantic import BaseModel, Field, ValidationError import json
from rich.console import Console import traceback
from typing import Dict, Any from abc import ABC, abstractmethod
from pathlib import Path from pathlib import Path
import typer from typing import Any, Dict, Literal
from pydantic import BaseModel, ConfigDict, Field, PrivateAttr, field_validator
from rich.console import Console
from utils.tools import CONFIGS
class ToolsConfig(BaseModel): class ToolsConfig(BaseModel):
apktool_jar_url: str apktool_jar_url: str
apktool_wrapper_url: str apktool_wrapper_url: str
@field_validator("apktool_jar_url", "apktool_wrapper_url")
@classmethod
def validate_url(cls, v: str) -> str:
if not v.startswith(("http://", "https://")):
raise ValueError("URL должен начинаться с http:// или https://")
return v
class SigningConfig(BaseModel):
keystore: Path = Field(default=Path("keystore.jks"))
keystore_pass_file: Path = Field(default=Path("keystore.pass"))
v1_signing: bool = False
v2_signing: bool = True
v3_signing: bool = True
class BuildConfig(BaseModel):
verbose: bool = False
force: bool = False
clean_after_build: bool = True
class Config(BaseModel): class Config(BaseModel):
tools: ToolsConfig tools: ToolsConfig
base: Dict[str, Any] signing: SigningConfig = Field(default_factory=SigningConfig)
build: BuildConfig = Field(default_factory=BuildConfig)
base: Dict[str, Any] = Field(default_factory=dict)
class PatchConfig(BaseModel):
enabled: bool = Field(True, description="Включить или отключить патч")
def load_config(console: Console) -> Config: def load_config(console: Console) -> Config:
try: """Загружает и валидирует конфигурацию"""
return Config.model_validate_json(Path("config.json").read_text()) config_path = Path("config.json")
except FileNotFoundError:
if not config_path.exists():
console.print("[red]Файл config.json не найден") console.print("[red]Файл config.json не найден")
raise typer.Exit(1) raise typer.Exit(1)
try:
return Config.model_validate_json(config_path.read_text())
except ValidationError as e: except ValidationError as e:
console.print("[red]Ошибка валидации config.json:", e) console.print(f"[red]Ошибка валидации config.json:\n{e}")
raise typer.Exit(1) raise typer.Exit(1)
class PatchTemplate(BaseModel, ABC):
model_config = ConfigDict(arbitrary_types_allowed=True, validate_default=True)
enabled: bool = Field(default=True, description="Включить или отключить патч")
priority: int = Field(default=0, description="Приоритет применения патча")
_name: str = PrivateAttr()
_applied: bool = PrivateAttr(default=False)
_console: Console | None = PrivateAttr(default=None)
def __init__(self, name: str, console: Console, **data):
loaded_data = self._load_config_static(name, console)
merged_data = {**loaded_data, **data}
valid_fields = set(self.model_fields.keys())
filtered_data = {k: v for k, v in merged_data.items() if k in valid_fields}
super().__init__(**filtered_data)
self._name = name
self._console = console
self._applied = False
@staticmethod
def _load_config_static(name: str, console: Console | None) -> Dict[str, Any]:
"""Загружает конфигурацию из файла (статический метод)"""
config_path = CONFIGS / f"{name}.json"
try:
if config_path.exists():
return json.loads(config_path.read_text())
except Exception as e:
if console:
console.print(
f"[red]Ошибка при загрузке конфигурации патча {name}: {e}"
)
console.print(f"[yellow]Используются значения по умолчанию")
return {}
def save_config(self) -> None:
"""Сохраняет конфигурацию в файл"""
config_path = CONFIGS / f"{self._name}.json"
config_path.parent.mkdir(parents=True, exist_ok=True)
config_path.write_text(self.model_dump_json(indent=2))
@property
def name(self) -> str:
return self._name
@property
def applied(self) -> bool:
return self._applied
@applied.setter
def applied(self, value: bool) -> None:
self._applied = value
@property
def console(self) -> Console | None:
return self._console
@abstractmethod
def apply(self, base: Dict[str, Any]) -> Any:
raise NotImplementedError(
"Попытка применения шаблона патча, а не его реализации"
)
def safe_apply(self, base: Dict[str, Any]) -> bool:
"""Безопасно применяет патч с обработкой ошибок"""
try:
self._applied = self.apply(base)
return self._applied
except Exception as e:
if self._console:
self._console.print(f"[red]Ошибка в патче {self._name}: {e}")
if base.get("verbose"):
self._console.print_exception()
return False
+159
View File
@@ -0,0 +1,159 @@
from typing import Any, get_args, get_origin
from pydantic import BaseModel
from pydantic.fields import FieldInfo
from rich.console import Console
from rich.table import Table
def format_field_type(annotation: Any) -> str:
"""Форматирует тип поля для отображения"""
if annotation is None:
return "None"
origin = get_origin(annotation)
if origin is not None:
args = get_args(annotation)
origin_name = getattr(origin, "__name__", str(origin))
if origin_name == "UnionType" or str(origin) == "typing.Union":
args_str = " | ".join(format_field_type(a) for a in args)
return args_str
if args:
args_str = ", ".join(format_field_type(a) for a in args)
return f"{origin_name}[{args_str}]"
return origin_name
if isinstance(annotation, type) and issubclass(annotation, BaseModel):
return f"[magenta]{annotation.__name__}[/magenta]"
return getattr(annotation, "__name__", str(annotation))
def print_model_fields(
console: Console,
model_class: type[BaseModel],
indent: int = 0,
visited: set | None = None,
) -> None:
"""Рекурсивно выводит поля модели с поддержкой вложенных моделей"""
if visited is None:
visited = set()
if model_class in visited:
console.print(
f"{' ' * indent}[dim](циклическая ссылка на {model_class.__name__})[/dim]"
)
return
visited.add(model_class)
prefix = " " * indent
for field_name, field_info in model_class.model_fields.items():
annotation = field_info.annotation
field_type = format_field_type(annotation)
default = field_info.default
description = field_info.description or ""
if default is None:
default_str = "[dim]None[/dim]"
elif default is ...:
default_str = "[red]required[/red]"
elif isinstance(default, bool):
default_str = "[green]true[/green]" if default else "[red]false[/red]"
else:
default_str = str(default)
console.print(
f"{prefix}[yellow]{field_name}[/yellow]: {field_type} = {default_str}"
+ (f" [dim]# {description}[/dim]" if description else "")
)
nested_model = None
if isinstance(annotation, type) and issubclass(annotation, BaseModel):
nested_model = annotation
else:
origin = get_origin(annotation)
if origin is not None:
for arg in get_args(annotation):
if isinstance(arg, type) and issubclass(arg, BaseModel):
nested_model = arg
break
if nested_model is not None:
console.print(f"{prefix} [dim]└─ {nested_model.__name__}:[/dim]")
print_model_fields(console, nested_model, indent + 2, visited.copy())
def print_model_table(
console: Console,
model_class: type[BaseModel],
prefix: str = "",
visited: set | None = None,
) -> Table:
"""Выводит поля модели в виде таблицы с вложенными моделями"""
if visited is None:
visited = set()
table = Table(show_header=True, box=None if prefix else None)
table.add_column("Поле", style="yellow")
table.add_column("Тип", style="cyan")
table.add_column("По умолчанию")
table.add_column("Описание", style="dim")
_add_model_rows(table, model_class, prefix, visited)
return table
def _add_model_rows(
table: Table,
model_class: type[BaseModel],
prefix: str = "",
visited: set | None = None,
) -> None:
"""Добавляет строки модели в таблицу рекурсивно"""
if visited is None:
visited = set()
if model_class in visited:
return
visited.add(model_class)
for field_name, field_info in model_class.model_fields.items():
annotation = field_info.annotation
field_type = format_field_type(annotation)
default = field_info.default
description = field_info.description or ""
if default is None:
default_str = "-"
elif default is ...:
default_str = "[red]required[/red]"
elif isinstance(default, bool):
default_str = "true" if default else "false"
elif isinstance(default, BaseModel):
default_str = "{...}"
else:
default_str = str(default)[:20]
full_name = f"{prefix}{field_name}" if prefix else field_name
table.add_row(full_name, field_type, default_str, description[:40])
nested_model = None
if isinstance(annotation, type) and issubclass(annotation, BaseModel):
nested_model = annotation
else:
origin = get_origin(annotation)
if origin is not None:
for arg in get_args(annotation):
if isinstance(arg, type) and issubclass(arg, BaseModel):
nested_model = arg
break
if nested_model is not None and nested_model not in visited:
_add_model_rows(table, nested_model, f" {full_name}.", visited.copy())
+107
View File
@@ -0,0 +1,107 @@
import importlib
from contextlib import contextmanager
from functools import wraps
from pathlib import Path
from typing import Dict, List, Type
import typer
from rich.console import Console
from utils.config import PatchTemplate
from utils.tools import PATCHES
class PatcherError(Exception):
"""Базовое исключение патчера"""
pass
class ConfigError(PatcherError):
"""Ошибка конфигурации"""
pass
class BuildError(PatcherError):
"""Ошибка сборки"""
pass
def handle_errors(func):
"""Декоратор для обработки ошибок CLI"""
@wraps(func)
def wrapper(*args, **kwargs):
try:
return func(*args, **kwargs)
except PatcherError as e:
Console().print(f"[red]Ошибка: {e}")
raise typer.Exit(1)
except KeyboardInterrupt:
Console().print("\n[yellow]Прервано пользователем")
raise typer.Exit(130)
return wrapper
class PatchManager:
"""Менеджер для работы с патчами"""
def __init__(self, console: Console, patches_dir: Path = PATCHES):
self.console = console
self.patches_dir = patches_dir
def discover_patches(self, include_todo: bool = False) -> List[str]:
"""Находит все доступные патчи"""
patches = []
for f in self.patches_dir.glob("*.py"):
if f.name == "__init__.py":
continue
if f.name.startswith("todo_") and not include_todo:
continue
patches.append(f.stem)
return patches
def discover_all(self) -> Dict[str, List[str]]:
"""Находит все патчи, разделяя на готовые и в разработке"""
ready = []
todo = []
for f in self.patches_dir.glob("*.py"):
if f.name == "__init__.py":
continue
if f.name.startswith("todo_"):
todo.append(f.stem)
else:
ready.append(f.stem)
return {"ready": ready, "todo": todo}
def load_patch_module(self, name: str) -> type:
"""Загружает модуль патча"""
module = importlib.import_module(f"patches.{name}")
return module
def load_patch_class(self, name: str) -> type:
"""Загружает класс патча"""
module = importlib.import_module(f"patches.{name}")
return module.Patch
def load_patch(self, name: str) -> PatchTemplate:
"""Загружает экземпляр патча"""
module = importlib.import_module(f"patches.{name}")
return module.Patch(name=name, console=self.console)
def load_enabled_patches(self) -> List[PatchTemplate]:
"""Загружает все включённые патчи, отсортированные по приоритету"""
patches = []
for name in self.discover_patches():
patch = self.load_patch(name)
if patch.enabled:
patches.append(patch)
else:
self.console.print(f"[dim]≫ Пропускаем {name}[/dim]")
return sorted(patches, key=lambda p: p.priority, reverse=True)
+2 -1
View File
@@ -1,6 +1,7 @@
from typing_extensions import Optional
from copy import deepcopy from copy import deepcopy
from lxml import etree from lxml import etree
from typing_extensions import Optional
def insert_after_public(anchor_name: str, elem_name: str) -> Optional[int]: def insert_after_public(anchor_name: str, elem_name: str) -> Optional[int]:
+10
View File
@@ -64,6 +64,16 @@ def find_and_replace_smali_line(
return lines return lines
def find_smali_line(
lines: list[str], search: str
) -> list[int]:
result = []
for index, line in enumerate(lines):
if line.find(search) >= 0:
result.append(index)
return result
def float_to_hex(f): def float_to_hex(f):
b = struct.pack(">f", f) b = struct.pack(">f", f)
return b.hex() return b.hex()
+26 -5
View File
@@ -1,11 +1,11 @@
from plumbum import local, ProcessExecutionError
from rich.progress import Progress
from rich.console import Console
from pathlib import Path from pathlib import Path
from typing import List from typing import List
import httpx import httpx
import typer import typer
from plumbum import FG, ProcessExecutionError, local
from rich.console import Console
from rich.progress import Progress
TOOLS = Path("tools") TOOLS = Path("tools")
ORIGINAL = Path("original") ORIGINAL = Path("original")
@@ -23,7 +23,7 @@ def ensure_dirs():
def run(console: Console, cmd: List[str], hide_output=True): def run(console: Console, cmd: List[str], hide_output=True):
prog = local[cmd[0]][cmd[1:]] prog = local[cmd[0]][cmd[1:]]
try: try:
prog() if hide_output else prog & FG # type: ignore [reportUndefinedVariable] prog() if hide_output else prog & FG
except ProcessExecutionError as e: except ProcessExecutionError as e:
console.print(f"[red]Ошибка при выполнении команды: {' '.join(cmd)}") console.print(f"[red]Ошибка при выполнении команды: {' '.join(cmd)}")
console.print(e.stderr) console.print(e.stderr)
@@ -31,6 +31,7 @@ def run(console: Console, cmd: List[str], hide_output=True):
def download(console: Console, url: str, dest: Path): def download(console: Console, url: str, dest: Path):
"""Скачивание файла по URL"""
console.print(f"[cyan]Скачивание {url}{dest.name}") console.print(f"[cyan]Скачивание {url}{dest.name}")
with httpx.Client(follow_redirects=True, timeout=60.0) as client: with httpx.Client(follow_redirects=True, timeout=60.0) as client:
@@ -45,3 +46,23 @@ def download(console: Console, url: str, dest: Path):
for chunk in response.iter_bytes(chunk_size=8192): for chunk in response.iter_bytes(chunk_size=8192):
f.write(chunk) f.write(chunk)
progress.update(task, advance=len(chunk)) progress.update(task, advance=len(chunk))
def select_apk(console) -> Path:
"""Выбор APK файла из папки original"""
apks = list(ORIGINAL.glob("*.apk"))
if not apks:
raise BuildError("Нет apk-файлов в папке original")
if len(apks) == 1:
console.print(f"[green]Выбран {apks[0].name}")
return apks[0]
console.print("[cyan]Доступные APK файлы:")
options = {str(i): apk for i, apk in enumerate(apks, 1)}
for k, v in options.items():
console.print(f" {k}. {v.name}")
choice = Prompt.ask("Выберите номер", choices=list(options.keys()))
return options[choice]