Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 70337ee3ec | |||
| ec047cd3a5 | |||
| 2fe61c1445 | |||
| 28c60aa7a3 | |||
| b646dbf6fe | |||
| f46425b169 | |||
| 19e1ce2f45 |
+21
-17
@@ -1,13 +1,10 @@
|
|||||||
name: Build mod
|
name: Сборка мода
|
||||||
|
|
||||||
on:
|
on:
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
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:
|
||||||
@@ -16,25 +13,25 @@ jobs:
|
|||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v3
|
||||||
|
|
||||||
- name: Download APK
|
- name: Скачивание APK
|
||||||
run: |
|
run: |
|
||||||
curl -L -o app.apk "https://mirror-dl.anixart-app.com/anixart-beta.apk"
|
curl -L -o app.apk "https://mirror-dl.anixart-app.com/anixart-beta.apk"
|
||||||
|
|
||||||
- name: Ensure aapt is installed
|
- name: Проверка наличия aapt
|
||||||
run: |
|
run: |
|
||||||
if ! command -v aapt &> /dev/null; then
|
if ! command -v aapt &> /dev/null; then
|
||||||
echo "aapt не найден, устанавливаем..."
|
echo "aapt не найден, устанавливаем..."
|
||||||
sudo apt-get update && sudo apt-get install -y --no-install-recommends android-sdk-build-tools
|
sudo apt-get update && sudo apt-get install -y --no-install-recommends android-sdk-build-tools
|
||||||
fi
|
fi
|
||||||
|
|
||||||
- name: Ensure pngquant is installed
|
- name: Проверка наличия pngquant
|
||||||
run: |
|
run: |
|
||||||
if ! command -v pngquant &> /dev/null; then
|
if ! command -v pngquant &> /dev/null; then
|
||||||
echo "pngquant не найден, устанавливаем..."
|
echo "pngquant не найден, устанавливаем..."
|
||||||
sudo apt-get update && sudo apt-get install -y --no-install-recommends pngquant
|
sudo apt-get update && sudo apt-get install -y --no-install-recommends pngquant
|
||||||
fi
|
fi
|
||||||
|
|
||||||
- name: Export secrets
|
- name: Извлечение хранилища ключей
|
||||||
env:
|
env:
|
||||||
KEYSTORE: ${{ secrets.KEYSTORE }}
|
KEYSTORE: ${{ secrets.KEYSTORE }}
|
||||||
KEYSTORE_PASS: ${{ secrets.KEYSTORE_PASS }}
|
KEYSTORE_PASS: ${{ secrets.KEYSTORE_PASS }}
|
||||||
@@ -43,7 +40,7 @@ jobs:
|
|||||||
echo "$KEYSTORE" | base64 -d > keystore.jks
|
echo "$KEYSTORE" | base64 -d > keystore.jks
|
||||||
echo "$KEYSTORE_PASS" > keystore.pass
|
echo "$KEYSTORE_PASS" > keystore.pass
|
||||||
|
|
||||||
- name: Prepare to build APK
|
- name: Подготовка к модифицированию APK
|
||||||
id: build
|
id: build
|
||||||
run: |
|
run: |
|
||||||
mkdir original
|
mkdir original
|
||||||
@@ -51,32 +48,39 @@ jobs:
|
|||||||
pip install -r ./requirements.txt --break-system-packages
|
pip install -r ./requirements.txt --break-system-packages
|
||||||
python ./main.py init
|
python ./main.py init
|
||||||
|
|
||||||
- name: Build APK
|
- name: Пересборка APK
|
||||||
id: build
|
id: build
|
||||||
run: |
|
run: |
|
||||||
python ./main.py build -f
|
python ./main.py build -f
|
||||||
|
|
||||||
- name: Read title from 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: Setup go
|
- 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
|
||||||
if: steps.build.outputs.BUILD_EXIT == '0'
|
if: steps.build.outputs.BUILD_EXIT == '0'
|
||||||
uses: actions/setup-go@v4
|
uses: actions/setup-go@v4
|
||||||
with:
|
with:
|
||||||
go-version: '>=1.20'
|
go-version: '>=1.20'
|
||||||
|
|
||||||
- name: Make release
|
- name: Создание релиза
|
||||||
if: steps.build.outputs.BUILD_EXIT == '0'
|
if: steps.build.outputs.BUILD_EXIT == '0'
|
||||||
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: |-
|
||||||
modified/**-mod.apk
|
modified/*-mod.apk
|
||||||
modified/report.log
|
modified/report.log
|
||||||
|
|||||||
@@ -76,6 +76,6 @@ flowchart TD
|
|||||||
Этот проект лицензирован под лицензией MIT. См. [LICENSE](./LICENSE) для получения подробной информации.
|
Этот проект лицензирован под лицензией MIT. См. [LICENSE](./LICENSE) для получения подробной информации.
|
||||||
|
|
||||||
### Вклад в проект:
|
### Вклад в проект:
|
||||||
- Kentai Radiquum - Значительный вклад в развитие патчера, разработка [anix](https://github.com/AniX-org/AniX) и помощь с API [[GitHub](https://github.com/adiquum) | [Telegram](https://t.me/radiquum)]
|
- [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)]
|
||||||
- Seele - Оригинальные патчи в начале разработки основаны на модификации от Seele
|
- [Seele](https://git.0x174.su/seele_archive) - Оригинальные патчи в начале разработки основаны на модификации от Seele
|
||||||
- ReCode Liner - Помощь в изучении моддинга приложения [[Telegram](https://t.me/recodius)]
|
- [ReCode Liner](https://git.0x174.su/ReCodeLiner) - Помощь в изучении моддинга приложения [[Telegram](https://t.me/recodius)]
|
||||||
|
|||||||
@@ -1 +1,4 @@
|
|||||||
{"enabled":true,"server":"https://anixarty.0x174.su/patch"}
|
{
|
||||||
|
"enabled": false,
|
||||||
|
"server": "https://anixarty.0x174.su/patch"
|
||||||
|
}
|
||||||
@@ -14,4 +14,4 @@
|
|||||||
"background": "#ffffff",
|
"background": "#ffffff",
|
||||||
"text": "#000000"
|
"text": "#000000"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"enabled": true,
|
||||||
|
"replace": true,
|
||||||
|
"custom_icons": true,
|
||||||
|
"icon_size": "18.0dip"
|
||||||
|
}
|
||||||
+13
-1
@@ -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
|
||||||
|
}
|
||||||
@@ -1 +1,3 @@
|
|||||||
{"enabled":true}
|
{
|
||||||
|
"enabled": true
|
||||||
|
}
|
||||||
@@ -1 +1,3 @@
|
|||||||
{"enabled":true}
|
{
|
||||||
|
"enabled": true
|
||||||
|
}
|
||||||
@@ -1 +0,0 @@
|
|||||||
{"enabled":true}
|
|
||||||
@@ -1 +1,4 @@
|
|||||||
{"enabled":true,"package_name":"com.wowlikon.anixart"}
|
{
|
||||||
|
"enabled": true,
|
||||||
|
"package_name": "com.wowlikon.anixart"
|
||||||
|
}
|
||||||
@@ -1 +1,10 @@
|
|||||||
{"enabled":true,"items":["home","discover","feed","bookmarks","profile"]}
|
{
|
||||||
|
"enabled": true,
|
||||||
|
"items": [
|
||||||
|
"home",
|
||||||
|
"discover",
|
||||||
|
"feed",
|
||||||
|
"bookmarks",
|
||||||
|
"profile"
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"enabled": true
|
||||||
|
}
|
||||||
@@ -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.wowlikon.tech/anixart-mod",
|
||||||
|
"icon": "@drawable/ic_custom_crown",
|
||||||
|
"icon_space_reserved": "false"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +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"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"enabled": true,
|
||||||
|
"title": "Anixarty",
|
||||||
|
"description": "Описание",
|
||||||
|
"link_text": "МЫ В TELEGRAM",
|
||||||
|
"link_url": "https://t.me/http_teapod",
|
||||||
|
"skip_text": "Пропустить",
|
||||||
|
"title_bg_color": "#FFFFFF"
|
||||||
|
}
|
||||||
@@ -1,226 +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 httpx
|
|
||||||
import typer
|
import typer
|
||||||
import importlib
|
from plumbum import ProcessExecutionError, local
|
||||||
import traceback
|
|
||||||
import yaml
|
|
||||||
|
|
||||||
from pydantic import BaseModel, ValidationError
|
|
||||||
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 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
|
||||||
# --- Paths ---
|
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="anixarty-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:
|
||||||
self.config = module.Config()
|
info["doc"] = doc.strip().split("\n")[0]
|
||||||
|
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"Anixarty {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():
|
for p in applied_patches:
|
||||||
download(console, conf.tools.apktool_jar_url, TOOLS / "apktool.jar")
|
info = get_patch_info(p)
|
||||||
|
lines.append(
|
||||||
if not (TOOLS / "apktool").exists():
|
f"| ✅ `{p.name}` | {p.priority} | {info['author']} | {info['doc']} |"
|
||||||
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__}")
|
|
||||||
else:
|
else:
|
||||||
console.print("[cyan]Список патчей:")
|
lines.append("> ⚠️ Нет применённых патчей")
|
||||||
for f in PATCHES.glob("*.py"):
|
|
||||||
if f.name.startswith("todo_") or f.name == "__init__.py":
|
|
||||||
continue
|
|
||||||
name = f.stem
|
|
||||||
if conf["patches"].get(name, {}).get("enabled", True):
|
|
||||||
console.print(f" [yellow]{name}: [green]✔ enabled")
|
|
||||||
else:
|
|
||||||
console.print(f" [yellow]{name}: [red]✘ disabled")
|
|
||||||
|
|
||||||
|
lines.append("")
|
||||||
|
|
||||||
def select_apk() -> Path:
|
if failed_patches:
|
||||||
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)
|
lines.append("|------|:---------:|-------|----------|")
|
||||||
|
|
||||||
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)}
|
lines.append("")
|
||||||
for k, v in options.items():
|
|
||||||
console.print(f"{k}. {v.name}")
|
|
||||||
|
|
||||||
choice = Prompt.ask("Выберите номер", choices=list(options.keys()))
|
lines.append("---")
|
||||||
return options[choice]
|
lines.append("")
|
||||||
|
lines.append(
|
||||||
|
"*Собрано с помощью [anixarty-patcher](https://git.0x174.su/anixart-mod/patcher)*"
|
||||||
def decompile(apk: Path):
|
|
||||||
console.print("[yellow]Декомпиляция apk...")
|
|
||||||
run(
|
|
||||||
console,
|
|
||||||
[
|
|
||||||
"java",
|
|
||||||
"-jar", str(TOOLS / "apktool.jar"),
|
|
||||||
"d", "-f",
|
|
||||||
"-o", str(DECOMPILED),
|
|
||||||
str(apk),
|
|
||||||
]
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
report_path.write_text("\n".join(lines), encoding="utf-8")
|
||||||
|
console.print(f"[dim]Отчёт сохранён: {report_path}[/dim]")
|
||||||
|
|
||||||
def compile(apk: Path, patches: List[Patch]):
|
|
||||||
console.print("[yellow]Сборка apk...")
|
|
||||||
|
|
||||||
with open(DECOMPILED / "apktool.yml", encoding="utf-8") as f:
|
# ========================= COMMANDS =========================
|
||||||
meta = yaml.safe_load(f)
|
@app.command()
|
||||||
version_info = meta.get("versionInfo", {})
|
@handle_errors
|
||||||
version_code = version_info.get("versionCode", 0)
|
def init():
|
||||||
version_name = version_info.get("versionName", "unknown")
|
"""Инициализация: создание директорий и скачивание инструментов"""
|
||||||
|
ensure_dirs()
|
||||||
|
|
||||||
filename_version = version_name.lower().replace(" ", "-").replace(".", "-")
|
conf = load_config(console)
|
||||||
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(
|
# Проверка Java
|
||||||
console,
|
console.print("[cyan]Проверка Java...")
|
||||||
[
|
try:
|
||||||
"java",
|
local["java"]["-version"].run(retcode=None)
|
||||||
"-jar", str(TOOLS / "apktool.jar"),
|
console.print("[green]✔ Java найдена")
|
||||||
"b", str(DECOMPILED),
|
except ProcessExecutionError:
|
||||||
"-o", str(out_apk),
|
raise PatcherError("Java не установлена. Установите JDK 11+")
|
||||||
]
|
|
||||||
)
|
|
||||||
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 успешно собран и подписан")
|
# Скачивание apktool
|
||||||
|
apktool_jar = TOOLS / "apktool.jar"
|
||||||
|
if not apktool_jar.exists():
|
||||||
|
download(console, conf.tools.apktool_jar_url, apktool_jar)
|
||||||
|
else:
|
||||||
|
console.print(f"[dim]✔ {apktool_jar.name} уже существует[/dim]")
|
||||||
|
|
||||||
with open(MODIFIED / "report.log", "w", encoding="utf-8") as f:
|
# Скачивание apktool wrapper
|
||||||
f.write(f"Anixarty mod v {version_name} ({version_code})\n")
|
apktool_wrapper = TOOLS / "apktool"
|
||||||
for p in patches:
|
if not apktool_wrapper.exists():
|
||||||
f.write(f"{'✔' if p.applied else '✘'} {p.name}\n")
|
download(console, conf.tools.apktool_wrapper_url, apktool_wrapper)
|
||||||
|
apktool_wrapper.chmod(0o755)
|
||||||
|
else:
|
||||||
|
console.print(f"[dim]✔ {apktool_wrapper.name} уже существует[/dim]")
|
||||||
|
|
||||||
|
# Проверка zipalign и apksigner
|
||||||
|
for tool in ["zipalign", "apksigner"]:
|
||||||
|
try:
|
||||||
|
local[tool]["--version"].run(retcode=None)
|
||||||
|
console.print(f"[green]✔ {tool} найден")
|
||||||
|
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()
|
@app.command()
|
||||||
def build(
|
@handle_errors
|
||||||
force: bool = typer.Option(False, "--force", "-f", help="Принудительная сборка"),
|
def info(
|
||||||
verbose: bool = typer.Option(False, "--verbose", "-v", help="Подробный вывод"),
|
patch_name: str = typer.Argument(..., help="Имя патча"),
|
||||||
|
tree: bool = typer.Option(False, "--tree", "-t", help="Древовидный вывод полей"),
|
||||||
):
|
):
|
||||||
"""Декомпиляция, патчи и сборка apk"""
|
"""Показать подробную информацию о патче"""
|
||||||
|
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)
|
conf = load_config(console)
|
||||||
apk = select_apk()
|
|
||||||
decompile(apk)
|
|
||||||
|
|
||||||
patch_objs: List[Patch] = []
|
apk_processor = APKProcessor(console, TOOLS)
|
||||||
conf.base |= {"verbose": verbose}
|
|
||||||
|
|
||||||
for f in PATCHES.glob("*.py"):
|
apk = select_apk(console)
|
||||||
if f.name.startswith("todo_") or f.name == "__init__.py":
|
apk_processor.decompile(apk, DECOMPILED)
|
||||||
continue
|
|
||||||
name = f.stem
|
|
||||||
module = importlib.import_module(f"patches.{name}")
|
|
||||||
if not module.Config.model_validate_json((CONFIGS / f"{name}.json").read_text()):
|
|
||||||
console.print(f"[yellow]≫ Пропускаем {name}")
|
|
||||||
continue
|
|
||||||
patch_objs.append(Patch(name, module))
|
|
||||||
|
|
||||||
patch_objs.sort(key=lambda p: p.priority, reverse=True)
|
manager = PatchManager(console)
|
||||||
|
patches = manager.load_enabled_patches()
|
||||||
|
|
||||||
console.print("[cyan]Применение патчей")
|
if not patches:
|
||||||
with Progress() as progress:
|
console.print("[yellow]Нет включённых патчей")
|
||||||
task = progress.add_task("Патчи", total=len(patch_objs))
|
if not force:
|
||||||
for p in patch_objs:
|
raise typer.Exit(0)
|
||||||
ok = p.apply(conf.base)
|
|
||||||
progress.console.print(f"{'✔' if ok else '✘'} {p.name}")
|
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)
|
||||||
|
|
||||||
|
|
||||||
|
@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]anixarty-patcher[/cyan] v{__version__}")
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
app()
|
app()
|
||||||
|
|||||||
+56
-42
@@ -1,5 +1,4 @@
|
|||||||
"""
|
"""Заменяет сервер api
|
||||||
Заменяет сервер api
|
|
||||||
|
|
||||||
"change_server": {
|
"change_server": {
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
@@ -7,58 +6,73 @@
|
|||||||
}
|
}
|
||||||
"""
|
"""
|
||||||
|
|
||||||
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):
|
class Patch(PatchTemplate):
|
||||||
|
priority: int = Field(frozen=True, exclude=True, default=0)
|
||||||
server: str = Field("https://anixarty.0x174.su/patch", description="URL сервера")
|
server: str = Field("https://anixarty.0x174.su/patch", description="URL сервера")
|
||||||
|
|
||||||
# Patch
|
def apply(self, base: Dict[str, Any]) -> bool:
|
||||||
def apply(config: Config, base: Dict[str, Any]) -> bool:
|
response = requests.get(self.server) # Получаем данные для патча
|
||||||
response = requests.get(config.server)
|
assert (
|
||||||
assert response.status_code == 200, f"Failed to fetch data {response.status_code} {response.text}"
|
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']:
|
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()
|
||||||
|
|
||||||
|
with open(filepath, "w") as f:
|
||||||
|
if content.count(item["src"]) == 0:
|
||||||
|
tqdm.write(f"⚠ Не найдено {item['src']}")
|
||||||
|
f.write(content.replace(item["src"], item["dst"]))
|
||||||
|
|
||||||
|
tqdm.write(
|
||||||
|
f"Изменение Github ссылки"
|
||||||
|
) # Обновление ссылки на поиск серверов в Github
|
||||||
|
filepath = "./decompiled/smali_classes2/com/swiftsoft/anixartd/utils/anixnet/GithubPagesNetFetcher.smali"
|
||||||
|
|
||||||
|
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:
|
f.write(
|
||||||
tqdm.write(f"⚠ Не найдено {item['src']}")
|
content.replace(
|
||||||
f.write(content.replace(item['src'], item['dst']))
|
'const-string v1, "https://anixhelper.github.io/pages/urls.json"',
|
||||||
|
f'const-string v1, "{new_api["gh"]}"',
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
tqdm.write(f"Изменение Github ссылки")
|
tqdm.write(
|
||||||
filepath = './decompiled/smali_classes2/com/swiftsoft/anixartd/utils/anixnet/GithubPagesNetFetcher.smali'
|
"Удаление динамического выбора сервера"
|
||||||
|
) # Отключение автовыбора сервера
|
||||||
|
filepath = "./decompiled/smali_classes2/com/swiftsoft/anixartd/DaggerApp_HiltComponents_SingletonC$SingletonCImpl$SwitchingProvider.smali"
|
||||||
|
|
||||||
with open(filepath, 'r') as f:
|
content = ""
|
||||||
content = f.read()
|
with open(filepath, "r") as f:
|
||||||
|
for line in f.readlines():
|
||||||
|
if "addInterceptor" in line:
|
||||||
|
continue
|
||||||
|
content += line
|
||||||
|
|
||||||
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)
|
||||||
|
|
||||||
content = ""
|
return True
|
||||||
tqdm.write("Удаление динамического выбора сервера")
|
|
||||||
filepath = './decompiled/smali_classes2/com/swiftsoft/anixartd/DaggerApp_HiltComponents_SingletonC$SingletonCImpl$SwitchingProvider.smali'
|
|
||||||
|
|
||||||
with open(filepath, 'r') as f:
|
|
||||||
for line in f.readlines():
|
|
||||||
if "addInterceptor" in line: continue
|
|
||||||
content += line
|
|
||||||
|
|
||||||
with open(filepath, 'w') as f:
|
|
||||||
f.write(content)
|
|
||||||
|
|
||||||
return True
|
|
||||||
|
|||||||
+147
-85
@@ -1,5 +1,4 @@
|
|||||||
"""
|
"""Изменяет цветовую тему приложения и иконку
|
||||||
Изменяет цветовую тему приложения и иконку
|
|
||||||
|
|
||||||
"color_theme": {
|
"color_theme": {
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
@@ -20,126 +19,189 @@
|
|||||||
}
|
}
|
||||||
"""
|
"""
|
||||||
|
|
||||||
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="Настройки цветов")
|
||||||
|
|
||||||
# No connection alert coolor
|
@model_validator(mode="before")
|
||||||
with open("./decompiled/assets/no_connection.html", "r", encoding="utf-8") as file:
|
@classmethod
|
||||||
file_contents = file.read()
|
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
|
||||||
|
|
||||||
new_contents = file_contents.replace("#f04e4e", main_color)
|
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,
|
||||||
|
)
|
||||||
|
|
||||||
with open("./decompiled/assets/no_connection.html", "w", encoding="utf-8") as file:
|
def apply(self, base: Dict[str, Any]) -> bool:
|
||||||
file.write(new_contents)
|
main_color = self.colors.primary
|
||||||
|
splash_color = self.colors.secondary
|
||||||
|
|
||||||
# For logo
|
# Обновление сообщения об отсутствии подключения
|
||||||
drawable_types = ["", "-night"]
|
with open(
|
||||||
|
"./decompiled/assets/no_connection.html", "r", encoding="utf-8"
|
||||||
|
) as file:
|
||||||
|
file_contents = file.read()
|
||||||
|
|
||||||
for drawable_type in drawable_types:
|
new_contents = file_contents.replace("#f04e4e", main_color)
|
||||||
# Application logo gradient colors
|
|
||||||
file_path = f"./decompiled/res/drawable{drawable_type}/$logo__0.xml"
|
|
||||||
|
|
||||||
parser = etree.XMLParser(remove_blank_text=True)
|
with open(
|
||||||
tree = etree.parse(file_path, parser)
|
"./decompiled/assets/no_connection.html", "w", encoding="utf-8"
|
||||||
root = tree.getroot()
|
) as file:
|
||||||
|
file.write(new_contents)
|
||||||
|
|
||||||
# Change attributes with namespace
|
# Суффиксы лого
|
||||||
root.set(f"{{{base['xml_ns']['android']}}}angle", str(config.logo.gradient.angle))
|
drawable_types = ["", "-night"]
|
||||||
root.set(f"{{{base['xml_ns']['android']}}}startColor", config.logo.gradient.start_color)
|
|
||||||
root.set(f"{{{base['xml_ns']['android']}}}endColor", config.logo.gradient.end_color)
|
|
||||||
|
|
||||||
# Save back
|
for drawable_type in drawable_types:
|
||||||
tree.write(file_path, pretty_print=True, xml_declaration=True, encoding="utf-8")
|
# Градиент лого приложения
|
||||||
|
file_path = f"./decompiled/res/drawable{drawable_type}/$logo__0.xml"
|
||||||
|
|
||||||
# Application logo anim color
|
parser = etree.XMLParser(remove_blank_text=True)
|
||||||
file_path = f"./decompiled/res/drawable{drawable_type}/$logo_splash_anim__0.xml"
|
tree = etree.parse(file_path, parser)
|
||||||
|
root = tree.getroot()
|
||||||
|
|
||||||
parser = etree.XMLParser(remove_blank_text=True)
|
# Замена атрибутов значениями из конфигурации
|
||||||
tree = etree.parse(file_path, parser)
|
root.set(
|
||||||
root = tree.getroot()
|
f"{{{base['xml_ns']['android']}}}angle", str(self.logo.gradient.angle)
|
||||||
|
)
|
||||||
|
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
|
||||||
|
)
|
||||||
|
|
||||||
# Finding "path"
|
# Сохранение
|
||||||
for el in root.findall("path", namespaces=base["xml_ns"]):
|
tree.write(
|
||||||
name = el.get(f"{{{base['xml_ns']['android']}}}name")
|
file_path, pretty_print=True, xml_declaration=True, encoding="utf-8"
|
||||||
if name == "path":
|
)
|
||||||
el.set(f"{{{base['xml_ns']['android']}}}fillColor", config.colors.secondary)
|
|
||||||
elif name in ["path_1", "path_2"]:
|
|
||||||
el.set(f"{{{base['xml_ns']['android']}}}fillColor", config.logo.ears_color)
|
|
||||||
|
|
||||||
# Save back
|
# Замена анимации лого
|
||||||
tree.write(file_path, pretty_print=True, xml_declaration=True, encoding="utf-8")
|
file_path = (
|
||||||
|
f"./decompiled/res/drawable{drawable_type}/$logo_splash_anim__0.xml"
|
||||||
|
)
|
||||||
|
|
||||||
for filename in ["$ic_launcher_foreground__0", "$ic_banner_foreground__0"]:
|
parser = etree.XMLParser(remove_blank_text=True)
|
||||||
file_path = f"./decompiled/res/drawable-v24/{filename}.xml"
|
tree = etree.parse(file_path, parser)
|
||||||
|
root = tree.getroot()
|
||||||
|
|
||||||
parser = etree.XMLParser(remove_blank_text=True)
|
for el in root.findall("path", namespaces=base["xml_ns"]):
|
||||||
tree = etree.parse(file_path, parser)
|
name = el.get(f"{{{base['xml_ns']['android']}}}name")
|
||||||
root = tree.getroot()
|
if name == "path":
|
||||||
|
el.set(
|
||||||
|
f"{{{base['xml_ns']['android']}}}fillColor",
|
||||||
|
self.colors.secondary,
|
||||||
|
)
|
||||||
|
elif name in ["path_1", "path_2"]:
|
||||||
|
el.set(
|
||||||
|
f"{{{base['xml_ns']['android']}}}fillColor",
|
||||||
|
self.logo.ears_color,
|
||||||
|
)
|
||||||
|
|
||||||
# Change attributes with namespace
|
# Сохранение
|
||||||
root.set(f"{{{base['xml_ns']['android']}}}angle", str(config.logo.gradient.angle))
|
tree.write(
|
||||||
items = root.findall("item", namespaces=base['xml_ns'])
|
file_path, pretty_print=True, xml_declaration=True, encoding="utf-8"
|
||||||
assert len(items) == 2
|
)
|
||||||
items[0].set(f"{{{base['xml_ns']['android']}}}color", config.logo.gradient.start_color)
|
|
||||||
items[1].set(f"{{{base['xml_ns']['android']}}}color", config.logo.gradient.end_color)
|
|
||||||
|
|
||||||
# Save back
|
for filename in ["$ic_launcher_foreground__0", "$ic_banner_foreground__0"]:
|
||||||
tree.write(file_path, pretty_print=True, xml_declaration=True, encoding="utf-8")
|
file_path = f"./decompiled/res/drawable-v24/{filename}.xml"
|
||||||
|
|
||||||
insert_after_public("carmine", "custom_color")
|
parser = etree.XMLParser(remove_blank_text=True)
|
||||||
insert_after_public("carmine_alpha_10", "custom_color_alpha_10")
|
tree = etree.parse(file_path, parser)
|
||||||
insert_after_color("carmine", "custom_color", main_color[0]+'ff'+main_color[1:])
|
root = tree.getroot()
|
||||||
insert_after_color("carmine_alpha_10", "custom_color_alpha_10", main_color[0]+'1a'+main_color[1:])
|
|
||||||
|
|
||||||
change_color("accent_alpha_10", main_color[0]+'1a'+main_color[1:])
|
# Замена атрибутов значениями из конфигурации
|
||||||
change_color("accent_alpha_20", main_color[0]+'33'+main_color[1:])
|
root.set(
|
||||||
change_color("accent_alpha_50", main_color[0]+'80'+main_color[1:])
|
f"{{{base['xml_ns']['android']}}}angle", str(self.logo.gradient.angle)
|
||||||
change_color("accent_alpha_70", main_color[0]+'b3'+main_color[1:])
|
)
|
||||||
|
items = root.findall("item", namespaces=base["xml_ns"])
|
||||||
|
assert len(items) == 2
|
||||||
|
items[0].set(
|
||||||
|
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
|
||||||
|
)
|
||||||
|
|
||||||
change_color("colorAccent", main_color[0]+'ff'+main_color[1:])
|
# Сохранение
|
||||||
change_color("link_color", main_color[0]+'ff'+main_color[1:])
|
tree.write(
|
||||||
change_color("link_color_alpha_70", main_color[0]+'b3'+main_color[1:])
|
file_path, pretty_print=True, xml_declaration=True, encoding="utf-8"
|
||||||
change_color("refresh_progress", main_color[0]+'ff'+main_color[1:])
|
)
|
||||||
|
|
||||||
change_color("ic_launcher_background", "#ff000000")
|
# Добаление новых цветов для темы
|
||||||
change_color("bottom_nav_indicator_active", "#ffffffff")
|
insert_after_public("carmine", "custom_color")
|
||||||
change_color("bottom_nav_indicator_icon_checked", main_color[0]+'ff'+main_color[1:])
|
insert_after_public("carmine_alpha_10", "custom_color_alpha_10")
|
||||||
change_color("bottom_nav_indicator_label_checked", main_color[0]+'ff'+main_color[1:])
|
insert_after_color(
|
||||||
|
"carmine", "custom_color", main_color[0] + "ff" + main_color[1:]
|
||||||
|
)
|
||||||
|
insert_after_color(
|
||||||
|
"carmine_alpha_10",
|
||||||
|
"custom_color_alpha_10",
|
||||||
|
main_color[0] + "1a" + main_color[1:],
|
||||||
|
)
|
||||||
|
|
||||||
return True
|
# Замена цветов
|
||||||
|
change_color("accent_alpha_10", main_color[0] + "1a" + main_color[1:])
|
||||||
|
change_color("accent_alpha_20", main_color[0] + "33" + main_color[1:])
|
||||||
|
change_color("accent_alpha_50", main_color[0] + "80" + main_color[1:])
|
||||||
|
change_color("accent_alpha_70", main_color[0] + "b3" + main_color[1:])
|
||||||
|
|
||||||
|
change_color("colorAccent", main_color[0] + "ff" + main_color[1:])
|
||||||
|
change_color("link_color", main_color[0] + "ff" + main_color[1:])
|
||||||
|
change_color("link_color_alpha_70", main_color[0] + "b3" + main_color[1:])
|
||||||
|
change_color("refresh_progress", main_color[0] + "ff" + main_color[1:])
|
||||||
|
|
||||||
|
change_color("ic_launcher_background", "#ff000000")
|
||||||
|
change_color("bottom_nav_indicator_active", "#ffffffff")
|
||||||
|
change_color(
|
||||||
|
"bottom_nav_indicator_icon_checked", main_color[0] + "ff" + main_color[1:]
|
||||||
|
)
|
||||||
|
change_color(
|
||||||
|
"bottom_nav_indicator_label_checked", main_color[0] + "ff" + main_color[1:]
|
||||||
|
)
|
||||||
|
|
||||||
|
return True
|
||||||
|
|||||||
@@ -0,0 +1,107 @@
|
|||||||
|
"""Меняет местами кнопки лайка и дизлайка у коментария и иконки
|
||||||
|
|
||||||
|
"comment_vote": {
|
||||||
|
"enabled": true,
|
||||||
|
"replace": true,
|
||||||
|
"custom_icons": true,
|
||||||
|
"icons_size": "14.0dip"
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
|
||||||
|
__author__ = "wowlikon <wowlikon@gmail.com>"
|
||||||
|
__version__ = "1.0.0"
|
||||||
|
import os
|
||||||
|
import shutil
|
||||||
|
from typing import Any, Dict
|
||||||
|
|
||||||
|
from lxml import etree
|
||||||
|
from pydantic import Field
|
||||||
|
from tqdm import tqdm
|
||||||
|
|
||||||
|
from utils.config import PatchTemplate
|
||||||
|
|
||||||
|
|
||||||
|
class Patch(PatchTemplate):
|
||||||
|
priority: int = Field(frozen=True, exclude=True, default=0)
|
||||||
|
replace: bool = Field(True, description="Менять местами лайк/дизлайк")
|
||||||
|
custom_icons: bool = Field(True, description="Кастомные иконки")
|
||||||
|
icon_size: str = Field("18.0dip", description="Размер иконки")
|
||||||
|
|
||||||
|
def apply(self, base: Dict[str, Any]) -> bool:
|
||||||
|
file_path = "./decompiled/res/layout/item_comment.xml"
|
||||||
|
parser = etree.XMLParser(remove_blank_text=True)
|
||||||
|
tree = etree.parse(file_path, parser)
|
||||||
|
root = tree.getroot()
|
||||||
|
|
||||||
|
tqdm.write("Меняем размер иконок лайка и дизлайка...")
|
||||||
|
for icon in root.xpath(
|
||||||
|
".//*[@android:id='@id/votePlusInactive']//ImageView | "
|
||||||
|
".//*[@android:id='@id/votePlusActive']//ImageView | "
|
||||||
|
".//*[@android:id='@id/voteMinusInactive']//ImageView | "
|
||||||
|
".//*[@android:id='@id/voteMinusActive']//ImageView",
|
||||||
|
namespaces=base["xml_ns"],
|
||||||
|
):
|
||||||
|
icon.set(f"{{{base['xml_ns']['android']}}}layout_width", self.icon_size)
|
||||||
|
# icon.set(f"{{{base['xml_ns']['android']}}}layout_height", self.icon_size)
|
||||||
|
|
||||||
|
if self.replace:
|
||||||
|
tqdm.write("Меняем местами лайк и дизлайк комментария...")
|
||||||
|
|
||||||
|
containers = root.xpath(
|
||||||
|
".//LinearLayout[.//*[@android:id='@id/voteMinus'] and .//*[@android:id='@id/votePlus']]",
|
||||||
|
namespaces=base["xml_ns"],
|
||||||
|
)
|
||||||
|
|
||||||
|
found = False
|
||||||
|
for container in containers:
|
||||||
|
children = list(container)
|
||||||
|
vote_plus = None
|
||||||
|
vote_minus = None
|
||||||
|
|
||||||
|
for ch in children:
|
||||||
|
cid = ch.get(f'{{{base["xml_ns"]["android"]}}}id')
|
||||||
|
if cid == "@id/votePlus":
|
||||||
|
vote_plus = ch
|
||||||
|
elif cid == "@id/voteMinus":
|
||||||
|
vote_minus = ch
|
||||||
|
|
||||||
|
if vote_plus is not None and vote_minus is not None:
|
||||||
|
found = True
|
||||||
|
i_plus = children.index(vote_plus)
|
||||||
|
i_minus = children.index(vote_minus)
|
||||||
|
children[i_plus], children[i_minus] = (
|
||||||
|
children[i_minus],
|
||||||
|
children[i_plus],
|
||||||
|
)
|
||||||
|
container[:] = children
|
||||||
|
tqdm.write("Кнопки лайк и дизлайк поменялись местами.")
|
||||||
|
break
|
||||||
|
|
||||||
|
if not found:
|
||||||
|
tqdm.write(
|
||||||
|
"Не удалось найти оба узла votePlus/voteMinus даже в общих LinearLayout."
|
||||||
|
)
|
||||||
|
|
||||||
|
if self.custom_icons:
|
||||||
|
tqdm.write("Заменяем иконки лайка и дизлайка на кастомные...")
|
||||||
|
for suffix in ["up", "up_40", "down", "down_40"]:
|
||||||
|
shutil.copy(
|
||||||
|
f"./resources/ic_chevron_{suffix}.xml",
|
||||||
|
f"./decompiled/res/drawable/ic_chevron_{suffix}.xml",
|
||||||
|
)
|
||||||
|
|
||||||
|
for inactive in root.xpath(
|
||||||
|
".//*[@android:id='@id/votePlusInactive'] | .//*[@android:id='@id/voteMinusInactive']",
|
||||||
|
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', "")
|
||||||
|
if src.startswith("@drawable/") and not src.endswith("_40"):
|
||||||
|
img.set(f'{{{base["xml_ns"]["android"]}}}src', src + "_40")
|
||||||
|
|
||||||
|
# Сохраняем
|
||||||
|
tree.write(file_path, encoding="utf-8", xml_declaration=True, pretty_print=True)
|
||||||
|
|
||||||
|
return True
|
||||||
+296
-272
@@ -1,19 +1,18 @@
|
|||||||
"""
|
"""Удаляет ненужное и сжимает ресурсы что-бы уменьшить размер АПК
|
||||||
Удаляет ненужное и сжимает ресурсы что-бы уменьшить размер АПК
|
|
||||||
|
|
||||||
Эффективность на проверена на версии 9.0 Beta 7
|
Эффективность на проверена на версии 9.0 Beta 7
|
||||||
разница = оригинальный размер апк - патченный размер апк, не учитывая другие патчи
|
разница = оригинальный размер апк - патченный размер апк, не учитывая другие патчи
|
||||||
|
|
||||||
| Настройка | Размер файла | Разница | % |
|
| Настройка | Размер файла | Разница | % |
|
||||||
| :----------- | :-------------------: | :-----------------: | :-: |
|
| :--------------: | :-------------------: | :-----------------: | :-: |
|
||||||
| None | 17092 bytes - 17.1 MB | - | - |
|
| Ничего | 17092 bytes - 17.1 MB | - | - |
|
||||||
| Compress PNG | 17072 bytes - 17.1 MB | 20 bytes - 0.0 MB | 0.11% |
|
| Сжатие PNG | 17072 bytes - 17.1 MB | 20 bytes - 0.0 MB | 0.11% |
|
||||||
| Remove files | 17020 bytes - 17.0 MB | 72 bytes - 0.1 MB | 0.42% |
|
| Удалить unknown | 17020 bytes - 17.0 MB | 72 bytes - 0.1 MB | 0.42% |
|
||||||
| Remove draws | 16940 bytes - 16.9 MB | 152 bytes - 0.2 MB | 0.89% |
|
| Удалить draws | 16940 bytes - 16.9 MB | 152 bytes - 0.2 MB | 0.89% |
|
||||||
| Remove lines | 16444 bytes - 16.4 MB | 648 bytes - 0.7 MB | 3.79% |
|
| Удалить lines | 16444 bytes - 16.4 MB | 648 bytes - 0.7 MB | 3.79% |
|
||||||
| Remove ai vo | 15812 bytes - 15.8 MB | 1280 bytes - 1.3 MB | 7.49% |
|
| Удалить ai voice | 15812 bytes - 15.8 MB | 1280 bytes - 1.3 MB | 7.49% |
|
||||||
| Remove langs | 15764 bytes - 15.7 MB | 1328 bytes - 1.3 MB | 7.76% |
|
| Удалить языки | 15764 bytes - 15.7 MB | 1328 bytes - 1.3 MB | 7.76% |
|
||||||
| Все включены | 13592 bytes - 13.6 MB | 3500 bytes - 4.8 MB | 20.5% |
|
| Все включены | 13592 bytes - 13.6 MB | 3500 bytes - 4.8 MB | 20.5% |
|
||||||
|
|
||||||
"compress": {
|
"compress": {
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
@@ -27,283 +26,308 @@
|
|||||||
}
|
}
|
||||||
"""
|
"""
|
||||||
|
|
||||||
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 typing import Dict, List, Any
|
|
||||||
from pydantic import Field
|
|
||||||
|
|
||||||
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)
|
||||||
path = "./decompiled/unknown"
|
remove_language_files: bool = Field(
|
||||||
items = os.listdir(path)
|
True, description="Удаляет все языки кроме русского и английского"
|
||||||
for item in items:
|
)
|
||||||
item_path = f"{path}/{item}"
|
remove_AI_voiceover: bool = Field(
|
||||||
if os.path.isfile(item_path):
|
True, description="Заменяет ИИ озвучку маскота аниксы пустыми mp3 файлами"
|
||||||
os.remove(item_path)
|
)
|
||||||
if base.get("verbose", False):
|
remove_debug_lines: bool = Field(
|
||||||
tqdm.write(f"Удалён файл: {item_path}")
|
False,
|
||||||
elif os.path.isdir(item_path):
|
description="Удаляет строки `.line n` из smali файлов использованные для дебага",
|
||||||
if item not in config.remove_unknown_files_keep_dirs:
|
)
|
||||||
shutil.rmtree(item_path)
|
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"
|
||||||
|
items = os.listdir(path)
|
||||||
|
for item in items:
|
||||||
|
item_path = f"{path}/{item}"
|
||||||
|
if os.path.isfile(item_path):
|
||||||
|
os.remove(item_path)
|
||||||
if base.get("verbose", False):
|
if base.get("verbose", False):
|
||||||
tqdm.write(f"Удалёна директория: {item_path}")
|
tqdm.write(f"Удалён файл: {item_path}")
|
||||||
return True
|
elif os.path.isdir(item_path):
|
||||||
|
if item not in self.remove_unknown_files_keep_dirs:
|
||||||
|
shutil.rmtree(item_path)
|
||||||
def remove_debug_lines(config: Dict[str, Any]):
|
if base.get("verbose", False):
|
||||||
for root, _, files in os.walk("./decompiled"):
|
tqdm.write(f"Удалёна директория: {item_path}")
|
||||||
for filename in files:
|
|
||||||
file_path = os.path.join(root, filename)
|
|
||||||
if os.path.isfile(file_path) and filename.endswith(".smali"):
|
|
||||||
file_content = get_smali_lines(file_path)
|
|
||||||
new_content = []
|
|
||||||
for line in file_content:
|
|
||||||
if line.find(".line") >= 0:
|
|
||||||
continue
|
|
||||||
new_content.append(line)
|
|
||||||
save_smali_lines(file_path, new_content)
|
|
||||||
if config.get("verbose", False):
|
|
||||||
tqdm.write(f"Удалены дебаг линии из: {file_path}")
|
|
||||||
return True
|
|
||||||
|
|
||||||
|
|
||||||
def compress_png(config: Dict[str, Any], png_path: str):
|
|
||||||
try:
|
|
||||||
assert subprocess.run(
|
|
||||||
[
|
|
||||||
"pngquant",
|
|
||||||
"--force",
|
|
||||||
"--ext", ".png",
|
|
||||||
"--quality=65-90",
|
|
||||||
png_path,
|
|
||||||
],
|
|
||||||
capture_output=True,
|
|
||||||
).returncode in [0, 99]
|
|
||||||
if config.get("verbose", False):
|
|
||||||
tqdm.write(f"Сжат файл PNG: {png_path}")
|
|
||||||
return True
|
return True
|
||||||
except subprocess.CalledProcessError as e:
|
|
||||||
tqdm.write(f"Ошибка при сжатии {png_path}: {e}")
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
def do_remove_debug_lines(self, config: Dict[str, Any]):
|
||||||
|
for root, _, files in os.walk("./decompiled"):
|
||||||
|
for filename in files:
|
||||||
|
file_path = os.path.join(root, filename)
|
||||||
|
if os.path.isfile(file_path) and filename.endswith(".smali"):
|
||||||
|
file_content = get_smali_lines(file_path)
|
||||||
|
new_content = []
|
||||||
|
for line in file_content:
|
||||||
|
if line.find(".line") >= 0:
|
||||||
|
continue
|
||||||
|
new_content.append(line)
|
||||||
|
save_smali_lines(file_path, new_content)
|
||||||
|
if config.get("verbose", False):
|
||||||
|
tqdm.write(f"Удалены дебаг линии из: {file_path}")
|
||||||
|
return True
|
||||||
|
|
||||||
|
def compress_png(self, config: Dict[str, Any], png_path: str):
|
||||||
|
try:
|
||||||
|
assert subprocess.run(
|
||||||
|
[
|
||||||
|
"pngquant",
|
||||||
|
"--force",
|
||||||
|
"--ext",
|
||||||
|
".png",
|
||||||
|
"--quality=65-90",
|
||||||
|
png_path,
|
||||||
|
],
|
||||||
|
capture_output=True,
|
||||||
|
).returncode in [0, 99]
|
||||||
|
if config.get("verbose", False):
|
||||||
|
tqdm.write(f"Сжат файл PNG: {png_path}")
|
||||||
|
return True
|
||||||
|
except subprocess.CalledProcessError as e:
|
||||||
|
tqdm.write(f"Ошибка при сжатии {png_path}: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def do_compress_png_files(self, config: Dict[str, Any]):
|
||||||
|
compressed = []
|
||||||
|
for root, _, files in os.walk("./decompiled"):
|
||||||
|
for file in files:
|
||||||
|
if file.lower().endswith(".png"):
|
||||||
|
self.compress_png(config, f"{root}/{file}")
|
||||||
|
compressed.append(f"{root}/{file}")
|
||||||
|
return len(compressed) > 0 and any(compressed)
|
||||||
|
|
||||||
|
def do_remove_AI_voiceover(self, config: Dict[str, Any]):
|
||||||
|
blank = "./resources/blank.mp3"
|
||||||
|
path = "./decompiled/res/raw"
|
||||||
|
files = [
|
||||||
|
"reputation_1.mp3",
|
||||||
|
"reputation_2.mp3",
|
||||||
|
"reputation_3.mp3",
|
||||||
|
"sound_beta_1.mp3",
|
||||||
|
"sound_create_blog_1.mp3",
|
||||||
|
"sound_create_blog_2.mp3",
|
||||||
|
"sound_create_blog_3.mp3",
|
||||||
|
"sound_create_blog_4.mp3",
|
||||||
|
"sound_create_blog_5.mp3",
|
||||||
|
"sound_create_blog_6.mp3",
|
||||||
|
"sound_create_blog_reputation_1.mp3",
|
||||||
|
"sound_create_blog_reputation_2.mp3",
|
||||||
|
"sound_create_blog_reputation_3.mp3",
|
||||||
|
"sound_create_blog_reputation_4.mp3",
|
||||||
|
"sound_create_blog_reputation_5.mp3",
|
||||||
|
"sound_create_blog_reputation_6.mp3",
|
||||||
|
]
|
||||||
|
|
||||||
def compress_png_files(config: Dict[str, Any]):
|
|
||||||
compressed = []
|
|
||||||
for root, _, files in os.walk("./decompiled"):
|
|
||||||
for file in files:
|
for file in files:
|
||||||
if file.lower().endswith(".png"):
|
if os.path.exists(f"{path}/{file}"):
|
||||||
compress_png(config, f"{root}/{file}")
|
os.remove(f"{path}/{file}")
|
||||||
compressed.append(f"{root}/{file}")
|
shutil.copyfile(blank, f"{path}/{file}")
|
||||||
return len(compressed) > 0 and any(compressed)
|
if config.get("verbose", False):
|
||||||
|
tqdm.write(f"Файл mp3 был заменён на пустой: {path}/{file}")
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
def remove_AI_voiceover(config: Dict[str, Any]):
|
def do_remove_language_files(self, config: Dict[str, Any]):
|
||||||
blank = "./resources/blank.mp3"
|
path = "./decompiled/res"
|
||||||
path = "./decompiled/res/raw"
|
folders = [
|
||||||
files = [
|
"values-af",
|
||||||
"reputation_1.mp3",
|
"values-am",
|
||||||
"reputation_2.mp3",
|
"values-ar",
|
||||||
"reputation_3.mp3",
|
"values-as",
|
||||||
"sound_beta_1.mp3",
|
"values-az",
|
||||||
"sound_create_blog_1.mp3",
|
"values-b+es+419",
|
||||||
"sound_create_blog_2.mp3",
|
"values-b+sr+Latn",
|
||||||
"sound_create_blog_3.mp3",
|
"values-be",
|
||||||
"sound_create_blog_4.mp3",
|
"values-bg",
|
||||||
"sound_create_blog_5.mp3",
|
"values-bn",
|
||||||
"sound_create_blog_6.mp3",
|
"values-bs",
|
||||||
"sound_create_blog_reputation_1.mp3",
|
"values-ca",
|
||||||
"sound_create_blog_reputation_2.mp3",
|
"values-cs",
|
||||||
"sound_create_blog_reputation_3.mp3",
|
"values-da",
|
||||||
"sound_create_blog_reputation_4.mp3",
|
"values-de",
|
||||||
"sound_create_blog_reputation_5.mp3",
|
"values-el",
|
||||||
"sound_create_blog_reputation_6.mp3",
|
"values-en-rAU",
|
||||||
]
|
"values-en-rCA",
|
||||||
|
"values-en-rGB",
|
||||||
|
"values-en-rIN",
|
||||||
|
"values-en-rXC",
|
||||||
|
"values-es",
|
||||||
|
"values-es-rGT",
|
||||||
|
"values-es-rUS",
|
||||||
|
"values-et",
|
||||||
|
"values-eu",
|
||||||
|
"values-fa",
|
||||||
|
"values-fi",
|
||||||
|
"values-fr",
|
||||||
|
"values-fr-rCA",
|
||||||
|
"values-gl",
|
||||||
|
"values-gu",
|
||||||
|
"values-hi",
|
||||||
|
"values-hr",
|
||||||
|
"values-hu",
|
||||||
|
"values-hy",
|
||||||
|
"values-in",
|
||||||
|
"values-is",
|
||||||
|
"values-it",
|
||||||
|
"values-iw",
|
||||||
|
"values-ja",
|
||||||
|
"values-ka",
|
||||||
|
"values-kk",
|
||||||
|
"values-km",
|
||||||
|
"values-kn",
|
||||||
|
"values-ko",
|
||||||
|
"values-ky",
|
||||||
|
"values-lo",
|
||||||
|
"values-lt",
|
||||||
|
"values-lv",
|
||||||
|
"values-mk",
|
||||||
|
"values-ml",
|
||||||
|
"values-mn",
|
||||||
|
"values-mr",
|
||||||
|
"values-ms",
|
||||||
|
"values-my",
|
||||||
|
"values-nb",
|
||||||
|
"values-ne",
|
||||||
|
"values-nl",
|
||||||
|
"values-or",
|
||||||
|
"values-pa",
|
||||||
|
"values-pl",
|
||||||
|
"values-pt",
|
||||||
|
"values-pt-rBR",
|
||||||
|
"values-pt-rPT",
|
||||||
|
"values-ro",
|
||||||
|
"values-si",
|
||||||
|
"values-sk",
|
||||||
|
"values-sl",
|
||||||
|
"values-sq",
|
||||||
|
"values-sr",
|
||||||
|
"values-sv",
|
||||||
|
"values-sw",
|
||||||
|
"values-ta",
|
||||||
|
"values-te",
|
||||||
|
"values-th",
|
||||||
|
"values-tl",
|
||||||
|
"values-tr",
|
||||||
|
"values-uk",
|
||||||
|
"values-ur",
|
||||||
|
"values-uz",
|
||||||
|
"values-vi",
|
||||||
|
"values-zh",
|
||||||
|
"values-zh-rCN",
|
||||||
|
"values-zh-rHK",
|
||||||
|
"values-zh-rTW",
|
||||||
|
"values-zu",
|
||||||
|
"values-watch",
|
||||||
|
]
|
||||||
|
|
||||||
for file in files:
|
for folder in folders:
|
||||||
if os.path.exists(f"{path}/{file}"):
|
if os.path.exists(f"{path}/{folder}"):
|
||||||
os.remove(f"{path}/{file}")
|
shutil.rmtree(f"{path}/{folder}")
|
||||||
shutil.copyfile(blank, f"{path}/{file}")
|
if config.get("verbose", False):
|
||||||
if config.get("verbose", False):
|
tqdm.write(f"Удалена директория: {path}/{folder}")
|
||||||
tqdm.write(f"Файл mp3 был заменён на пустой: {path}/{file}")
|
return True
|
||||||
|
|
||||||
return True
|
def do_remove_drawable_files(self, config: Dict[str, Any]):
|
||||||
|
path = "./decompiled/res"
|
||||||
|
folders = [
|
||||||
|
"drawable-en-hdpi",
|
||||||
|
"drawable-en-ldpi",
|
||||||
|
"drawable-en-mdpi",
|
||||||
|
"drawable-en-xhdpi",
|
||||||
|
"drawable-en-xxhdpi",
|
||||||
|
"drawable-en-xxxhdpi",
|
||||||
|
"drawable-ldrtl-hdpi",
|
||||||
|
"drawable-ldrtl-mdpi",
|
||||||
|
"drawable-ldrtl-xhdpi",
|
||||||
|
"drawable-ldrtl-xxhdpi",
|
||||||
|
"drawable-ldrtl-xxxhdpi",
|
||||||
|
"drawable-tr-anydpi",
|
||||||
|
"drawable-tr-hdpi",
|
||||||
|
"drawable-tr-ldpi",
|
||||||
|
"drawable-tr-mdpi",
|
||||||
|
"drawable-tr-xhdpi",
|
||||||
|
"drawable-tr-xxhdpi",
|
||||||
|
"drawable-tr-xxxhdpi",
|
||||||
|
"drawable-watch",
|
||||||
|
"layout-watch",
|
||||||
|
]
|
||||||
|
|
||||||
|
for folder in folders:
|
||||||
|
if os.path.exists(f"{path}/{folder}"):
|
||||||
|
shutil.rmtree(f"{path}/{folder}")
|
||||||
|
if config.get("verbose", False):
|
||||||
|
tqdm.write(f"Удалена директория: {path}/{folder}")
|
||||||
|
return True
|
||||||
|
|
||||||
def remove_language_files(config: Dict[str, Any]):
|
def apply(self, base: Dict[str, Any]) -> bool:
|
||||||
path = "./decompiled/res"
|
actions = [
|
||||||
folders = [
|
(
|
||||||
"values-af",
|
self.remove_unknown_files,
|
||||||
"values-am",
|
"Удаление неизвестных файлов...",
|
||||||
"values-ar",
|
self.do_remove_unknown_files,
|
||||||
"values-as",
|
),
|
||||||
"values-az",
|
(
|
||||||
"values-b+es+419",
|
self.remove_drawable_files,
|
||||||
"values-b+sr+Latn",
|
"Удаление директорий drawable-xx...",
|
||||||
"values-be",
|
self.do_remove_drawable_files,
|
||||||
"values-bg",
|
),
|
||||||
"values-bn",
|
(
|
||||||
"values-bs",
|
self.compress_png_files,
|
||||||
"values-ca",
|
"Сжатие PNG файлов...",
|
||||||
"values-cs",
|
self.do_compress_png_files,
|
||||||
"values-da",
|
),
|
||||||
"values-de",
|
(
|
||||||
"values-el",
|
self.remove_language_files,
|
||||||
"values-en-rAU",
|
"Удаление языков...",
|
||||||
"values-en-rCA",
|
self.do_remove_language_files,
|
||||||
"values-en-rGB",
|
),
|
||||||
"values-en-rIN",
|
(
|
||||||
"values-en-rXC",
|
self.remove_AI_voiceover,
|
||||||
"values-es",
|
"Удаление ИИ озвучки...",
|
||||||
"values-es-rGT",
|
self.do_remove_AI_voiceover,
|
||||||
"values-es-rUS",
|
),
|
||||||
"values-et",
|
(
|
||||||
"values-eu",
|
self.remove_debug_lines,
|
||||||
"values-fa",
|
"Удаление дебаг линий...",
|
||||||
"values-fi",
|
self.do_remove_debug_lines,
|
||||||
"values-fr",
|
),
|
||||||
"values-fr-rCA",
|
]
|
||||||
"values-gl",
|
|
||||||
"values-gu",
|
|
||||||
"values-hi",
|
|
||||||
"values-hr",
|
|
||||||
"values-hu",
|
|
||||||
"values-hy",
|
|
||||||
"values-in",
|
|
||||||
"values-is",
|
|
||||||
"values-it",
|
|
||||||
"values-iw",
|
|
||||||
"values-ja",
|
|
||||||
"values-ka",
|
|
||||||
"values-kk",
|
|
||||||
"values-km",
|
|
||||||
"values-kn",
|
|
||||||
"values-ko",
|
|
||||||
"values-ky",
|
|
||||||
"values-lo",
|
|
||||||
"values-lt",
|
|
||||||
"values-lv",
|
|
||||||
"values-mk",
|
|
||||||
"values-ml",
|
|
||||||
"values-mn",
|
|
||||||
"values-mr",
|
|
||||||
"values-ms",
|
|
||||||
"values-my",
|
|
||||||
"values-nb",
|
|
||||||
"values-ne",
|
|
||||||
"values-nl",
|
|
||||||
"values-or",
|
|
||||||
"values-pa",
|
|
||||||
"values-pl",
|
|
||||||
"values-pt",
|
|
||||||
"values-pt-rBR",
|
|
||||||
"values-pt-rPT",
|
|
||||||
"values-ro",
|
|
||||||
"values-si",
|
|
||||||
"values-sk",
|
|
||||||
"values-sl",
|
|
||||||
"values-sq",
|
|
||||||
"values-sr",
|
|
||||||
"values-sv",
|
|
||||||
"values-sw",
|
|
||||||
"values-ta",
|
|
||||||
"values-te",
|
|
||||||
"values-th",
|
|
||||||
"values-tl",
|
|
||||||
"values-tr",
|
|
||||||
"values-uk",
|
|
||||||
"values-ur",
|
|
||||||
"values-uz",
|
|
||||||
"values-vi",
|
|
||||||
"values-zh",
|
|
||||||
"values-zh-rCN",
|
|
||||||
"values-zh-rHK",
|
|
||||||
"values-zh-rTW",
|
|
||||||
"values-zu",
|
|
||||||
"values-watch",
|
|
||||||
]
|
|
||||||
|
|
||||||
for folder in folders:
|
for enabled, message, action in actions:
|
||||||
if os.path.exists(f"{path}/{folder}"):
|
if enabled:
|
||||||
shutil.rmtree(f"{path}/{folder}")
|
tqdm.write(message)
|
||||||
if config.get("verbose", False):
|
action(base)
|
||||||
tqdm.write(f"Удалена директория: {path}/{folder}")
|
|
||||||
return True
|
|
||||||
|
|
||||||
|
return True
|
||||||
def remove_drawable_files(config: Dict[str, Any]):
|
|
||||||
path = "./decompiled/res"
|
|
||||||
folders = [
|
|
||||||
"drawable-en-hdpi",
|
|
||||||
"drawable-en-ldpi",
|
|
||||||
"drawable-en-mdpi",
|
|
||||||
"drawable-en-xhdpi",
|
|
||||||
"drawable-en-xxhdpi",
|
|
||||||
"drawable-en-xxxhdpi",
|
|
||||||
"drawable-ldrtl-hdpi",
|
|
||||||
"drawable-ldrtl-mdpi",
|
|
||||||
"drawable-ldrtl-xhdpi",
|
|
||||||
"drawable-ldrtl-xxhdpi",
|
|
||||||
"drawable-ldrtl-xxxhdpi",
|
|
||||||
"drawable-tr-anydpi",
|
|
||||||
"drawable-tr-hdpi",
|
|
||||||
"drawable-tr-ldpi",
|
|
||||||
"drawable-tr-mdpi",
|
|
||||||
"drawable-tr-xhdpi",
|
|
||||||
"drawable-tr-xxhdpi",
|
|
||||||
"drawable-tr-xxxhdpi",
|
|
||||||
"drawable-watch",
|
|
||||||
"layout-watch",
|
|
||||||
]
|
|
||||||
|
|
||||||
for folder in folders:
|
|
||||||
if os.path.exists(f"{path}/{folder}"):
|
|
||||||
shutil.rmtree(f"{path}/{folder}")
|
|
||||||
if config.get("verbose", False):
|
|
||||||
tqdm.write(f"Удалена директория: {path}/{folder}")
|
|
||||||
return True
|
|
||||||
|
|
||||||
|
|
||||||
def apply(config: Config, base: Dict[str, Any]) -> bool:
|
|
||||||
if config.remove_unknown_files:
|
|
||||||
tqdm.write(f"Удаление неизвестных файлов...")
|
|
||||||
remove_unknown_files(config, 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
|
|
||||||
|
|||||||
+34
-36
@@ -1,49 +1,47 @@
|
|||||||
"""
|
"""Удаляет баннеры рекламы
|
||||||
Удаляет баннеры рекламы
|
|
||||||
|
|
||||||
"disable_ad": {
|
"disable_ad": {
|
||||||
"enabled": true
|
"enabled": true
|
||||||
}
|
}
|
||||||
"""
|
"""
|
||||||
|
|
||||||
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:
|
||||||
|
path = "./decompiled/smali_classes2/com/swiftsoft/anixartd/Prefs.smali"
|
||||||
|
replacement = [
|
||||||
|
f"\t{line}\n"
|
||||||
|
for line in textwrap.dedent(
|
||||||
|
"""\
|
||||||
|
.locals 0
|
||||||
|
const/4 p0, 0x1
|
||||||
|
return p0
|
||||||
|
"""
|
||||||
|
).splitlines()
|
||||||
|
]
|
||||||
|
|
||||||
# Patch
|
lines = get_smali_lines(path)
|
||||||
def apply(config: Config, base: Dict[str, Any]) -> bool:
|
for index, line in enumerate(lines):
|
||||||
replacement = [f'\t{line}\n' for line in textwrap.dedent("""\
|
if line.find("IS_SPONSOR") >= 0:
|
||||||
.locals 0
|
method_start = find_smali_method_start(lines, index)
|
||||||
const/4 p0, 0x1
|
method_end = find_smali_method_end(lines, index)
|
||||||
return p0
|
new_content = replace_smali_method_body(
|
||||||
""").splitlines()]
|
lines, method_start, method_end, replacement
|
||||||
|
)
|
||||||
|
|
||||||
path = "./decompiled/smali_classes2/com/swiftsoft/anixartd/Prefs.smali"
|
with open(path, "w", encoding="utf-8") as file:
|
||||||
lines = get_smali_lines(path)
|
file.writelines(new_content)
|
||||||
for index, line in enumerate(lines):
|
return True
|
||||||
if line.find("IS_SPONSOR") >= 0:
|
|
||||||
method_start = find_smali_method_start(lines, index)
|
|
||||||
method_end = find_smali_method_end(lines, index)
|
|
||||||
new_content = replace_smali_method_body(
|
|
||||||
lines, method_start, method_end, replacement
|
|
||||||
)
|
|
||||||
|
|
||||||
with open(path, "w", encoding="utf-8") as file:
|
|
||||||
file.writelines(new_content)
|
|
||||||
return True
|
|
||||||
|
|||||||
@@ -1,53 +1,56 @@
|
|||||||
"""
|
"""Удаляет баннеры бета-версии
|
||||||
Удаляет баннеры бета-версии
|
|
||||||
|
|
||||||
"disable_beta_banner": {
|
"disable_beta_banner": {
|
||||||
"enabled": true
|
"enabled": true
|
||||||
}
|
}
|
||||||
"""
|
"""
|
||||||
|
|
||||||
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)
|
||||||
attributes = [
|
|
||||||
"paddingTop",
|
|
||||||
"paddingBottom",
|
|
||||||
"paddingStart",
|
|
||||||
"paddingEnd",
|
|
||||||
"layout_width",
|
|
||||||
"layout_height",
|
|
||||||
"layout_marginTop",
|
|
||||||
"layout_marginBottom",
|
|
||||||
"layout_marginStart",
|
|
||||||
"layout_marginEnd",
|
|
||||||
]
|
|
||||||
|
|
||||||
beta_banner_xml = "./decompiled/res/layout/item_beta.xml"
|
def apply(self, base: Dict[str, Any]) -> bool:
|
||||||
if os.path.exists(beta_banner_xml):
|
beta_banner_xml = "./decompiled/res/layout/item_beta.xml"
|
||||||
parser = etree.XMLParser(remove_blank_text=True)
|
attributes = [
|
||||||
tree = etree.parse(beta_banner_xml, parser)
|
"paddingTop",
|
||||||
root = tree.getroot()
|
"paddingBottom",
|
||||||
|
"paddingStart",
|
||||||
|
"paddingEnd",
|
||||||
|
"layout_width",
|
||||||
|
"layout_height",
|
||||||
|
"layout_marginTop",
|
||||||
|
"layout_marginBottom",
|
||||||
|
"layout_marginStart",
|
||||||
|
"layout_marginEnd",
|
||||||
|
]
|
||||||
|
|
||||||
for attr in attributes:
|
if os.path.exists(beta_banner_xml):
|
||||||
if base.get("verbose", False):
|
parser = etree.XMLParser(remove_blank_text=True)
|
||||||
tqdm.write(f"set {attr} = 0.0dip")
|
tree = etree.parse(beta_banner_xml, parser)
|
||||||
root.set(f"{{{base['xml_ns']['android']}}}{attr}", "0.0dip")
|
root = tree.getroot()
|
||||||
|
|
||||||
tree.write(
|
for attr in attributes:
|
||||||
beta_banner_xml, pretty_print=True, xml_declaration=True, encoding="utf-8"
|
if base.get("verbose", False):
|
||||||
)
|
tqdm.write(f"set {attr} = 0.0dip")
|
||||||
|
root.set(f"{{{base['xml_ns']['android']}}}{attr}", "0.0dip")
|
||||||
|
|
||||||
return True
|
tree.write(
|
||||||
|
beta_banner_xml,
|
||||||
|
pretty_print=True,
|
||||||
|
xml_declaration=True,
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
|
|
||||||
|
return True
|
||||||
|
|||||||
@@ -1,34 +0,0 @@
|
|||||||
"""
|
|
||||||
Вставляет новые файлы в проект
|
|
||||||
|
|
||||||
"insert_new": {
|
|
||||||
"enabled": true
|
|
||||||
}
|
|
||||||
"""
|
|
||||||
|
|
||||||
priority = 0
|
|
||||||
|
|
||||||
# imports
|
|
||||||
import os
|
|
||||||
import shutil
|
|
||||||
from typing import Dict, Any
|
|
||||||
from utils.config import PatchConfig
|
|
||||||
from utils.public import insert_after_public
|
|
||||||
|
|
||||||
|
|
||||||
#Config
|
|
||||||
class Config(PatchConfig): ...
|
|
||||||
|
|
||||||
# Patch
|
|
||||||
def apply(config: Config, base: Dict[str, Any]) -> bool:
|
|
||||||
# Mod first launch window
|
|
||||||
shutil.copy("./resources/avatar.png", "./decompiled/assets/avatar.png")
|
|
||||||
shutil.copy(
|
|
||||||
"./resources/OpenSans-Regular.ttf",
|
|
||||||
"./decompiled/assets/OpenSans-Regular.ttf",
|
|
||||||
)
|
|
||||||
shutil.copytree(
|
|
||||||
"./resources/smali_classes4/", "./decompiled/smali_classes4/"
|
|
||||||
)
|
|
||||||
|
|
||||||
return True
|
|
||||||
+93
-90
@@ -1,5 +1,4 @@
|
|||||||
"""
|
"""Изменяет имя пакета в apk, удаляет вход по google и vk
|
||||||
Изменяет имя пакета в apk, удаляет вход по google и vk
|
|
||||||
|
|
||||||
"package_name": {
|
"package_name": {
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
@@ -7,110 +6,114 @@
|
|||||||
}
|
}
|
||||||
"""
|
"""
|
||||||
|
|
||||||
priority = -1
|
__author__ = "wowlikon <wowlikon@gmail.com>"
|
||||||
|
__version__ = "1.0.0"
|
||||||
# imports
|
|
||||||
import os
|
import os
|
||||||
|
from typing import Any, Dict
|
||||||
|
|
||||||
from lxml import etree
|
from lxml import etree
|
||||||
from tqdm import tqdm
|
|
||||||
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:
|
||||||
|
for root, dirs, files in os.walk("./decompiled"):
|
||||||
|
for filename in files:
|
||||||
|
file_path = os.path.join(root, filename)
|
||||||
|
|
||||||
def apply(config: Config, base: Dict[str, Any]) -> bool:
|
if os.path.isfile(file_path):
|
||||||
for root, dirs, files in os.walk("./decompiled"):
|
try: # Изменяем имя пакета в файлах
|
||||||
for filename in files:
|
with open(file_path, "r", encoding="utf-8") as file:
|
||||||
file_path = os.path.join(root, filename)
|
file_contents = file.read()
|
||||||
|
|
||||||
if os.path.isfile(file_path):
|
new_contents = file_contents.replace(
|
||||||
try:
|
"com.swiftsoft.anixartd", self.package_name
|
||||||
with open(file_path, "r", encoding="utf-8") as file:
|
)
|
||||||
file_contents = file.read()
|
new_contents = new_contents.replace(
|
||||||
|
"com/swiftsoft/anixartd",
|
||||||
|
self.package_name.replace(".", "/"),
|
||||||
|
).replace(
|
||||||
|
"com/swiftsoft",
|
||||||
|
"/".join(self.package_name.split(".")[:2]),
|
||||||
|
)
|
||||||
|
with open(file_path, "w", encoding="utf-8") as file:
|
||||||
|
file.write(new_contents)
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
new_contents = file_contents.replace(
|
# Изменяем названия папок
|
||||||
"com.swiftsoft.anixartd", config.package_name
|
if os.path.exists("./decompiled/smali/com/swiftsoft/anixartd"):
|
||||||
)
|
self.rename_dir(
|
||||||
new_contents = new_contents.replace(
|
"./decompiled/smali/com/swiftsoft/anixartd",
|
||||||
"com/swiftsoft/anixartd",
|
os.path.join(
|
||||||
config.package_name.replace(".", "/"),
|
"./decompiled", "smali", self.package_name.replace(".", "/")
|
||||||
)
|
),
|
||||||
with open(file_path, "w", encoding="utf-8") as file:
|
)
|
||||||
file.write(new_contents)
|
if os.path.exists("./decompiled/smali_classes2/com/swiftsoft/anixartd"):
|
||||||
except:
|
self.rename_dir(
|
||||||
pass
|
"./decompiled/smali_classes2/com/swiftsoft/anixartd",
|
||||||
|
os.path.join(
|
||||||
|
"./decompiled",
|
||||||
|
"smali_classes2",
|
||||||
|
self.package_name.replace(".", "/"),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
if os.path.exists("./decompiled/smali_classes4/com/swiftsoft"):
|
||||||
|
self.rename_dir(
|
||||||
|
"./decompiled/smali_classes4/com/swiftsoft",
|
||||||
|
os.path.join(
|
||||||
|
"./decompiled",
|
||||||
|
"smali_classes4",
|
||||||
|
"/".join(self.package_name.split(".")[:2]),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
if os.path.exists("./decompiled/smali/com/swiftsoft/anixartd"):
|
# rename_dir(
|
||||||
rename_dir(
|
# "./decompiled/smali_classes3/com/swiftsoft/anixartd",
|
||||||
"./decompiled/smali/com/swiftsoft/anixartd",
|
# os.path.join(
|
||||||
os.path.join(
|
# "./decompiled",
|
||||||
"./decompiled", "smali", config.package_name.replace(".", "/")
|
# "smali_classes3",
|
||||||
),
|
# config["new_package_name"].replace(".", "/"),
|
||||||
)
|
# ),
|
||||||
if os.path.exists("./decompiled/smali_classes2/com/swiftsoft/anixartd"):
|
# )
|
||||||
rename_dir(
|
|
||||||
"./decompiled/smali_classes2/com/swiftsoft/anixartd",
|
|
||||||
os.path.join(
|
|
||||||
"./decompiled",
|
|
||||||
"smali_classes2",
|
|
||||||
config.package_name.replace(".", "/"),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
if os.path.exists("./decompiled/smali_classes4/com/swiftsoft"):
|
|
||||||
rename_dir(
|
|
||||||
"./decompiled/smali_classes4/com/swiftsoft",
|
|
||||||
os.path.join(
|
|
||||||
"./decompiled",
|
|
||||||
"smali_classes4",
|
|
||||||
"/".join(config.package_name.split(".")[:-1]),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
# rename_dir(
|
# Замена названия пакета для smali_classes4
|
||||||
# "./decompiled/smali_classes3/com/swiftsoft/anixartd",
|
for root, dirs, files in os.walk("./decompiled/smali_classes4/"):
|
||||||
# os.path.join(
|
for filename in files:
|
||||||
# "./decompiled",
|
file_path = os.path.join(root, filename)
|
||||||
# "smali_classes3",
|
|
||||||
# config["new_package_name"].replace(".", "/"),
|
|
||||||
# ),
|
|
||||||
# )
|
|
||||||
|
|
||||||
for root, dirs, files in os.walk("./decompiled/smali_classes4/"):
|
if os.path.isfile(file_path):
|
||||||
for filename in files:
|
try:
|
||||||
file_path = os.path.join(root, filename)
|
with open(file_path, "r", encoding="utf-8") as file:
|
||||||
|
file_contents = file.read()
|
||||||
|
|
||||||
if os.path.isfile(file_path):
|
new_contents = file_contents.replace(
|
||||||
try:
|
"com/swiftsoft",
|
||||||
with open(file_path, "r", encoding="utf-8") as file:
|
"/".join(self.package_name.split(".")[:-1]),
|
||||||
file_contents = file.read()
|
)
|
||||||
|
with open(file_path, "w", encoding="utf-8") as file:
|
||||||
|
file.write(new_contents)
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
new_contents = file_contents.replace(
|
# Скрытие входа по Google и VK (НЕ РАБОТАЮТ В МОДАХ)
|
||||||
"com/swiftsoft",
|
file_path = "./decompiled/res/layout/fragment_sign_in.xml"
|
||||||
"/".join(config.package_name.split(".")[:-1]),
|
parser = etree.XMLParser(remove_blank_text=True)
|
||||||
)
|
tree = etree.parse(file_path, parser)
|
||||||
with open(file_path, "w", encoding="utf-8") as file:
|
root = tree.getroot()
|
||||||
file.write(new_contents)
|
|
||||||
except:
|
|
||||||
pass
|
|
||||||
|
|
||||||
file_path = "./decompiled/res/layout/fragment_sign_in.xml"
|
last_linear = root.xpath("//LinearLayout/LinearLayout[4]")[0]
|
||||||
parser = etree.XMLParser(remove_blank_text=True)
|
last_linear.set(f"{{{base['xml_ns']['android']}}}visibility", "gone")
|
||||||
tree = etree.parse(file_path, parser)
|
|
||||||
root = tree.getroot()
|
|
||||||
|
|
||||||
last_linear = root.xpath("//LinearLayout/LinearLayout[4]")[0]
|
tree.write(file_path, pretty_print=True, xml_declaration=True, encoding="utf-8")
|
||||||
last_linear.set(f"{{{base['xml_ns']['android']}}}visibility", "gone")
|
|
||||||
|
|
||||||
tree.write(file_path, pretty_print=True, xml_declaration=True, encoding="utf-8")
|
return True
|
||||||
|
|
||||||
return True
|
|
||||||
|
|||||||
+42
-34
@@ -7,53 +7,61 @@
|
|||||||
}
|
}
|
||||||
"""
|
"""
|
||||||
|
|
||||||
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
|
||||||
|
|
||||||
#Config
|
|
||||||
class Config(PatchConfig):
|
|
||||||
items: List[str] = Field(["home", "discover", "feed", "bookmarks", "profile"], description="Список элементов в панели навигации")
|
|
||||||
|
|
||||||
# Patch
|
class Patch(PatchTemplate):
|
||||||
def apply(config: Config, base: Dict[str, Any]) -> bool:
|
priority: int = Field(frozen=True, exclude=True, default=0)
|
||||||
file_path = "./decompiled/res/menu/bottom.xml"
|
items: List[str] = Field(
|
||||||
|
["home", "discover", "feed", "bookmarks", "profile"],
|
||||||
|
description="Список элементов в панели навигации",
|
||||||
|
)
|
||||||
|
|
||||||
parser = etree.XMLParser(remove_blank_text=True)
|
def apply(self, base: Dict[str, Any]) -> bool:
|
||||||
tree = etree.parse(file_path, parser)
|
file_path = "./decompiled/res/menu/bottom.xml"
|
||||||
root = tree.getroot()
|
|
||||||
|
|
||||||
items = root.findall("item", namespaces=base['xml_ns'])
|
parser = etree.XMLParser(remove_blank_text=True)
|
||||||
|
tree = etree.parse(file_path, parser)
|
||||||
|
root = tree.getroot()
|
||||||
|
|
||||||
def get_id_suffix(item):
|
# Получение элементов панели навигации
|
||||||
full_id = item.get(f"{{{base['xml_ns']['android']}}}id")
|
items = root.findall("item", namespaces=base["xml_ns"])
|
||||||
return full_id.split("tab_")[-1] if full_id else None
|
|
||||||
|
|
||||||
items_by_id = {get_id_suffix(item): item for item in items}
|
def get_id_suffix(item):
|
||||||
existing_order = [get_id_suffix(item) for item in items]
|
full_id = item.get(f"{{{base['xml_ns']['android']}}}id")
|
||||||
|
return full_id.split("tab_")[-1] if full_id else None
|
||||||
|
|
||||||
ordered_items = []
|
items_by_id = {get_id_suffix(item): item for item in items}
|
||||||
for key in config.items:
|
existing_order = [get_id_suffix(item) for item in items]
|
||||||
if key in items_by_id:
|
|
||||||
ordered_items.append(items_by_id[key])
|
|
||||||
|
|
||||||
extra = [i for i in items if get_id_suffix(i) not in config.items]
|
# Размещение в новом порядке
|
||||||
if extra:
|
ordered_items = []
|
||||||
tqdm.write("⚠Найдены лишние элементы: " + str([get_id_suffix(i) for i in extra]))
|
for key in self.items:
|
||||||
ordered_items.extend(extra)
|
if key in items_by_id:
|
||||||
|
ordered_items.append(items_by_id[key])
|
||||||
|
|
||||||
for i in root.findall("item", namespaces=base['xml_ns']):
|
# Если есть не указанные в конфиге они помещаются в конец списка
|
||||||
root.remove(i)
|
extra = [i for i in items if get_id_suffix(i) not in self.items]
|
||||||
|
if extra:
|
||||||
|
tqdm.write(
|
||||||
|
"⚠Найдены лишние элементы: " + str([get_id_suffix(i) for i in extra])
|
||||||
|
)
|
||||||
|
ordered_items.extend(extra)
|
||||||
|
|
||||||
for item in ordered_items:
|
for i in root.findall("item", namespaces=base["xml_ns"]):
|
||||||
root.append(item)
|
root.remove(i)
|
||||||
|
|
||||||
tree.write(file_path, pretty_print=True, xml_declaration=True, encoding="utf-8")
|
for item in ordered_items:
|
||||||
|
root.append(item)
|
||||||
|
|
||||||
return True
|
tree.write(file_path, pretty_print=True, xml_declaration=True, encoding="utf-8")
|
||||||
|
|
||||||
|
return True
|
||||||
|
|||||||
@@ -0,0 +1,45 @@
|
|||||||
|
"""Делает текст в описании аниме копируемым
|
||||||
|
|
||||||
|
"selectable_text": {
|
||||||
|
"enabled": true
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
|
||||||
|
__author__ = "wowlikon <wowlikon@gmail.com>"
|
||||||
|
__version__ = "1.0.0"
|
||||||
|
from typing import Any, Dict
|
||||||
|
|
||||||
|
from lxml import etree
|
||||||
|
from pydantic import Field
|
||||||
|
|
||||||
|
from utils.config import PatchTemplate
|
||||||
|
|
||||||
|
|
||||||
|
class Patch(PatchTemplate):
|
||||||
|
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"
|
||||||
|
|
||||||
|
parser = etree.XMLParser(remove_blank_text=True)
|
||||||
|
tree = etree.parse(file_path, parser)
|
||||||
|
root = tree.getroot()
|
||||||
|
|
||||||
|
# Список тегов, к которым нужно добавить атрибут
|
||||||
|
tags = ["TextView", "at.blogc.android.views.ExpandableTextView"]
|
||||||
|
|
||||||
|
for tag in tags:
|
||||||
|
for element in root.findall(f".//{tag}", namespaces=base["xml_ns"]):
|
||||||
|
# Проверяем, нет ли уже атрибута
|
||||||
|
if (
|
||||||
|
f"{{{base['xml_ns']['android']}}}textIsSelectable"
|
||||||
|
not in element.attrib
|
||||||
|
):
|
||||||
|
element.set(
|
||||||
|
f"{{{base['xml_ns']['android']}}}textIsSelectable", "true"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Сохраняем
|
||||||
|
tree.write(file_path, encoding="utf-8", xml_declaration=True, pretty_print=True)
|
||||||
|
|
||||||
|
return True
|
||||||
+105
-96
@@ -1,5 +1,4 @@
|
|||||||
"""
|
"""Добавляет в настройки ссылки и добвляет текст к версии приложения
|
||||||
Добавляет в настройки ссылки и добвляет текст к версии приложения
|
|
||||||
|
|
||||||
"settings_urls": {
|
"settings_urls": {
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
@@ -21,118 +20,128 @@
|
|||||||
}
|
}
|
||||||
"""
|
"""
|
||||||
|
|
||||||
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": "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",
|
"title": "Мы в Telegram",
|
||||||
"description": "Подпишитесь на канал, чтобы быть в курсе последних новостей.",
|
"description": "Подпишитесь на канал, чтобы быть в курсе последних новостей.",
|
||||||
"url": "https://t.me/http_teapod",
|
"url": "https://t.me/http_teapod",
|
||||||
"icon": "@drawable/ic_custom_telegram",
|
"icon": "@drawable/ic_custom_telegram",
|
||||||
"icon_space_reserved": "false"
|
"icon_space_reserved": "false",
|
||||||
}
|
},
|
||||||
],
|
],
|
||||||
"Прочее": [
|
"Прочее": [
|
||||||
{
|
{
|
||||||
"title": "Помочь проекту",
|
"title": "Помочь проекту",
|
||||||
"description": "Вы можете помочь нам в разработке мода, написании кода или тестировании.",
|
"description": "Вы можете помочь нам с идеями, написанием кода или тестированием.",
|
||||||
"url": "https://git.wowlikon.tech/anixart-mod",
|
"url": "https://git.wowlikon.tech/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")
|
|
||||||
|
|
||||||
for item in items:
|
for item in items:
|
||||||
pref = etree.SubElement(cat, "Preference", nsmap=ns)
|
pref = etree.SubElement(cat, "Preference", nsmap=ns)
|
||||||
pref.set(f"{{{ns['android']}}}title", item["title"])
|
pref.set(f"{{{ns['android']}}}title", item["title"])
|
||||||
pref.set(f"{{{ns['android']}}}summary", item["description"])
|
pref.set(f"{{{ns['android']}}}summary", item["description"])
|
||||||
pref.set(f"{{{ns['app']}}}icon", item["icon"])
|
pref.set(f"{{{ns['app']}}}icon", item["icon"])
|
||||||
pref.set(f"{{{ns['app']}}}iconSpaceReserved", item["icon_space_reserved"])
|
pref.set(f"{{{ns['app']}}}iconSpaceReserved", item["icon_space_reserved"])
|
||||||
|
|
||||||
intent = etree.SubElement(pref, "intent", nsmap=ns)
|
intent = etree.SubElement(pref, "intent", nsmap=ns)
|
||||||
intent.set(f"{{{ns['android']}}}action", "android.intent.action.VIEW")
|
intent.set(f"{{{ns['android']}}}action", "android.intent.action.VIEW")
|
||||||
intent.set(f"{{{ns['android']}}}data", item["url"])
|
intent.set(f"{{{ns['android']}}}data", item["url"])
|
||||||
intent.set(f"{{{ns['app']}}}iconSpaceReserved", item["icon_space_reserved"])
|
intent.set(f"{{{ns['app']}}}iconSpaceReserved", item["icon_space_reserved"])
|
||||||
|
|
||||||
return cat
|
return cat
|
||||||
|
|
||||||
def apply(config: Config, base: Dict[str, Any]) -> bool:
|
def apply(self, base: Dict[str, Any]) -> bool:
|
||||||
shutil.copy(
|
# Добавление кастомных иконок
|
||||||
"./resources/ic_custom_crown.xml",
|
shutil.copy(
|
||||||
"./decompiled/res/drawable/ic_custom_crown.xml",
|
"./resources/ic_custom_crown.xml",
|
||||||
)
|
"./decompiled/res/drawable/ic_custom_crown.xml",
|
||||||
insert_after_public("warning_error_counter_background", "ic_custom_crown")
|
)
|
||||||
|
insert_after_public("warning_error_counter_background", "ic_custom_crown")
|
||||||
|
|
||||||
shutil.copy(
|
shutil.copy(
|
||||||
"./resources/ic_custom_telegram.xml",
|
"./resources/ic_custom_telegram.xml",
|
||||||
"./decompiled/res/drawable/ic_custom_telegram.xml",
|
"./decompiled/res/drawable/ic_custom_telegram.xml",
|
||||||
)
|
)
|
||||||
insert_after_public("warning_error_counter_background", "ic_custom_telegram")
|
insert_after_public("warning_error_counter_background", "ic_custom_telegram")
|
||||||
|
|
||||||
file_path = "./decompiled/res/xml/preference_main.xml"
|
file_path = "./decompiled/res/xml/preference_main.xml"
|
||||||
parser = etree.XMLParser(remove_blank_text=True)
|
parser = etree.XMLParser(remove_blank_text=True)
|
||||||
tree = etree.parse(file_path, parser)
|
tree = etree.parse(file_path, parser)
|
||||||
root = tree.getroot()
|
root = tree.getroot()
|
||||||
|
|
||||||
# Insert new PreferenceCategory before the last element
|
# Вставка новых пунктов перед последним
|
||||||
last = root[-1] # last element
|
pos = root.index(root[-1])
|
||||||
pos = root.index(last)
|
for section, items in self.menu.items():
|
||||||
for section, items in config.menu.items():
|
root.insert(pos, self.make_category(base["xml_ns"], section, items))
|
||||||
root.insert(pos, make_category(base["xml_ns"], section, items))
|
pos += 1
|
||||||
pos += 1
|
|
||||||
|
|
||||||
# Save back
|
# Сохранение
|
||||||
tree.write(file_path, pretty_print=True, xml_declaration=True, encoding="utf-8")
|
tree.write(file_path, pretty_print=True, xml_declaration=True, encoding="utf-8")
|
||||||
|
|
||||||
filepaths = [
|
# Добавление суффикса версии
|
||||||
"./decompiled/smali_classes2/com/swiftsoft/anixartd/ui/activity/UpdateActivity.smali",
|
filepaths = [
|
||||||
"./decompiled/smali_classes2/com/swiftsoft/anixartd/ui/fragment/main/preference/MainPreferenceFragment.smali",
|
"./decompiled/smali_classes2/com/swiftsoft/anixartd/ui/activity/UpdateActivity.smali",
|
||||||
]
|
"./decompiled/smali_classes2/com/swiftsoft/anixartd/ui/fragment/main/preference/MainPreferenceFragment.smali",
|
||||||
for filepath in filepaths:
|
]
|
||||||
content = ""
|
for filepath in filepaths:
|
||||||
with open(filepath, "r", encoding="utf-8") as file:
|
content = ""
|
||||||
for line in file.readlines():
|
with open(filepath, "r", encoding="utf-8") as file:
|
||||||
if '"\u0412\u0435\u0440\u0441\u0438\u044f'.encode("unicode_escape").decode() in line:
|
for line in file.readlines():
|
||||||
content += line[:line.rindex('"')] + config.version + line[line.rindex('"'):]
|
if (
|
||||||
else:
|
'"\u0412\u0435\u0440\u0441\u0438\u044f'.encode(
|
||||||
content += line
|
"unicode_escape"
|
||||||
with open(filepath, "w", encoding="utf-8") as file:
|
).decode()
|
||||||
file.write(content)
|
in line
|
||||||
return True
|
):
|
||||||
|
content += (
|
||||||
|
line[: line.rindex('"')]
|
||||||
|
+ self.version
|
||||||
|
+ line[line.rindex('"') :]
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
content += line
|
||||||
|
with open(filepath, "w", encoding="utf-8") as file:
|
||||||
|
file.write(content)
|
||||||
|
return True
|
||||||
|
|||||||
@@ -0,0 +1,53 @@
|
|||||||
|
"""Изменяет формат "поделиться"
|
||||||
|
|
||||||
|
"selectable_text": {
|
||||||
|
"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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
|
||||||
|
__author__ = "wowlikon <wowlikon@gmail.com>"
|
||||||
|
__version__ = "1.0.0"
|
||||||
|
from typing import Any, Dict
|
||||||
|
|
||||||
|
from lxml import etree
|
||||||
|
from pydantic import Field
|
||||||
|
|
||||||
|
from utils.config import PatchTemplate
|
||||||
|
|
||||||
|
DEFAULT_FORMATS = {
|
||||||
|
"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",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class Patch(PatchTemplate):
|
||||||
|
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"
|
||||||
|
|
||||||
|
parser = etree.XMLParser(remove_blank_text=True)
|
||||||
|
tree = etree.parse(file_path, parser)
|
||||||
|
root = tree.getroot()
|
||||||
|
|
||||||
|
# Обновляем значения
|
||||||
|
for string in root.findall("string"):
|
||||||
|
name = string.get("name")
|
||||||
|
if name in self.format:
|
||||||
|
string.text = self.format[name]
|
||||||
|
|
||||||
|
# Сохраняем обратно
|
||||||
|
tree.write(file_path, encoding="utf-8", xml_declaration=True, pretty_print=True)
|
||||||
|
|
||||||
|
return True
|
||||||
@@ -1,5 +1,4 @@
|
|||||||
"""
|
"""Добавляет пользовательские скорости воспроизведения видео
|
||||||
Добавляет пользовательские скорости воспроизведения видео
|
|
||||||
|
|
||||||
"custom_speed": {
|
"custom_speed": {
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
@@ -7,33 +6,30 @@
|
|||||||
}
|
}
|
||||||
"""
|
"""
|
||||||
|
|
||||||
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)
|
||||||
assert float_to_hex(1.5) == "0x3fc00000"
|
speeds: List[float] = Field(
|
||||||
|
[9.0], description="Список пользовательских скоростей воспроизведения"
|
||||||
|
)
|
||||||
|
|
||||||
last = "speed75"
|
def apply(self, base: Dict[str, Any]) -> bool:
|
||||||
for speed in config.speeds:
|
assert float_to_hex(1.5) == "0x3fc00000"
|
||||||
insert_after_public(last, f"speed{int(float(speed)*10)}")
|
|
||||||
insert_after_id(last, f"speed{int(float(speed)*10)}")
|
|
||||||
last = f"speed{int(float(speed)*10)}"
|
|
||||||
|
|
||||||
return False
|
last = "speed75"
|
||||||
|
for speed in self.speeds:
|
||||||
|
insert_after_public(last, f"speed{int(float(speed)*10)}")
|
||||||
|
insert_after_id(last, f"speed{int(float(speed)*10)}")
|
||||||
|
last = f"speed{int(float(speed)*10)}"
|
||||||
|
|
||||||
|
return False
|
||||||
|
|||||||
+22
-18
@@ -1,11 +1,10 @@
|
|||||||
"""
|
"""Шаблон патча
|
||||||
Шаблон патча
|
|
||||||
|
|
||||||
Здесь вы можете добавить описание патча, его назначение и другие детали.
|
Здесь вы можете добавить описание патча, его назначение и другие детали.
|
||||||
|
|
||||||
Каждый патч должен независеть от других патчей и проверять себя при применении. Он не должен вернуть True, если есть проблемы.
|
Каждый патч должен независеть от других патчей и проверять себя при применении. Он не должен вернуть True, если есть проблемы.
|
||||||
На данный момент каждый патч должен иметь функцию `apply`, которая принимает на вход конфигурацию и возвращает True или False.
|
На данный момент каждый патч должен иметь функцию `apply`, которая принимает на вход конфигурацию и возвращает True или False.
|
||||||
И модель `Config`, которая наследуется от `PatchConfig` (поле `enabled` добавлять не нужно).
|
И модель `Config`, которая наследуется от `PatchTemplate` (поле `enabled` добавлять не нужно).
|
||||||
Это позволяет упростить конфигурирование и проверять типы данных, и делать значения по умолчанию.
|
Это позволяет упростить конфигурирование и проверять типы данных, и делать значения по умолчанию.
|
||||||
При успешном применении патча, функция apply должна вернуть True, иначе False.
|
При успешном применении патча, функция apply должна вернуть True, иначе False.
|
||||||
Ошибка будет интерпретирована как False. С выводом ошибки в консоль.
|
Ошибка будет интерпретирована как False. С выводом ошибки в консоль.
|
||||||
@@ -26,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
|
||||||
tqdm.write("Вывод информации через tqdm, чтобы не мешать прогресс-бару")
|
priority: int = Field(
|
||||||
tqdm.write("Пример включен" if config.example else "Пример отключен")
|
frozen=True, exclude=True, default=0
|
||||||
if base["verbose"]:
|
) # Приоритет патча, чем выше, тем раньше он будет применен
|
||||||
tqdm.write("Для вывода подробной и отладочной информации используйте флаг --verbose")
|
tqdm.write("Вывод информации через tqdm, чтобы не мешать прогресс-бару")
|
||||||
return True
|
tqdm.write("Пример включен" if self.example else "Пример отключен")
|
||||||
|
if base["verbose"]:
|
||||||
|
tqdm.write(
|
||||||
|
"Для вывода подробной и отладочной информации используйте флаг --verbose"
|
||||||
|
)
|
||||||
|
return True
|
||||||
|
|||||||
@@ -0,0 +1,55 @@
|
|||||||
|
"""Добавляет всплывающее окно при первом входе
|
||||||
|
|
||||||
|
"welcome": {
|
||||||
|
"enabled": true,
|
||||||
|
"title": "Anixarty",
|
||||||
|
"description": "Описание",
|
||||||
|
"link_text": "МЫ В TELEGRAM",
|
||||||
|
"link_url": "https://t.me/http_teapod",
|
||||||
|
"skip_text": "Пропустить",
|
||||||
|
"title_bg_color": "#FFFFFF"
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
|
||||||
|
__author__ = "wowlikon <wowlikon@gmail.com>"
|
||||||
|
__version__ = "1.0.0"
|
||||||
|
import shutil
|
||||||
|
from typing import Any, Dict
|
||||||
|
|
||||||
|
from pydantic import Field
|
||||||
|
|
||||||
|
from utils.config import PatchTemplate
|
||||||
|
from utils.smali_parser import (find_and_replace_smali_line, get_smali_lines,
|
||||||
|
save_smali_lines)
|
||||||
|
|
||||||
|
|
||||||
|
class Patch(PatchTemplate):
|
||||||
|
priority: int = Field(frozen=True, exclude=True, default=0)
|
||||||
|
title: str = Field("Anixarty", description="Заголовок")
|
||||||
|
description: str = Field("Описание", description="Описание")
|
||||||
|
link_text: str = Field("МЫ В TELEGRAM", description="Текст ссылки")
|
||||||
|
link_url: str = Field("https://t.me/http_teapod", description="Ссылка")
|
||||||
|
skip_text: str = Field("Пропустить", description="Текст кнопки пропуска")
|
||||||
|
title_bg_color: str = Field("#FFFFFF", description="Цвет фона заголовка")
|
||||||
|
|
||||||
|
def apply(self, base: Dict[str, Any]) -> bool:
|
||||||
|
file_path = "./decompiled/smali_classes2/com/swiftsoft/anixartd/ui/activity/MainActivity.smali"
|
||||||
|
# Добавление ресурсов окна первого входа
|
||||||
|
shutil.copy("./resources/avatar.png", "./decompiled/assets/avatar.png")
|
||||||
|
shutil.copy(
|
||||||
|
"./resources/OpenSans-Regular.ttf",
|
||||||
|
"./decompiled/assets/OpenSans-Regular.ttf",
|
||||||
|
)
|
||||||
|
shutil.copytree("./resources/smali_classes4/", "./decompiled/smali_classes4/")
|
||||||
|
|
||||||
|
method = "invoke-super {p0}, Lmoxy/MvpAppCompatActivity;->onResume()V"
|
||||||
|
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;",
|
||||||
|
)
|
||||||
|
save_smali_lines(file_path, lines)
|
||||||
|
|
||||||
|
return True
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="24"
|
||||||
|
android:viewportHeight="24">
|
||||||
|
<path
|
||||||
|
android:pathData="M12.59 23.26l-0.01 0-0.07 0.03-0.02 0.01-0.01-0.01-0.07-0.03q-0.02-0.01-0.03 0l0 0.01-0.02 0.43 0.01 0.02 0.01 0.02 0.1 0.07 0.02 0 0.01 0 0.1-0.07 0.01-0.02 0.01-0.02-0.02-0.42q0-0.02-0.02-0.02m0.27-0.12l-0.01 0.01-0.19 0.09-0.01 0.01 0 0.01 0.02 0.43 0 0.01 0.01 0.01 0.2 0.09q0.02 0.01 0.03-0.01l0-0.01-0.03-0.61q-0.01-0.02-0.02-0.03m-0.72 0.01a0.02 0.02 0 0 0-0.02 0l-0.01 0.02-0.03 0.61q0 0.02 0.01 0.02l0.02 0 0.2-0.09 0.01-0.01 0-0.01 0.02-0.43 0-0.01-0.01-0.01z"/>
|
||||||
|
<path
|
||||||
|
android:fillColor="#FF000000"
|
||||||
|
android:pathData="M15 16h2.4a4 4 0 0 0 3.94-4.72l-0.91-5A4 4 0 0 0 16.5 3H8v12l1.82 5.79c0.3 0.69 1.06 1.32 2.02 1.13C13.37 21.63 15 20.43 15 18.5z m-9-1a3 3 0 0 1-3-3V6a3 3 0 0 1 3-3z"/>
|
||||||
|
</vector>
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="24"
|
||||||
|
android:viewportHeight="24">
|
||||||
|
<path
|
||||||
|
android:pathData="M12.59 23.26l-0.01 0-0.07 0.03-0.02 0.01-0.01-0.01-0.07-0.03q-0.02-0.01-0.03 0l0 0.01-0.02 0.43 0.01 0.02 0.01 0.02 0.1 0.07 0.02 0 0.01 0 0.1-0.07 0.01-0.02 0.01-0.02-0.02-0.42q0-0.02-0.02-0.02m0.27-0.12l-0.01 0.01-0.19 0.09-0.01 0.01 0 0.01 0.02 0.43 0 0.01 0.01 0.01 0.2 0.09q0.02 0.01 0.03-0.01l0-0.01-0.03-0.61q-0.01-0.02-0.02-0.03m-0.72 0.01a0.02 0.02 0 0 0-0.02 0l-0.01 0.02-0.03 0.61q0 0.02 0.01 0.02l0.02 0 0.2-0.09 0.01-0.01 0-0.01 0.02-0.43 0-0.01-0.01-0.01z"/>
|
||||||
|
<path
|
||||||
|
android:fillType="evenOdd"
|
||||||
|
android:fillColor="#FF000000"
|
||||||
|
android:pathData="M16.5 3a4 4 0 0 1 3.93 3.28l0.91 5a4 4 0 0 1-3.94 4.72H15v2.5c0 1.93-1.63 3.12-3.15 3.42-0.96 0.18-1.73-0.44-2.03-1.13l-2.48-5.79H6a3 3 0 0 1-3-3v-6a3 3 0 0 1 3-3z m0 2H9v8.59a1 1 0 0 0 0.08 0.39l2.54 5.94c0.88-0.22 1.38-0.83 1.38-1.42v-2.5a2 2 0 0 1 2-2h2.4a2 2 0 0 0 1.97-2.36l-0.91-5a2 2 0 0 0-1.96-1.64M7 5H6a1 1 0 0 0-0.99 0.88L5 6v6a1 1 0 0 0 0.88 0.99l0.12 0.01h1z"/>
|
||||||
|
</vector>
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="24"
|
||||||
|
android:viewportHeight="24">
|
||||||
|
<path
|
||||||
|
android:pathData="M12.59 23.26l-0.01 0-0.07 0.03-0.02 0.01-0.01-0.01-0.07-0.03q-0.02-0.01-0.03 0l0 0.01-0.02 0.43 0.01 0.02 0.01 0.02 0.1 0.07 0.02 0 0.01 0 0.1-0.07 0.01-0.02 0.01-0.02-0.02-0.42q0-0.02-0.02-0.02m0.27-0.12l-0.01 0.01-0.19 0.09-0.01 0.01 0 0.01 0.02 0.43 0 0.01 0.01 0.01 0.2 0.09q0.02 0.01 0.03-0.01l0-0.01-0.03-0.61q-0.01-0.02-0.02-0.03m-0.72 0.01a0.02 0.02 0 0 0-0.02 0l-0.01 0.02-0.03 0.61q0 0.02 0.01 0.02l0.02 0 0.2-0.09 0.01-0.01 0-0.01 0.02-0.43 0-0.01-0.01-0.01z"/>
|
||||||
|
<path
|
||||||
|
android:fillColor="#FF000000"
|
||||||
|
android:pathData="M15 8h2.4a4 4 0 0 1 3.94 4.72l-0.91 5A4 4 0 0 1 16.5 21H8V9l1.82-5.79c0.3-0.69 1.06-1.32 2.02-1.13C13.37 2.38 15 3.57 15 5.5zM6 9a3 3 0 0 0-3 3v6a3 3 0 0 0 3 3z"/>
|
||||||
|
</vector>
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="24"
|
||||||
|
android:viewportHeight="24">
|
||||||
|
<path
|
||||||
|
android:pathData="M12.59 23.26l-0.01 0-0.07 0.03-0.02 0.01-0.01-0.01-0.07-0.03q-0.02-0.01-0.03 0l0 0.01-0.02 0.43 0.01 0.02 0.01 0.02 0.1 0.07 0.02 0 0.01 0 0.1-0.07 0.01-0.02 0.01-0.02-0.02-0.42q0-0.02-0.02-0.02m0.27-0.12l-0.01 0.01-0.19 0.09-0.01 0.01 0 0.01 0.02 0.43 0 0.01 0.01 0.01 0.2 0.09q0.02 0.01 0.03-0.01l0-0.01-0.03-0.61q-0.01-0.02-0.02-0.03m-0.72 0.01a0.02 0.02 0 0 0-0.02 0l-0.01 0.02-0.03 0.61q0 0.02 0.01 0.02l0.02 0 0.2-0.09 0.01-0.01 0-0.01 0.02-0.43 0-0.01-0.01-0.01z"/>
|
||||||
|
<path
|
||||||
|
android:fillType="evenOdd"
|
||||||
|
android:fillColor="#FF000000"
|
||||||
|
android:pathData="M9.82 3.21c0.3-0.69 1.06-1.32 2.02-1.13 1.47 0.28 3.04 1.4 3.15 3.22L15 5.5V8h2.4a4 4 0 0 1 3.97 4.52l-0.03 0.2-0.91 5a4 4 0 0 1-3.74 3.28l-0.19 0H6a3 3 0 0 1-3-2.82L3 18v-6a3 3 0 0 1 2.82-3L6 9h1.34zM7 11H6a1 1 0 0 0-1 1v6a1 1 0 0 0 1 1h1z m4.63-6.92l-2.55 5.94a1 1 0 0 0-0.07 0.26L9 10.41V19h7.5a2 2 0 0 0 1.93-1.49l0.03-0.15 0.91-5a2 2 0 0 0-1.82-2.35L17.41 10H15a2 2 0 0 1-2-1.85L13 8V5.5c0-0.55-0.43-1.12-1.21-1.37z"/>
|
||||||
|
</vector>
|
||||||
+228
@@ -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"Anixarty-v{self.safe_version}.apk"
|
||||||
|
|
||||||
|
@computed_field
|
||||||
|
@property
|
||||||
|
def aligned_name(self) -> str:
|
||||||
|
"""Имя выровненного файла"""
|
||||||
|
return f"Anixarty-v{self.safe_version}-aligned.apk"
|
||||||
|
|
||||||
|
@computed_field
|
||||||
|
@property
|
||||||
|
def signed_name(self) -> str:
|
||||||
|
"""Имя подписанного файла"""
|
||||||
|
return f"Anixarty-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
@@ -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
|
||||||
|
|||||||
@@ -1,6 +0,0 @@
|
|||||||
import struct
|
|
||||||
|
|
||||||
|
|
||||||
def float_to_hex(f):
|
|
||||||
b = struct.pack(">f", f)
|
|
||||||
return b.hex()
|
|
||||||
+159
@@ -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())
|
||||||
@@ -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)
|
||||||
+14
-5
@@ -1,8 +1,10 @@
|
|||||||
from lxml import etree
|
|
||||||
from copy import deepcopy
|
from copy import deepcopy
|
||||||
|
|
||||||
|
from lxml import etree
|
||||||
|
from typing_extensions import Optional
|
||||||
|
|
||||||
def insert_after_public(anchor_name: str, elem_name: str):
|
|
||||||
|
def insert_after_public(anchor_name: str, elem_name: str) -> Optional[int]:
|
||||||
file_path = "./decompiled/res/values/public.xml"
|
file_path = "./decompiled/res/values/public.xml"
|
||||||
parser = etree.XMLParser(remove_blank_text=True)
|
parser = etree.XMLParser(remove_blank_text=True)
|
||||||
tree = etree.parse(file_path, parser)
|
tree = etree.parse(file_path, parser)
|
||||||
@@ -19,6 +21,8 @@ def insert_after_public(anchor_name: str, elem_name: str):
|
|||||||
anchor = (elem, attrs)
|
anchor = (elem, attrs)
|
||||||
types[attrs["type"]] = types.get(attrs["type"], []) + [int(attrs["id"], 16)]
|
types[attrs["type"]] = types.get(attrs["type"], []) + [int(attrs["id"], 16)]
|
||||||
|
|
||||||
|
assert anchor != None
|
||||||
|
|
||||||
free_ids = set()
|
free_ids = set()
|
||||||
group = types[anchor[1]["type"]]
|
group = types[anchor[1]["type"]]
|
||||||
for i in range(min(group), max(group) + 1):
|
for i in range(min(group), max(group) + 1):
|
||||||
@@ -47,7 +51,7 @@ def insert_after_public(anchor_name: str, elem_name: str):
|
|||||||
return new_id
|
return new_id
|
||||||
|
|
||||||
|
|
||||||
def insert_after_id(anchor_name: str, elem_name: str):
|
def insert_after_id(anchor_name: str, elem_name: str) -> None:
|
||||||
file_path = "./decompiled/res/values/ids.xml"
|
file_path = "./decompiled/res/values/ids.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)
|
||||||
@@ -62,13 +66,15 @@ def insert_after_id(anchor_name: str, elem_name: str):
|
|||||||
assert anchor == None
|
assert anchor == None
|
||||||
anchor = (elem, attrs)
|
anchor = (elem, attrs)
|
||||||
|
|
||||||
|
assert anchor != None
|
||||||
|
|
||||||
new_elem = deepcopy(anchor[0])
|
new_elem = deepcopy(anchor[0])
|
||||||
new_elem.set("name", elem_name)
|
new_elem.set("name", elem_name)
|
||||||
anchor[0].addnext(new_elem)
|
anchor[0].addnext(new_elem)
|
||||||
tree.write(file_path, pretty_print=True, xml_declaration=True, encoding="utf-8")
|
tree.write(file_path, pretty_print=True, xml_declaration=True, encoding="utf-8")
|
||||||
|
|
||||||
|
|
||||||
def change_color(name: str, value: str):
|
def change_color(name: str, value: str) -> None:
|
||||||
file_path = "./decompiled/res/values/colors.xml"
|
file_path = "./decompiled/res/values/colors.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)
|
||||||
@@ -86,7 +92,8 @@ def change_color(name: str, value: str):
|
|||||||
assert replacements >= 1
|
assert replacements >= 1
|
||||||
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")
|
||||||
|
|
||||||
def insert_after_color(anchor_name: str, elem_name: str, elem_value: str):
|
|
||||||
|
def insert_after_color(anchor_name: str, elem_name: str, elem_value: str) -> None:
|
||||||
file_path = "./decompiled/res/values/colors.xml"
|
file_path = "./decompiled/res/values/colors.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)
|
||||||
@@ -101,6 +108,8 @@ def insert_after_color(anchor_name: str, elem_name: str, elem_value: str):
|
|||||||
assert anchor == None
|
assert anchor == None
|
||||||
anchor = (elem, attrs)
|
anchor = (elem, attrs)
|
||||||
|
|
||||||
|
assert anchor != None
|
||||||
|
|
||||||
new_elem = deepcopy(anchor[0])
|
new_elem = deepcopy(anchor[0])
|
||||||
new_elem.set("name", elem_name)
|
new_elem.set("name", elem_name)
|
||||||
anchor[0].addnext(new_elem)
|
anchor[0].addnext(new_elem)
|
||||||
|
|||||||
@@ -67,3 +67,11 @@ def find_and_replace_smali_line(
|
|||||||
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()
|
||||||
|
|
||||||
|
|
||||||
|
def quick_replace(file: str) -> None:
|
||||||
|
content = ""
|
||||||
|
with open(file, "r", encoding="utf-8") as smali:
|
||||||
|
content = smali.read()
|
||||||
|
with open(file, "w", encoding="utf-8") as f:
|
||||||
|
f.writelines(content)
|
||||||
|
|||||||
+28
-5
@@ -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")
|
||||||
@@ -19,16 +19,19 @@ def ensure_dirs():
|
|||||||
for d in [TOOLS, ORIGINAL, MODIFIED, DECOMPILED, PATCHES, CONFIGS]:
|
for d in [TOOLS, ORIGINAL, MODIFIED, DECOMPILED, PATCHES, CONFIGS]:
|
||||||
d.mkdir(exist_ok=True)
|
d.mkdir(exist_ok=True)
|
||||||
|
|
||||||
|
|
||||||
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)
|
||||||
raise typer.Exit(1)
|
raise typer.Exit(1)
|
||||||
|
|
||||||
|
|
||||||
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:
|
||||||
@@ -43,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]
|
||||||
|
|||||||
Reference in New Issue
Block a user