35 Commits

Author SHA1 Message Date
ec047cd3a5 Исправление создания релиза
Сборка мода / build (push) Successful in 4m59s
2025-10-11 19:08:31 +03:00
2fe61c1445 Перевод названий этапов в build.yml
Сборка мода / build (push) Successful in 7m31s
2025-10-11 18:53:51 +03:00
28c60aa7a3 Обновление вывода информации о патчах, добавление патча лайков/дизлайков 2025-10-11 18:40:44 +03:00
b646dbf6fe Добавление ссылко на аккаунты gitea 2025-10-02 17:41:34 +00:00
f46425b169 Патч для замены текста ссылок в "поделиться" 2025-10-02 17:13:11 +03:00
19e1ce2f45 Патч для выделения информации о релизе 2025-10-02 10:32:51 +03:00
fbc8b3e017 Обновление структуры проекта, использование pydantic для конфигураций, улучшение отчёта о патчах
Build mod / build (push) Successful in 6m39s
2025-10-01 23:07:37 +03:00
5ba590cc31 Обновить README.md 2025-09-28 08:40:25 +00:00
0a4aa544a2 Обновить README.md 2025-09-28 08:24:57 +00:00
40f9cf0307 Обновить README.md 2025-09-28 08:22:32 +00:00
8b8ca63bb1 Удаление ненужного 2025-09-22 09:39:19 +03:00
670c53ba69 Перенос resources и добавления assets в патч настроек 2025-09-22 09:35:51 +03:00
5ff882a8d5 Рефакторинг патчей, реализация Список патчей:
settings_urls: ✔ enabled
  disable_ad: ✔ enabled
  disable_beta_banner: ✔ enabled
  insert_new: ✔ enabled
  color_theme: ✔ enabled
  change_server: ✘ disabled
  package_name: ✔ enabled
  replace_navbar: ✔ enabled
  compress: ✔ enabled, обновление описаний
2025-09-20 23:00:00 +03:00
66336f3a5c Обновление ссылки 2001-01-01 00:00:00 +00:00
85aef3d997 Добавлеие вывода информации о включеных патчах
Build mod / build (push) Successful in 5m27s
2025-09-18 10:54:30 +03:00
41399eca2c Запуск сборки на создание тэга
Build mod / build (push) Successful in 5m1s
2025-09-15 23:34:24 +03:00
137c939e1d Загрузка pngquant 2025-09-15 23:31:19 +03:00
18ad769d33 Дополнение requirements.txt 2025-09-15 23:26:33 +03:00
5b39aec161 Исправление загрузки файлов с прогресс-баром 2025-09-15 23:22:48 +03:00
c1bb2f8845 Исправление загрузки tools 2025-09-15 16:48:00 +03:00
630ab0d094 Merge remote-tracking branch 'origin/main' 2025-09-15 16:33:39 +03:00
0f9f6f2932 Исправление патча compress обновление списка зависимостей 2025-09-15 16:33:26 +03:00
a09181fe5a Исправление requirements 2025-09-15 11:47:17 +00:00
77694ec4b7 Merge remote-tracking branch 'origin/main' 2025-09-15 00:27:11 +03:00
b8ab508dfb Доавление описания патчей в docstring 2025-09-15 00:26:45 +03:00
debf561cf9 Обновить README.md 2025-09-14 18:51:03 +00:00
f7e186d5db Обновить README.md 2025-09-14 18:47:46 +00:00
24a8a1d4d3 Патч панели навигации и подписи версии в настройках 2025-09-14 21:46:39 +03:00
550427338a Обновление документации репозитория 2025-09-14 17:46:33 +00:00
ac241e1189 Перенос добавления ресурсов в соответствующие патчи 2025-09-14 20:37:14 +03:00
c22ef507ba Слияние с обновлённым main 2025-09-14 20:26:24 +03:00
9da9e98547 Улучшение кода main.py и конфигурации 2025-09-14 20:12:28 +03:00
48953a857b Merge pull request 'Объединение патчей cleanup.py и compress_png.py в один compress.py. Добавление доп. функций для сжатия апк' (#9) from Radiquum/patcher:main into main
Reviewed-on: https://git.wowlikon.tech/anixart-mod/patcher/pulls/9
2025-09-14 17:05:08 +00:00
871ec11f7e Объединение патчей cleanup.py и compress_png.py в один compress.py. Добавление доп. функций для сжатия апк 2025-09-14 19:45:52 +05:00
e2614990df Обновление workflow 2001-01-01 00:00:00 +00:00
62 changed files with 1474 additions and 561 deletions
+35 -11
View File
@@ -1,7 +1,10 @@
name: Build mod name: Сборка мода
on: on:
workflow_dispatch: workflow_dispatch:
push:
tags:
- 'v*'
#schedule: # раз в 36 часов #schedule: # раз в 36 часов
# - cron: "0 0 */3 * *" # каждые 3 дня в 00:00 # - cron: "0 0 */3 * *" # каждые 3 дня в 00:00
# - cron: "0 12 */3 * *" # каждые 3 дня в 12:00 # - cron: "0 12 */3 * *" # каждые 3 дня в 12:00
@@ -13,18 +16,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: Export secrets - name: Проверка наличия pngquant
run: |
if ! command -v pngquant &> /dev/null; then
echo "pngquant не найден, устанавливаем..."
sudo apt-get update && sudo apt-get install -y --no-install-recommends pngquant
fi
- name: Извлечение хранилища ключей
env: env:
KEYSTORE: ${{ secrets.KEYSTORE }} KEYSTORE: ${{ secrets.KEYSTORE }}
KEYSTORE_PASS: ${{ secrets.KEYSTORE_PASS }} KEYSTORE_PASS: ${{ secrets.KEYSTORE_PASS }}
@@ -33,33 +43,47 @@ jobs:
echo "$KEYSTORE" | base64 -d > keystore.jks echo "$KEYSTORE" | base64 -d > keystore.jks
echo "$KEYSTORE_PASS" > keystore.pass echo "$KEYSTORE_PASS" > keystore.pass
- name: Build APK - name: Подготовка к модифицированию APK
id: build id: build
run: | run: |
mkdir original mkdir original
mv app.apk original/ mv app.apk original/
python ./main.py -f pip install -r ./requirements.txt --break-system-packages
python ./main.py init
- name: Read title from report.log - name: Пересборка APK
id: build
run: |
python ./main.py build -f
- name: Чтение title из report.log
id: get_title id: get_title
run: | run: |
TITLE=$(head -n 1 modified/report.log) TITLE=$(head -n 1 modified/report.log)
echo "title=${TITLE}" >> $GITHUB_OUTPUT echo "title=${TITLE}" >> $GITHUB_OUTPUT
- name: Setup go - name: Чтение body из report.log
id: get_body
run: |
BODY=$(tail -n +2 modified/report.log)
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 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
+12 -21
View File
@@ -11,7 +11,7 @@
- `patches` Модули патчей - `patches` Модули патчей
- `utils` Вспомогательные модули - `utils` Вспомогательные модули
- `tools` Инструменты для модификации - `tools` Инструменты для модификации
- `patches/resources` Ресурсы, используемые патчами - `resources` Ресурсы, используемые патчами
- `todo_drafts` Заметки для новых патчей(можно в любом формате) - `todo_drafts` Заметки для новых патчей(можно в любом формате)
### Схема ### Схема
@@ -32,7 +32,7 @@ flowchart TD
end end
p f3@==> F[Сборка apk обратно] p f3@==> F[Сборка apk обратно]
F f4@==> G[Выравнивание zipaling] F f4@==> G[Выравнивание zipalign]
G f5@==> H[Подпись V2+V3] G f5@==> H[Подпись V2+V3]
H f6@==> I([Модифицированый apk]) H f6@==> I([Модифицированый apk])
@@ -52,39 +52,30 @@ flowchart TD
git clone https://git.wowlikon.tech/anixart-mod/patcher.git git clone https://git.wowlikon.tech/anixart-mod/patcher.git
``` ```
Требования: Требования:
- Python 3.6+ - Python 3.8+
- Java 8+ - Java 8+
- zipalign - zipalign
- apksigner - apksigner
- pngquant - pngquant
Все остальные инструменты и зависимости будут автоматически установлены при запуске `main.py`. Все остальные инструменты и зависимости будут автоматически установлены при запуске `main.py init`.
2. Создайте keystore с помощью `keytool` (требуется только один раз): 2. Создайте keystore с помощью `keytool` (требуется только один раз):
```sh ```sh
keytool -genkey -v -keystore keystore.jks -alias [имя_пользователя] -keyalg RSA -keysize 2048 -validity 10000 keytool -genkey -v -keystore keystore.jks -alias [имя_пользователя] -keyalg RSA -keysize 2048 -validity 10000
``` ```
Пароль от keystore нужно сохранить в `keystore.pass` для полностью автоматической сборки.
2. Измените настройки мода в файле `patches/config.json`. Если вы развернули свой [сервер](https://git.wowlikon.tech/anixart-mod/server), то измените `"server": "https://new.url"` 3. Измените настройки мода в файле `patches/config.json`. Если вы развернули свой [сервер](https://git.wowlikon.tech/anixart-mod/server), то измените `"server": "https://new.url"`
3. Поместите оригинальный apk файла anixart в папку `original` 4. Поместите оригинальный apk файла anixart в папку `original`
4. Запустите `main.py` и выберите файл apk 5. Запустите `main.py build` и выберите файл apk
6. Установите приложение на ваше устройство.
## ПОКА ЕЩЁ В РАЗРАБОТКЕ И ПОЭТОМУ НЕ В СКРИПТЕ
1. Перейдите в папку `anixart/dist` и запустите `zipalign`:
```sh
zipalign -p 4 anixart.apk anixart-aligned.apk
```
2. Запустите `apksigner` для подписи apk файла:
```sh
apksigner sign --ks /путь/до/keystore.jks --out anixart-modded.apk anixart-aligned.apk
```
3. Установите приложение на ваше устройство.
## Лицензия: ## Лицензия:
Этот проект лицензирован под лицензией MIT. См. [LICENSE](./LICENSE) для получения подробной информации. Этот проект лицензирован под лицензией MIT. См. [LICENSE](./LICENSE) для получения подробной информации.
### Вклад в проект: ### Вклад в проект:
- Seele - Все оригинальные патчи основаны на модификации приложения от Seele [[GitHub](https://github.com/seeleme) | [Telegram](https://t.me/seele_off)] - [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)]
- Kentai Radiquum - Разработка неофициального сайта и помощь с изучением API [[GitHub](https://github.com/Radiquum) | [Telegram](https://t.me/radiquum)] - [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)]
+3 -54
View File
@@ -3,61 +3,10 @@
"apktool_jar_url": "https://bitbucket.org/iBotPeaches/apktool/downloads/apktool_2.12.0.jar", "apktool_jar_url": "https://bitbucket.org/iBotPeaches/apktool/downloads/apktool_2.12.0.jar",
"apktool_wrapper_url": "https://raw.githubusercontent.com/iBotPeaches/Apktool/master/scripts/linux/apktool" "apktool_wrapper_url": "https://raw.githubusercontent.com/iBotPeaches/Apktool/master/scripts/linux/apktool"
}, },
"new_package_name": "com.wowlikon.anixart2", "base": {
"server": "https://anixarty.wowlikon.tech/modding",
"theme": {
"colors": {
"primary": "#ccff00",
"secondary": "#ffffd700",
"background": "#ffffff",
"text": "#000000"
},
"gradient": {
"angle": "135.0",
"from": "#ffff6060",
"to": "#ffccff00"
}
},
"cleanup": {
"keep_dirs": ["META-INF", "kotlin"]
},
"settings_urls": {
"Мы в социальных сетях": [
{
"title": "wowlikon",
"description": "Разработчик",
"url": "https://t.me/wowlikon",
"icon": "@drawable/ic_custom_telegram",
"icon_space_reserved": "false"
},
{
"title": "Kentai Radiquum",
"description": "Разработчик",
"url": "https://t.me/radiquum",
"icon": "@drawable/ic_custom_telegram",
"icon_space_reserved": "false"
},
{
"title": "Мы в Telegram",
"description": "Подпишитесь на канал, чтобы быть в курсе последних новостей.",
"url": "https://t.me/http_teapod",
"icon": "@drawable/ic_custom_telegram",
"icon_space_reserved": "false"
}
],
"Прочее": [
{
"title": "Поддержать проект",
"description": "После пожертвования вы сможете выбрать в своём профиле любую роль, например «Кошка-девочка», которая будет видна всем пользователям мода.",
"url": "https://t.me/wowlikon",
"icon": "@drawable/ic_custom_crown",
"icon_space_reserved": "false"
}
]
},
"xml_ns": { "xml_ns": {
"android": "http://schemas.android.com/apk/res/android", "android": "http://schemas.android.com/apk/res/android",
"app": "http://schemas.android.com/apk/res-auto" "app": "http://schemas.android.com/apk/res-auto"
}, }
"speeds": [9.0] }
} }
+1
View File
@@ -0,0 +1 @@
{"enabled":true,"server":"https://anixarty.0x174.su/patch"}
+1
View File
@@ -0,0 +1 @@
{"enabled":true,"logo":{"gradient":{"angle":0.0,"start_color":"#ffccff00","end_color":"#ffcccc00"},"ears_color":"#ffd0d0d0"},"colors":{"primary":"#ccff00","secondary":"#ffcccc00","background":"#ffffff","text":"#000000"}}
+1
View File
@@ -0,0 +1 @@
{"enabled":true,"replace":true,"custom_icons":true,"icon_size":"18.0dip"}
+1
View File
@@ -0,0 +1 @@
{"enabled":true,"remove_language_files":true,"remove_AI_voiceover":true,"remove_debug_lines":false,"remove_drawable_files":false,"remove_unknown_files":true,"remove_unknown_files_keep_dirs":["META-INF","kotlin"],"compress_png_files":true}
+1
View File
@@ -0,0 +1 @@
{"enabled":true}
+1
View File
@@ -0,0 +1 @@
{"enabled":true}
+1
View File
@@ -0,0 +1 @@
{"enabled":true,"package_name":"com.wowlikon.anixart"}
+1
View File
@@ -0,0 +1 @@
{"enabled":true,"items":["home","discover","feed","bookmarks","profile"]}
+1
View File
@@ -0,0 +1 @@
{"enabled":true}
+1
View File
@@ -0,0 +1 @@
{"enabled":true,"version":" by wowlikon","menu":{"Мы в социальных сетях":[{"title":"wowlikon","description":"Разработчик","url":"https://t.me/wowlikon","icon":"@drawable/ic_custom_telegram","icon_space_reserved":"false"},{"title":"Kentai Radiquum","description":"Разработчик","url":"https://t.me/radiquum","icon":"@drawable/ic_custom_telegram","icon_space_reserved":"false"},{"title":"Мы в Telegram","description":"Подпишитесь на канал, чтобы быть в курсе последних новостей.","url":"https://t.me/http_teapod","icon":"@drawable/ic_custom_telegram","icon_space_reserved":"false"}],"Прочее":[{"title":"Помочь проекту","description":"Вы можете помочь нам в разработке мода, написании кода или тестировании.","url":"https://git.wowlikon.tech/anixart-mod","icon":"@drawable/ic_custom_crown","icon_space_reserved":"false"}]}}
+1
View File
@@ -0,0 +1 @@
{"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"}}
+1
View File
@@ -0,0 +1 @@
{"enabled":true,"title":"Anixarty","description":"Описание","link_text":"МЫ В TELEGRAM","link_url":"https://t.me/http_teapod","skip_text":"Пропустить","title_bg_color":"#FFFFFF"}
+213 -202
View File
@@ -1,230 +1,241 @@
import os from typing import List, Dict, Any
import sys
import json import typer
import yaml
import requests
import argparse
import colorama
import importlib import importlib
import traceback import traceback
import subprocess import yaml
from tqdm import tqdm
def init() -> dict: from plumbum import local, ProcessExecutionError
for directory in ["original", "modified", "patches", "tools", "decompiled"]: from rich.console import Console
if not os.path.exists(directory): from rich.progress import Progress
os.makedirs(directory) from rich.prompt import Prompt
with open("./config.json", "r") as config_file: from utils.config import *
conf = json.load(config_file) from utils.tools import *
if not os.path.exists("./tools/apktool.jar"):
try:
print("Скачивание Apktool...")
jar_response = requests.get(conf["tools"]["apktool_jar_url"], stream=True)
jar_path = "tools/apktool.jar"
with open(jar_path, "wb") as f:
for chunk in jar_response.iter_content(chunk_size=8192):
f.write(chunk)
wrapper_response = requests.get(conf["tools"]["apktool_wrapper_url"])
wrapper_path = "tools/apktool"
with open(wrapper_path, "w") as f:
f.write(wrapper_response.text)
os.chmod(wrapper_path, 0o755)
except Exception as e:
print(f"Ошибка при скачивании Apktool: {e}")
exit(1)
try:
result = subprocess.run(
["java", "-version"], capture_output=True, text=True, check=True
)
version_line = result.stderr.splitlines()[0]
if "1.8" in version_line or any(f"{i}." in version_line for i in range(9, 100)):
print("Java 8 или более поздняя версия установлена.")
else:
print("Java 8 или более поздняя версия не установлена.")
sys.exit(1)
except subprocess.CalledProcessError:
print("Java не установлена. Установите Java 8 или более позднюю версию.")
exit(1)
return conf
def select_apk() -> str:
apks = []
for file in os.listdir("original"):
if file.endswith(".apk") and os.path.isfile(os.path.join("original", file)):
apks.append(file)
if not apks:
print("Нет файлов .apk в текущей директории")
sys.exit(1)
if len(apks) == 1:
apk = apks[0]
print(f"Выбран файл {apk}")
return apk
while True:
print("Выберете файл для модификации")
for index, apk in enumerate(apks):
print(f"{index + 1}. {apk}")
print("0. Exit")
try:
selected_index = int(input("\nВведите номер файла: "))
if selected_index == 0:
sys.exit(0)
elif selected_index > len(apks):
print("Неверный номер файла")
else:
apk = apks[selected_index - 1]
print(f"Выбран файл {apk}")
return apk
except ValueError:
print("Неверный формат ввода")
except KeyboardInterrupt:
print("Прервано пользователем")
sys.exit(0)
def decompile_apk(apk: str): console = Console()
print("Декомпилируем apk...") app = typer.Typer()
try:
result = subprocess.run(
"tools/apktool d -f -o decompiled " + os.path.join("original", apk),
shell=True,
check=True,
text=True,
stdout=subprocess.DEVNULL,
stderr=subprocess.PIPE,
)
except subprocess.CalledProcessError as e:
print("Ошибка при выполнении команды:")
print(e.stderr)
sys.exit(1)
def compile_apk(apk: str):
print("Компилируем apk...")
try:
subprocess.run(
"tools/apktool b decompiled -o " + os.path.join("modified", apk),
shell=True,
check=True,
text=True,
stdout=subprocess.DEVNULL,
stderr=subprocess.PIPE,
)
subprocess.run(
"zipalign -v 4 " + os.path.join("modified", apk) + " " + os.path.join("modified", apk.replace(".apk", "-aligned.apk")),
shell=True,
check=True,
text=True,
stdout=subprocess.DEVNULL,
stderr=subprocess.PIPE,
)
subprocess.run(
"apksigner sign " +
"--v1-signing-enabled false " +
"--v2-signing-enabled true " +
"--v3-signing-enabled true " +
"--ks keystore.jks " +
"--ks-pass file:keystore.pass " +
"--out " + os.path.join("modified", apk.replace(".apk", "-mod.apk")) +
" " + os.path.join("modified", apk.replace(".apk", "-aligned.apk")),
shell=True,
check=True,
text=True,
stdout=subprocess.DEVNULL,
stderr=subprocess.PIPE,
)
title = "anixart mod "
with open('./decompiled/apktool.yml') as f:
package = yaml.safe_load(f)
title += ' '.join([f'{k}: {v}' for k, v in package['versionInfo'].items()])
with open("./modified/report.log", "w") as log_file:
log_file.write(title+'\n')
log_file.write("\n".join([f"{patch.name}: {'applied' if patch.applied else 'failed'}" for patch in patches]))
except subprocess.CalledProcessError as e:
print("Ошибка при выполнении команды:")
print(e.stderr)
sys.exit(1)
# ======================= PATCHING =========================
class Patch: class Patch:
def __init__(self, name, pkg): def __init__(self, name: str, module):
self.name = name self.name = name
self.package = pkg self.module = module
self.applied = False self.applied = False
self.priority = getattr(module, "priority", 0)
try: try:
self.priority = pkg.priority self.config = module.Config.model_validate_json((CONFIGS / f"{name}.json").read_text())
except AttributeError:
self.priority = 0
def apply(self, conf: dict) -> bool:
try:
self.applied = self.package.apply(conf)
return True
except Exception as e: except Exception as e:
print(f"Ошибка при применении патча {self.name}: {e}") console.print(f"[red]Ошибка при загрузке конфигурации патча {name}: {e}")
print(type(e), e.args) console.print(f"[yellow]Используются значения по умолчанию")
self.config = module.Config()
def apply(self, conf: Dict[str, Any]) -> bool:
try:
self.applied = bool(self.module.apply(self.config, conf))
return self.applied
except Exception as e:
console.print(f"[red]Ошибка в патче {self.name}: {e}")
traceback.print_exc() traceback.print_exc()
return False return False
if __name__ == "__main__":
parser = argparse.ArgumentParser( # ========================= INIT =========================
description="Автоматический патчер anixart" @app.command()
def init():
"""Создание директорий и скачивание инструментов"""
ensure_dirs()
conf = load_config(console)
for f in PATCHES.glob("*.py"):
if f.name.startswith("todo_") or f.name == "__init__.py":
continue
patch = Patch(f.stem, __import__(f"patches.{f.stem}", fromlist=[""]))
json_string = patch.config.model_dump_json()
(CONFIGS / f"{patch.name}.json").write_text(json_string)
if not (TOOLS / "apktool.jar").exists():
download(console, conf.tools.apktool_jar_url, TOOLS / "apktool.jar")
if not (TOOLS / "apktool").exists():
download(console, conf.tools.apktool_wrapper_url, TOOLS / "apktool")
(TOOLS / "apktool").chmod(0o755)
try:
local["java"]["-version"]()
console.print("[green]Java найдена")
except ProcessExecutionError:
console.print("[red]Java не установлена")
raise typer.Exit(1)
# ========================= INFO =========================
@app.command()
def info(patch_name: str = ""):
"""Вывод информации о патче"""
conf = load_config(console).model_dump()
if patch_name:
patch = Patch(patch_name, __import__(f"patches.{patch_name}", fromlist=[""]))
console.print(f"[green]Информация о патче {patch.name}:")
console.print(f" [yellow]Приоритет: {patch.priority}")
console.print(f" [yellow]Описание: {patch.module.__doc__}")
console.print(f"[blue]Поля конфигурации")
for field_name, field_info in type(patch.config).model_fields.items():
field_data = {
'type': field_info.annotation.__name__,
'description': field_info.description,
'default': field_info.default,
'json_schema_extra': field_info.json_schema_extra,
}
console.print(f'{field_name} {field_data}')
console.print("\n[blue]" + "="*50 + "\n")
else:
conf = load_config(console)
console.print("[cyan]Список патчей:")
patch_list = []
for f in PATCHES.glob("*.py"):
if f.name == "__init__.py": continue
if f.name.startswith("todo_"):
try: priority = __import__(f"patches.{f.stem}.priority", fromlist=[""])
except: priority = None
patch_list.append((priority, f" [{priority}] [yellow]{f.stem}: [yellow]⚠ в разработке"))
continue
patch = Patch(f.stem, __import__(f"patches.{f.stem}", fromlist=[""]))
if patch.config.enabled: patch_list.append((patch.priority, f" [{patch.priority}] [yellow]{f.stem}: [green]✔ включен"))
else: patch_list.append((patch.priority, f" [{patch.priority}] [yellow]{f.stem}: [red]✘ выключен"))
for _, patch in sorted(patch_list, key=lambda x: (x[0] is None, x[0]), reverse=True): console.print(patch)
# ========================= UTIL =========================
def select_apk() -> Path:
apks = [f for f in ORIGINAL.glob("*.apk")]
if not apks:
console.print("[red]Нет apk-файлов в папке original")
raise typer.Exit(1)
if len(apks) == 1:
console.print(f"[green]Выбран {apks[0].name}")
return apks[0]
options = {str(i): apk for i, apk in enumerate(apks, 1)}
for k, v in options.items():
console.print(f"{k}. {v.name}")
choice = Prompt.ask("Выберите номер", choices=list(options.keys()))
return options[choice]
def decompile(apk: Path):
console.print("[yellow]Декомпиляция apk...")
run(
console,
[
"java",
"-jar", str(TOOLS / "apktool.jar"),
"d", "-f",
"-o", str(DECOMPILED),
str(apk),
]
) )
parser.add_argument("-v", "--verbose",
action="store_true",
help="Выводить подробные сообщения")
parser.add_argument("-f", "--force", def compile(apk: Path, patches: List[Patch]):
action="store_true", console.print("[yellow]Сборка apk...")
help="Принудительно собрать APK")
args = parser.parse_args() with open(DECOMPILED / "apktool.yml", encoding="utf-8") as f:
meta = yaml.safe_load(f)
version_info = meta.get("versionInfo", {})
version_code = version_info.get("versionCode", 0)
version_name = version_info.get("versionName", "unknown")
conf = init() filename_version = version_name.lower().replace(" ", "-").replace(".", "-")
out_apk = MODIFIED / f"Anixarty-mod-v{filename_version}.apk"
aligned = out_apk.with_stem(out_apk.stem + "-aligned")
signed = out_apk.with_stem(out_apk.stem + "-mod")
run(
console,
[
"java",
"-jar", str(TOOLS / "apktool.jar"),
"b", str(DECOMPILED),
"-o", str(out_apk),
]
)
run(
console,
["zipalign", "-v", "4", str(out_apk), str(aligned)]
)
run(
console,
[
"apksigner", "sign",
"--v1-signing-enabled", "false",
"--v2-signing-enabled", "true",
"--v3-signing-enabled", "true",
"--ks", "keystore.jks",
"--ks-pass", "file:keystore.pass",
"--out", str(signed),
str(aligned),
]
)
console.print("[green]✔ APK успешно собран и подписан")
with open(MODIFIED / "report.log", "w", encoding="utf-8") as f:
f.write(f"Anixarty mod v {version_name} ({version_code})\n")
for p in patches:
f.write(f"{'' if p.applied else ''} {p.name}\n")
# ========================= BUILD =========================
@app.command()
def build(
force: bool = typer.Option(False, "--force", "-f", help="Принудительная сборка"),
verbose: bool = typer.Option(False, "--verbose", "-v", help="Подробный вывод"),
):
"""Декомпиляция, патчи и сборка apk"""
conf = load_config(console)
apk = select_apk() apk = select_apk()
patch = decompile_apk(apk) decompile(apk)
if args.verbose: conf["verbose"] = True patch_objs: List[Patch] = []
conf.base |= {"verbose": verbose}
patches = [] for f in PATCHES.glob("*.py"):
for filename in os.listdir("patches/"): if f.name.startswith("todo_") or f.name == "__init__.py":
if filename.endswith(".py") and filename != "__init__.py" and not filename.startswith("todo_"): continue
module_name = filename[:-3] name = f.stem
module = importlib.import_module(f"patches.{module_name}") module = importlib.import_module(f"patches.{name}")
patches.append(Patch(module_name, module)) 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))
patches.sort(key=lambda x: x.package.priority, reverse=True) patch_objs.sort(key=lambda p: p.priority, reverse=True)
for patch in tqdm(patches, colour="green", desc="Применение патчей"): console.print("[cyan]Применение патчей")
tqdm.write(f"Применение патча: {patch.name}") with Progress() as progress:
patch.apply(conf) task = progress.add_task("Патчи", total=len(patch_objs))
for p in patch_objs:
ok = p.apply(conf.base)
progress.console.print(f"{'' if ok else ''} {p.name}")
progress.advance(task)
statuses = {} successes = sum(p.applied for p in patch_objs)
for patch in patches: if successes == len(patch_objs):
statuses[patch.name] = patch.applied compile(apk, patch_objs)
marker = colorama.Fore.GREEN + "" if patch.applied else colorama.Fore.RED + "" elif successes > 0 and (
print(f"{marker}{colorama.Style.RESET_ALL} {patch.name}") force or Prompt.ask("Продолжить сборку?", choices=["y", "n"]) == "y"
):
if all(statuses.values()): compile(apk, patch_objs)
print(f"{colorama.Fore.GREEN}Все патчи успешно применены{colorama.Style.RESET_ALL}")
compile_apk(apk)
elif any(statuses.values()):
print(f"{colorama.Fore.YELLOW}{colorama.Style.RESET_ALL} Некоторые патчи не были успешно применены")
if args.force or input("Продолжить? (y/n): ").lower() == "y":
compile_apk(apk)
else: else:
print(colorama.Fore.RED + "Операция отменена" + colorama.Style.RESET_ALL) console.print("[red]Сборка отменена")
else: raise typer.Exit(1)
print(f"{colorama.Fore.RED}Ни один патч не был успешно применен{colorama.Style.RESET_ALL}")
sys.exit(1)
if __name__ == "__main__": app()
+32 -9
View File
@@ -1,39 +1,62 @@
"""Change api server""" """Заменяет сервер api
priority = 0
from tqdm import tqdm
"change_server": {
"enabled": true,
"server": "https://anixarty.0x174.su/patch"
}
"""
priority = 0
# imports
import json import json
import requests import requests
from tqdm import tqdm
from typing import Dict, Any
from pydantic import Field
from utils.config import PatchConfig
def apply(config: dict) -> bool: #Config
response = requests.get(config['server']) class Config(PatchConfig):
server: str = Field("https://anixarty.0x174.su/patch", description="URL сервера")
# Patch
def apply(config: Config, base: Dict[str, Any]) -> bool:
response = requests.get(config.server) # Получаем данные для патча
assert response.status_code == 200, f"Failed to fetch data {response.status_code} {response.text}" assert response.status_code == 200, f"Failed to fetch data {response.status_code} {response.text}"
new_api = json.loads(response.text) new_api = json.loads(response.text)
for item in new_api['modifications']: for item in new_api['modifications']: # Применяем замены API
tqdm.write(f"Изменение {item['file']}") tqdm.write(f"Изменение {item['file']}")
filepath = './decompiled/smali_classes2/com/swiftsoft/anixartd/network/api/'+item['file'] filepath = './decompiled/smali_classes2/com/swiftsoft/anixartd/network/api/'+item['file']
with open(filepath, 'r') as f: with open(filepath, 'r') as f:
content = f.read() content = f.read()
with open(filepath, 'w') as f: with open(filepath, 'w') as f:
if content.count(item['src']) == 0: if content.count(item['src']) == 0:
tqdm.write(f"Не найдено {item['src']}") tqdm.write(f"Не найдено {item['src']}")
f.write(content.replace(item['src'], item['dst'])) f.write(content.replace(item['src'], item['dst']))
tqdm.write(f"Изменение Github ссылки") tqdm.write(f"Изменение Github ссылки") # Обновление ссылки на поиск серверов в Github
filepath = './decompiled/smali_classes2/com/swiftsoft/anixartd/utils/anixnet/GithubPagesNetFetcher.smali' filepath = './decompiled/smali_classes2/com/swiftsoft/anixartd/utils/anixnet/GithubPagesNetFetcher.smali'
with open(filepath, 'r') as f: with open(filepath, 'r') as f:
content = f.read() content = f.read()
with open(filepath, 'w') as f: with open(filepath, 'w') as f:
f.write(content.replace('const-string v1, "https://anixhelper.github.io/pages/urls.json"', f'const-string v1, "{new_api["gh"]}"')) f.write(content.replace('const-string v1, "https://anixhelper.github.io/pages/urls.json"', f'const-string v1, "{new_api["gh"]}"'))
content = "" tqdm.write("Удаление динамического выбора сервера") # Отключение автовыбора сервера
tqdm.write("Удаление динамического выбора сервера")
filepath = './decompiled/smali_classes2/com/swiftsoft/anixartd/DaggerApp_HiltComponents_SingletonC$SingletonCImpl$SwitchingProvider.smali' filepath = './decompiled/smali_classes2/com/swiftsoft/anixartd/DaggerApp_HiltComponents_SingletonC$SingletonCImpl$SwitchingProvider.smali'
content = ""
with open(filepath, 'r') as f: with open(filepath, 'r') as f:
for line in f.readlines(): for line in f.readlines():
if "addInterceptor" in line: continue if "addInterceptor" in line: continue
content += line content += line
with open(filepath, 'w') as f: with open(filepath, 'w') as f:
f.write(content) f.write(content)
-22
View File
@@ -1,22 +0,0 @@
"""Remove unnecessary files"""
priority = 0
from tqdm import tqdm
import os
import shutil
def apply(config: dict) -> bool:
for item in os.listdir("./decompiled/unknown/"):
item_path = os.path.join("./decompiled/unknown/", item)
if os.path.isfile(item_path):
os.remove(item_path)
if config.get("verbose", False):
tqdm.write(f'Удалён файл: {item_path}')
elif os.path.isdir(item_path):
if item not in config["cleanup"]["keep_dirs"]:
shutil.rmtree(item_path)
if config.get("verbose", False):
tqdm.write(f'Удалена папка: {item_path}')
return True
+73 -39
View File
@@ -1,23 +1,64 @@
"""Change application theme""" """Изменяет цветовую тему приложения и иконку
"color_theme": {
"enabled": true,
"logo": {
"gradient": {
"angle": 0.0,
"start_color": "#ffccff00",
"end_color": "#ffcccc00"
},
"ears_color": "#ffffd0d0"
},
"colors": {
"primary": "#ccff00",
"secondary": "#ffcccc00",
"background": "#ffffff",
"text": "#000000"
}
}
"""
priority = 0 priority = 0
# imports
from lxml import etree from lxml import etree
from typing import Dict, Any
from pydantic import Field, BaseModel
from utils.config import PatchConfig
from utils.public import ( from utils.public import (
insert_after_public, insert_after_public,
insert_after_color, insert_after_color,
insert_after_id,
change_color, change_color,
) )
def apply(config: dict) -> bool: #Config
main_color = config["theme"]["colors"]["primary"] class Gradient(BaseModel):
splash_color = config["theme"]["colors"]["secondary"] angle: float = Field(0.0, description="Угол градиента")
gradient_angle = config["theme"]["gradient"]["angle"] start_color: str = Field("#ffccff00", description="Начальный цвет градиента")
gradient_from = config["theme"]["gradient"]["from"] end_color: str = Field("#ffcccc00", description="Конечный цвет градиента")
gradient_to = config["theme"]["gradient"]["to"]
# No connection alert coolor class Logo(BaseModel):
gradient: Gradient = Field(Gradient(), description="Настройки градиента") # type: ignore [reportCallIssue]
ears_color: str = Field("#ffd0d0d0", description="Цвет ушей логотипа")
class Colors(BaseModel):
primary: str = Field("#ccff00", description="Основной цвет")
secondary: str = Field("#ffcccc00", description="Вторичный цвет")
background: str = Field("#ffffff", description="Фоновый цвет")
text: str = Field("#000000", description="Цвет текста")
class Config(PatchConfig):
logo: Logo = Field(Logo(), description="Настройки цветов логотипа") # type: ignore [reportCallIssue]
colors: Colors = Field(Colors(), description="Настройки цветов") # type: ignore [reportCallIssue]
# Patch
def apply(config: Config, base: Dict[str, Any]) -> bool:
main_color = config.colors.primary
splash_color = config.colors.secondary
# Обновление сообщения об отсутствии подключения
with open("./decompiled/assets/no_connection.html", "r", encoding="utf-8") as file: with open("./decompiled/assets/no_connection.html", "r", encoding="utf-8") as file:
file_contents = file.read() file_contents = file.read()
@@ -26,39 +67,40 @@ def apply(config: dict) -> bool:
with open("./decompiled/assets/no_connection.html", "w", encoding="utf-8") as file: with open("./decompiled/assets/no_connection.html", "w", encoding="utf-8") as file:
file.write(new_contents) file.write(new_contents)
# For logo # Суффиксы лого
drawable_types = ["", "-night"] drawable_types = ["", "-night"]
for drawable_type in drawable_types: for drawable_type in drawable_types:
# Application logo gradient colors # Градиент лого приложения
file_path = f"./decompiled/res/drawable{drawable_type}/$logo__0.xml" file_path = f"./decompiled/res/drawable{drawable_type}/$logo__0.xml"
parser = etree.XMLParser(remove_blank_text=True) parser = etree.XMLParser(remove_blank_text=True)
tree = etree.parse(file_path, parser) tree = etree.parse(file_path, parser)
root = tree.getroot() root = tree.getroot()
# Change attributes with namespace # Замена атрибутов значениями из конфигурации
root.set(f"{{{config['xml_ns']['android']}}}angle", gradient_angle) root.set(f"{{{base['xml_ns']['android']}}}angle", str(config.logo.gradient.angle))
root.set(f"{{{config['xml_ns']['android']}}}startColor", gradient_from) root.set(f"{{{base['xml_ns']['android']}}}startColor", config.logo.gradient.start_color)
root.set(f"{{{config['xml_ns']['android']}}}endColor", gradient_to) root.set(f"{{{base['xml_ns']['android']}}}endColor", config.logo.gradient.end_color)
# Save back # Сохранение
tree.write(file_path, pretty_print=True, xml_declaration=True, encoding="utf-8") tree.write(file_path, pretty_print=True, xml_declaration=True, encoding="utf-8")
# Application logo anim color # Замена анимации лого
file_path = f"./decompiled/res/drawable{drawable_type}/$logo_splash_anim__0.xml" file_path = f"./decompiled/res/drawable{drawable_type}/$logo_splash_anim__0.xml"
parser = etree.XMLParser(remove_blank_text=True) parser = etree.XMLParser(remove_blank_text=True)
tree = etree.parse(file_path, parser) tree = etree.parse(file_path, parser)
root = tree.getroot() root = tree.getroot()
# Finding "path" for el in root.findall("path", namespaces=base["xml_ns"]):
for el in root.findall("path", namespaces=config["xml_ns"]): name = el.get(f"{{{base['xml_ns']['android']}}}name")
name = el.get(f"{{{config['xml_ns']['android']}}}name")
if name == "path": if name == "path":
el.set(f"{{{config['xml_ns']['android']}}}fillColor", splash_color) el.set(f"{{{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") tree.write(file_path, pretty_print=True, xml_declaration=True, encoding="utf-8")
for filename in ["$ic_launcher_foreground__0", "$ic_banner_foreground__0"]: for filename in ["$ic_launcher_foreground__0", "$ic_banner_foreground__0"]:
@@ -68,25 +110,28 @@ def apply(config: dict) -> bool:
tree = etree.parse(file_path, parser) tree = etree.parse(file_path, parser)
root = tree.getroot() root = tree.getroot()
# Change attributes with namespace # Замена атрибутов значениями из конфигурации
root.set(f"{{{config['xml_ns']['android']}}}angle", gradient_angle) root.set(f"{{{base['xml_ns']['android']}}}angle", str(config.logo.gradient.angle))
items = root.findall("item", namespaces=config['xml_ns']) items = root.findall("item", namespaces=base['xml_ns'])
assert len(items) == 2 assert len(items) == 2
items[0].set(f"{{{config['xml_ns']['android']}}}color", gradient_from) items[0].set(f"{{{base['xml_ns']['android']}}}color", config.logo.gradient.start_color)
items[1].set(f"{{{config['xml_ns']['android']}}}color", gradient_to) items[1].set(f"{{{base['xml_ns']['android']}}}color", config.logo.gradient.end_color)
# Save back # Сохранение
tree.write(file_path, pretty_print=True, xml_declaration=True, encoding="utf-8") tree.write(file_path, pretty_print=True, xml_declaration=True, encoding="utf-8")
# Добаление новых цветов для темы
insert_after_public("carmine", "custom_color") insert_after_public("carmine", "custom_color")
insert_after_public("carmine_alpha_10", "custom_color_alpha_10") insert_after_public("carmine_alpha_10", "custom_color_alpha_10")
insert_after_color("carmine", "custom_color", main_color[0]+'ff'+main_color[1:]) insert_after_color("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:]) insert_after_color("carmine_alpha_10", "custom_color_alpha_10", main_color[0]+'1a'+main_color[1:])
# Замена цветов
change_color("accent_alpha_10", main_color[0]+'1a'+main_color[1:]) change_color("accent_alpha_10", main_color[0]+'1a'+main_color[1:])
change_color("accent_alpha_20", main_color[0]+'33'+main_color[1:]) change_color("accent_alpha_20", main_color[0]+'33'+main_color[1:])
change_color("accent_alpha_50", main_color[0]+'80'+main_color[1:]) change_color("accent_alpha_50", main_color[0]+'80'+main_color[1:])
change_color("accent_alpha_70", main_color[0]+'b3'+main_color[1:]) change_color("accent_alpha_70", main_color[0]+'b3'+main_color[1:])
change_color("colorAccent", main_color[0]+'ff'+main_color[1:]) change_color("colorAccent", main_color[0]+'ff'+main_color[1:])
change_color("link_color", main_color[0]+'ff'+main_color[1:]) change_color("link_color", main_color[0]+'ff'+main_color[1:])
change_color("link_color_alpha_70", main_color[0]+'b3'+main_color[1:]) change_color("link_color_alpha_70", main_color[0]+'b3'+main_color[1:])
@@ -97,15 +142,4 @@ def apply(config: dict) -> bool:
change_color("bottom_nav_indicator_icon_checked", main_color[0]+'ff'+main_color[1:]) 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:]) change_color("bottom_nav_indicator_label_checked", main_color[0]+'ff'+main_color[1:])
insert_after_public("warning_error_counter_background", "ic_custom_telegram")
insert_after_public("warning_error_counter_background", "ic_custom_crown")
try:
last = "speed75"
for speed in config.get("speeds", []):
insert_after_public(last, f"speed{int(float(speed)*10)}")
insert_after_id(last, f"speed{int(float(speed)*10)}")
last = f"speed{int(float(speed)*10)}"
except Exception as e:
print(f"Error occurred while processing speeds: {e}")
return True return True
+99
View File
@@ -0,0 +1,99 @@
"""Меняет местами кнопки лайка и дизлайка у коментария и иконки
"comment_vote": {
"enabled": true,
"replace": true,
"custom_icons": true,
"icons_size": "14.0dip"
}
"""
priority = 0
# imports
import os
import shutil
from tqdm import tqdm
from lxml import etree
from pydantic import Field
from typing import Dict, Any
from utils.config import PatchConfig
#Config
class Config(PatchConfig):
replace: bool = Field(True, description="Менять местами лайк/дизлайк")
custom_icons: bool = Field(True, description="Кастомные иконки")
icon_size: str = Field("18.0dip", description="Размер иконки")
# Patch
def apply(config, 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", config.icon_size)
# icon.set(f"{{{base['xml_ns']['android']}}}layout_height", config.icon_size)
if config.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 config.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
+308
View File
@@ -0,0 +1,308 @@
"""Удаляет ненужное и сжимает ресурсы что-бы уменьшить размер АПК
Эффективность на проверена на версии 9.0 Beta 7
разница = оригинальный размер апк - патченный размер апк, не учитывая другие патчи
| Настройка | Размер файла | Разница | % |
| :--------------: | :-------------------: | :-----------------: | :-: |
| Ничего | 17092 bytes - 17.1 MB | - | - |
| Сжатие PNG | 17072 bytes - 17.1 MB | 20 bytes - 0.0 MB | 0.11% |
| Удалить unknown | 17020 bytes - 17.0 MB | 72 bytes - 0.1 MB | 0.42% |
| Удалить draws | 16940 bytes - 16.9 MB | 152 bytes - 0.2 MB | 0.89% |
| Удалить lines | 16444 bytes - 16.4 MB | 648 bytes - 0.7 MB | 3.79% |
| Удалить ai voice | 15812 bytes - 15.8 MB | 1280 bytes - 1.3 MB | 7.49% |
| Удалить языки | 15764 bytes - 15.7 MB | 1328 bytes - 1.3 MB | 7.76% |
| Все включены | 13592 bytes - 13.6 MB | 3500 bytes - 4.8 MB | 20.5% |
"compress": {
"enabled": true,
"remove_language_files": true, // удаляет все языки кроме русского и английского
"remove_AI_voiceover": true, // заменяет ИИ озвучку маскота аниксы пустыми mp3 файлами
"remove_debug_lines": false, // удаляет строки `.line n` из smali файлов использованные для дебага
"remove_drawable_files": false, // удаляет неиспользованные drawable-* из директории decompiled/res
"remove_unknown_files": true, // удаляет файлы из директории decompiled/unknown
"remove_unknown_files_keep_dirs": ["META-INF", "kotlin"], // оставляет указанные директории в decompiled/unknown
"compress_png_files": true // сжимает PNG в директории decompiled/res
}
"""
priority = -1
# imports
import os
import shutil
import subprocess
from tqdm import tqdm
from pydantic import Field
from typing import Dict, List, Any
from utils.config import PatchConfig
from utils.smali_parser import get_smali_lines, save_smali_lines
#Config
class Config(PatchConfig):
remove_language_files: bool = Field(True, description="Удаляет все языки кроме русского и английского")
remove_AI_voiceover: bool = Field(True, description="Заменяет ИИ озвучку маскота аниксы пустыми mp3 файлами")
remove_debug_lines: bool = Field(False, description="Удаляет строки `.line n` из smali файлов использованные для дебага")
remove_drawable_files: bool = Field(False, description="Удаляет неиспользованные drawable-* из директории decompiled/res")
remove_unknown_files: bool = Field(True, description="Удаляет файлы из директории decompiled/unknown")
remove_unknown_files_keep_dirs: List[str] = Field(["META-INF", "kotlin"], description="Оставляет указанные директории в decompiled/unknown")
compress_png_files: bool = Field(True, description="Сжимает PNG в директории decompiled/res")
# Patch
def remove_unknown_files(config: Config, base: Dict[str, Any]):
path = "./decompiled/unknown"
items = os.listdir(path)
for item in items:
item_path = f"{path}/{item}"
if os.path.isfile(item_path):
os.remove(item_path)
if base.get("verbose", False):
tqdm.write(f"Удалён файл: {item_path}")
elif os.path.isdir(item_path):
if item not in config.remove_unknown_files_keep_dirs:
shutil.rmtree(item_path)
if base.get("verbose", False):
tqdm.write(f"Удалёна директория: {item_path}")
return True
def remove_debug_lines(config: Dict[str, Any]):
for root, _, files in os.walk("./decompiled"):
for filename in files:
file_path = os.path.join(root, filename)
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
except subprocess.CalledProcessError as e:
tqdm.write(f"Ошибка при сжатии {png_path}: {e}")
return False
def compress_png_files(config: Dict[str, Any]):
compressed = []
for root, _, files in os.walk("./decompiled"):
for file in files:
if file.lower().endswith(".png"):
compress_png(config, f"{root}/{file}")
compressed.append(f"{root}/{file}")
return len(compressed) > 0 and any(compressed)
def remove_AI_voiceover(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",
]
for file in files:
if os.path.exists(f"{path}/{file}"):
os.remove(f"{path}/{file}")
shutil.copyfile(blank, f"{path}/{file}")
if config.get("verbose", False):
tqdm.write(f"Файл mp3 был заменён на пустой: {path}/{file}")
return True
def remove_language_files(config: Dict[str, Any]):
path = "./decompiled/res"
folders = [
"values-af",
"values-am",
"values-ar",
"values-as",
"values-az",
"values-b+es+419",
"values-b+sr+Latn",
"values-be",
"values-bg",
"values-bn",
"values-bs",
"values-ca",
"values-cs",
"values-da",
"values-de",
"values-el",
"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 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_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
-36
View File
@@ -1,36 +0,0 @@
"""Compress PNGs"""
priority = -1
from tqdm import tqdm
import os
import subprocess
def compress_pngs(root_dir: str, verbose: bool = False):
compressed_files = []
for dirpath, _, filenames in os.walk(root_dir):
for filename in filenames:
if filename.lower().endswith(".png"):
filepath = os.path.join(dirpath, filename)
if verbose: tqdm.write(f"Сжимаю: {filepath}")
try:
assert subprocess.run(
[
"pngquant",
"--force",
"--ext",
".png",
"--quality=65-90",
filepath,
],
capture_output=True,
).returncode in [0, 99]
compressed_files.append(filepath)
except subprocess.CalledProcessError as e:
tqdm.write(f"Ошибка при сжатии {filepath}: {e}")
return compressed_files
def apply(config: dict) -> bool:
files = compress_pngs("./decompiled", config.get("verbose", False))
return len(files) > 0 and any(files)
+22 -7
View File
@@ -1,6 +1,18 @@
"""Disable ad banners""" """Удаляет баннеры рекламы
"disable_ad": {
"enabled": true
}
"""
priority = 0 priority = 0
# imports
import textwrap
from tqdm import tqdm
from typing import Dict, Any
from utils.config import PatchConfig
from utils.smali_parser import ( from utils.smali_parser import (
find_smali_method_end, find_smali_method_end,
find_smali_method_start, find_smali_method_start,
@@ -9,15 +21,18 @@ from utils.smali_parser import (
) )
replace = """ .locals 0 #Config
class Config(PatchConfig): ...
# Patch
def apply(config: Config, base: Dict[str, Any]) -> bool:
replacement = [f'\t{line}\n' for line in textwrap.dedent("""\
.locals 0
const/4 p0, 0x1 const/4 p0, 0x1
return p0 return p0
""" """).splitlines()]
def apply(config) -> bool:
path = "./decompiled/smali_classes2/com/swiftsoft/anixartd/Prefs.smali" path = "./decompiled/smali_classes2/com/swiftsoft/anixartd/Prefs.smali"
lines = get_smali_lines(path) lines = get_smali_lines(path)
for index, line in enumerate(lines): for index, line in enumerate(lines):
@@ -25,7 +40,7 @@ def apply(config) -> bool:
method_start = find_smali_method_start(lines, index) method_start = find_smali_method_start(lines, index)
method_end = find_smali_method_end(lines, index) method_end = find_smali_method_end(lines, index)
new_content = replace_smali_method_body( new_content = replace_smali_method_body(
lines, method_start, method_end, replace lines, method_start, method_end, replacement
) )
with open(path, "w", encoding="utf-8") as file: with open(path, "w", encoding="utf-8") as file:
+20 -5
View File
@@ -1,12 +1,26 @@
"""Remove beta banner""" """Удаляет баннеры бета-версии
"disable_beta_banner": {
"enabled": true
}
"""
priority = 0 priority = 0
from tqdm import tqdm
# imports
import os import os
from tqdm import tqdm
from lxml import etree from lxml import etree
from typing import Dict, Any
from utils.config import PatchConfig
from utils.smali_parser import get_smali_lines, save_smali_lines
def apply(config) -> bool: #Config
class Config(PatchConfig): ...
# Patch
def apply(config: Config, base: Dict[str, Any]) -> bool:
attributes = [ attributes = [
"paddingTop", "paddingTop",
"paddingBottom", "paddingBottom",
@@ -27,8 +41,9 @@ def apply(config) -> bool:
root = tree.getroot() root = tree.getroot()
for attr in attributes: for attr in attributes:
# tqdm.write(f"set {attr} = 0.0dip") if base.get("verbose", False):
root.set(f"{{{config["xml_ns"]['android']}}}{attr}", "0.0dip") tqdm.write(f"set {attr} = 0.0dip")
root.set(f"{{{base['xml_ns']['android']}}}{attr}", "0.0dip")
tree.write( tree.write(
beta_banner_xml, pretty_print=True, xml_declaration=True, encoding="utf-8" beta_banner_xml, pretty_print=True, xml_declaration=True, encoding="utf-8"
-44
View File
@@ -1,44 +0,0 @@
"""Insert new files"""
priority = 0
import shutil
import os
def apply(config: dict) -> bool:
# Mod first launch window
shutil.copytree(
"./patches/resources/smali_classes4/", "./decompiled/smali_classes4/"
)
# Mod assets
shutil.copy("./patches/resources/avatar.png", "./decompiled/assets/avatar.png")
shutil.copy(
"./patches/resources/OpenSans-Regular.ttf",
"./decompiled/assets/OpenSans-Regular.ttf",
)
shutil.copy(
"./patches/resources/ic_custom_crown.xml",
"./decompiled/res/drawable/ic_custom_crown.xml",
)
shutil.copy(
"./patches/resources/ic_custom_telegram.xml",
"./decompiled/res/drawable/ic_custom_telegram.xml",
)
shutil.copy(
"./patches/resources/ytsans_medium.ttf",
"./decompiled/res/font/ytsans_medium.ttf",
)
os.remove("./decompiled/res/font/ytsans_medium.otf")
# IDK
shutil.move(
"./decompiled/res/raw/bundled_cert.crt",
"./decompiled/res/raw/bundled_cert.cer",
)
shutil.move(
"./decompiled/res/raw/sdkinternalca.crt",
"./decompiled/res/raw/sdkinternalca.cer",
)
return True
+33 -15
View File
@@ -1,44 +1,63 @@
"""Change package name of apk""" """Изменяет имя пакета в apk, удаляет вход по google и vk
"package_name": {
"enabled": true,
"new_package_name": "com.wowlikon.anixart"
}
"""
priority = -1 priority = -1
# imports
import os import os
from tqdm import tqdm
from lxml import etree from lxml import etree
from typing import Dict, Any
from pydantic import Field
from utils.config import PatchConfig
#Config
class Config(PatchConfig):
package_name: str = Field("com.wowlikon.anixart", description="Название пакета")
# Patch
def rename_dir(src, dst): 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(config: dict) -> bool: def apply(config: Config, base: Dict[str, Any]) -> bool:
assert config["new_package_name"] is not None, "new_package_name is not configured"
for root, dirs, files in os.walk("./decompiled"): for root, dirs, files in os.walk("./decompiled"):
for filename in files: for filename in files:
file_path = os.path.join(root, filename) file_path = os.path.join(root, filename)
if os.path.isfile(file_path): if os.path.isfile(file_path):
try: try: # Изменяем имя пакета в файлах
with open(file_path, "r", encoding="utf-8") as file: with open(file_path, "r", encoding="utf-8") as file:
file_contents = file.read() file_contents = file.read()
new_contents = file_contents.replace( new_contents = file_contents.replace(
"com.swiftsoft.anixartd", config["new_package_name"] "com.swiftsoft.anixartd", config.package_name
) )
new_contents = new_contents.replace( new_contents = new_contents.replace(
"com/swiftsoft/anixartd", "com/swiftsoft/anixartd",
config["new_package_name"].replace(".", "/"), config.package_name.replace(".", "/"),
).replace(
"com/swiftsoft",
"/".join(config.package_name.split(".")[:2]),
) )
with open(file_path, "w", encoding="utf-8") as file: with open(file_path, "w", encoding="utf-8") as file:
file.write(new_contents) file.write(new_contents)
except: except:
pass pass
# Изменяем названия папок
if os.path.exists("./decompiled/smali/com/swiftsoft/anixartd"): if os.path.exists("./decompiled/smali/com/swiftsoft/anixartd"):
rename_dir( rename_dir(
"./decompiled/smali/com/swiftsoft/anixartd", "./decompiled/smali/com/swiftsoft/anixartd",
os.path.join( os.path.join(
"./decompiled", "smali", config["new_package_name"].replace(".", "/") "./decompiled", "smali", config.package_name.replace(".", "/")
), ),
) )
if os.path.exists("./decompiled/smali_classes2/com/swiftsoft/anixartd"): if os.path.exists("./decompiled/smali_classes2/com/swiftsoft/anixartd"):
@@ -47,7 +66,7 @@ def apply(config: dict) -> bool:
os.path.join( os.path.join(
"./decompiled", "./decompiled",
"smali_classes2", "smali_classes2",
config["new_package_name"].replace(".", "/"), config.package_name.replace(".", "/"),
), ),
) )
if os.path.exists("./decompiled/smali_classes4/com/swiftsoft"): if os.path.exists("./decompiled/smali_classes4/com/swiftsoft"):
@@ -56,7 +75,7 @@ def apply(config: dict) -> bool:
os.path.join( os.path.join(
"./decompiled", "./decompiled",
"smali_classes4", "smali_classes4",
"/".join(config["new_package_name"].split(".")[:-1]), "/".join(config.package_name.split(".")[:2]),
), ),
) )
@@ -69,6 +88,7 @@ def apply(config: dict) -> bool:
# ), # ),
# ) # )
# Замена названия пакета для smali_classes4
for root, dirs, files in os.walk("./decompiled/smali_classes4/"): for root, dirs, files in os.walk("./decompiled/smali_classes4/"):
for filename in files: for filename in files:
file_path = os.path.join(root, filename) file_path = os.path.join(root, filename)
@@ -80,24 +100,22 @@ def apply(config: dict) -> bool:
new_contents = file_contents.replace( new_contents = file_contents.replace(
"com/swiftsoft", "com/swiftsoft",
"/".join(config["new_package_name"].split(".")[:-1]), "/".join(config.package_name.split(".")[:-1]),
) )
with open(file_path, "w", encoding="utf-8") as file: with open(file_path, "w", encoding="utf-8") as file:
file.write(new_contents) file.write(new_contents)
except: except:
pass pass
# Скрытие входа по Google и VK (НЕ РАБОТАЮТ В МОДАХ)
file_path = "./decompiled/res/layout/fragment_sign_in.xml" file_path = "./decompiled/res/layout/fragment_sign_in.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()
last_linear = root.xpath("//LinearLayout/LinearLayout[4]")[0] last_linear = root.xpath("//LinearLayout/LinearLayout[4]")[0]
last_linear.set(f"{{{config['xml_ns']['android']}}}visibility", "gone") last_linear.set(f"{{{base['xml_ns']['android']}}}visibility", "gone")
tree.write(file_path, pretty_print=True, xml_declaration=True, encoding="utf-8") tree.write(file_path, pretty_print=True, xml_declaration=True, encoding="utf-8")
return True return True
# smali_classes2/com/wowlikon/anixart/utils/DeviceInfoUtil.smali: const-string v3, "\u0411\u0430\u0433-\u0440\u0435\u043f\u043e\u0440\u0442 9.0 BETA 5 (25062213)"
+62
View File
@@ -0,0 +1,62 @@
"""
Меняет порядок вкладок в панели навигации
"replace_navbar": {
"enabled": true,
"items": ["home", "discover", "feed", "bookmarks", "profile"]
}
"""
priority = 0
# imports
from lxml import etree
from tqdm import tqdm
from typing import Dict, List, Any
from pydantic import Field
from utils.config import PatchConfig
#Config
class Config(PatchConfig):
items: List[str] = Field(["home", "discover", "feed", "bookmarks", "profile"], description="Список элементов в панели навигации")
# Patch
def apply(config: Config, base: Dict[str, Any]) -> bool:
file_path = "./decompiled/res/menu/bottom.xml"
parser = etree.XMLParser(remove_blank_text=True)
tree = etree.parse(file_path, parser)
root = tree.getroot()
# Получение элементов панели навигации
items = root.findall("item", namespaces=base['xml_ns'])
def get_id_suffix(item):
full_id = item.get(f"{{{base['xml_ns']['android']}}}id")
return full_id.split("tab_")[-1] if full_id else None
items_by_id = {get_id_suffix(item): item for item in items}
existing_order = [get_id_suffix(item) for item in items]
# Размещение в новом порядке
ordered_items = []
for key in config.items:
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:
tqdm.write("⚠Найдены лишние элементы: " + str([get_id_suffix(i) for i in extra]))
ordered_items.extend(extra)
for i in root.findall("item", namespaces=base['xml_ns']):
root.remove(i)
for item in ordered_items:
root.append(item)
tree.write(file_path, pretty_print=True, xml_declaration=True, encoding="utf-8")
return True
Binary file not shown.
+42
View File
@@ -0,0 +1,42 @@
"""Делает текст в описании аниме копируемым
"selectable_text": {
"enabled": true
}
"""
priority = 0
# imports
from tqdm import tqdm
from lxml import etree
from typing import Dict, Any
from pydantic import Field
from utils.config import PatchConfig
#Config
class Config(PatchConfig): ...
# Patch
def apply(config: Config, base: Dict[str, Any]) -> bool:
file_path = "./decompiled/res/layout/release_info.xml"
parser = etree.XMLParser(remove_blank_text=True)
tree = etree.parse(file_path, parser)
root = tree.getroot()
# Список тегов, к которым нужно добавить атрибут
tags = ["TextView", "at.blogc.android.views.ExpandableTextView"]
for tag in tags:
for element in root.findall(f".//{tag}", namespaces=base["xml_ns"]):
# Проверяем, нет ли уже атрибута
if f"{{{base['xml_ns']['android']}}}textIsSelectable" not in element.attrib:
element.set(f"{{{base['xml_ns']['android']}}}textIsSelectable", "true")
# Сохраняем
tree.write(file_path, encoding="utf-8", xml_declaration=True, pretty_print=True)
return True
+105 -8
View File
@@ -1,9 +1,78 @@
"""Add new settings""" """Добавляет в настройки ссылки и добвляет текст к версии приложения
"settings_urls": {
"enabled": true,
"menu": {
"Раздел": [
{
"title": "Заголовок",
"description": "Описание",
"url": "ссылка",
"icon": "@drawable/ic_custom_telegram",
"icon_space_reserved": "false"
},
...
],
...
]
},
"version": " by wowlikon"
}
"""
priority = 0 priority = 0
# imports
import shutil
from lxml import etree from lxml import etree
from tqdm import tqdm
from typing import Dict, List, Any
from pydantic import Field
from utils.config import PatchConfig
from utils.public import insert_after_public
#Config
DEFAULT_MENU = {
"Мы в социальных сетях": [
{
"title": "wowlikon",
"description": "Разработчик",
"url": "https://t.me/wowlikon",
"icon": "@drawable/ic_custom_telegram",
"icon_space_reserved": "false"
},
{
"title": "Kentai Radiquum",
"description": "Разработчик",
"url": "https://t.me/radiquum",
"icon": "@drawable/ic_custom_telegram",
"icon_space_reserved": "false"
},
{
"title": "Мы в Telegram",
"description": "Подпишитесь на канал, чтобы быть в курсе последних новостей.",
"url": "https://t.me/http_teapod",
"icon": "@drawable/ic_custom_telegram",
"icon_space_reserved": "false"
}
],
"Прочее": [
{
"title": "Помочь проекту",
"description": "Вы можете помочь нам с идеями, написанием кода или тестированием.",
"url": "https://git.wowlikon.tech/anixart-mod",
"icon": "@drawable/ic_custom_crown",
"icon_space_reserved": "false"
}
]
}
class Config(PatchConfig):
version: str = Field(" by wowlikon", description="Суффикс версии")
menu: Dict[str, List[Dict[str, str]]] = Field(DEFAULT_MENU, description="Меню")
# Patch
def make_category(ns, name, items): 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)
@@ -23,19 +92,47 @@ def make_category(ns, name, items):
return cat return cat
def apply(config: dict) -> bool: def apply(config: Config, base: Dict[str, Any]) -> bool:
# Добавление кастомных иконок
shutil.copy(
"./resources/ic_custom_crown.xml",
"./decompiled/res/drawable/ic_custom_crown.xml",
)
insert_after_public("warning_error_counter_background", "ic_custom_crown")
shutil.copy(
"./resources/ic_custom_telegram.xml",
"./decompiled/res/drawable/ic_custom_telegram.xml",
)
insert_after_public("warning_error_counter_background", "ic_custom_telegram")
file_path = "./decompiled/res/xml/preference_main.xml" 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 config.menu.items():
for section, items in config["settings_urls"].items(): root.insert(pos, make_category(base["xml_ns"], section, items))
root.insert(pos, make_category(config["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",
"./decompiled/smali_classes2/com/swiftsoft/anixartd/ui/fragment/main/preference/MainPreferenceFragment.smali",
]
for filepath in filepaths:
content = ""
with open(filepath, "r", encoding="utf-8") as file:
for line in file.readlines():
if '"\u0412\u0435\u0440\u0441\u0438\u044f'.encode("unicode_escape").decode() in line:
content += line[:line.rindex('"')] + config.version + line[line.rindex('"'):]
else:
content += line
with open(filepath, "w", encoding="utf-8") as file:
file.write(content)
return True return True
+53
View File
@@ -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"
}
}
"""
priority = 0
# imports
from tqdm import tqdm
from lxml import etree
from typing import Dict, Any
from pydantic import Field
from utils.config import PatchConfig
#Config
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 Config(PatchConfig):
format: Dict[str, str] = Field(DEFAULT_FORMATS, description="Строки для замены в `strings.xml`")
# Patch
def apply(config: Config, 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 config.format:
string.text = config.format[name]
# Сохраняем обратно
tree.write(file_path, encoding="utf-8", xml_declaration=True, pretty_print=True)
return True
-6
View File
@@ -1,6 +0,0 @@
"""Change application icon"""
priority = 0
def apply(config: dict) -> bool:
return False
+32 -8
View File
@@ -1,14 +1,38 @@
"""Change application icon""" """Добавляет пользовательские скорости воспроизведения видео
"custom_speed": {
"enabled": true,
"speeds": [9.0]
}
"""
priority = 0 priority = 0
import struct # imports
from tqdm import tqdm
from typing import Dict, List, Any
from pydantic import Field
from utils.config import PatchConfig
from utils.smali_parser import float_to_hex
from utils.public import (
insert_after_public,
insert_after_id,
)
#Config
class Config(PatchConfig):
speeds: List[float] = Field([9.0], description="Список пользовательских скоростей воспроизведения")
def float_to_hex(f): # Patch
b = struct.pack(">f", f) def apply(config: Config, base: Dict[str, Any]) -> bool:
return b.hex()
def apply(config: dict) -> bool:
assert float_to_hex(1.5) == "0x3fc00000" assert float_to_hex(1.5) == "0x3fc00000"
last = "speed75"
for speed in config.speeds:
insert_after_public(last, f"speed{int(float(speed)*10)}")
insert_after_id(last, f"speed{int(float(speed)*10)}")
last = f"speed{int(float(speed)*10)}"
return False return False
+48
View File
@@ -0,0 +1,48 @@
"""Шаблон патча
Здесь вы можете добавить описание патча, его назначение и другие детали.
Каждый патч должен независеть от других патчей и проверять себя при применении. Он не должен вернуть True, если есть проблемы.
На данный момент каждый патч должен иметь функцию `apply`, которая принимает на вход конфигурацию и возвращает True или False.
И модель `Config`, которая наследуется от `PatchConfig` (поле `enabled` добавлять не нужно).
Это позволяет упростить конфигурирование и проверять типы данных, и делать значения по умолчанию.
При успешном применении патча, функция apply должна вернуть True, иначе False.
Ошибка будет интерпретирована как False. С выводом ошибки в консоль.
Ещё патч должен иметь переменную `priority`, которая указывает приоритет патча, чем выше, тем раньше он будет применен.
Коротко о конфигурации. Она состоит из двух частей `config` (на основе модели `Config` и из файла `configs/название_патча.json`).
И постоянной не типизированной переменной `base` из `config.json` и флага `verbose`.
```
python ./main.py build --verbose
```
В конце docstring может быть дополнительное описание конфигурации патча (основное описание получается из модели `Config`).
Это может быть как короткий фрагмент из названия патча и одной опции "enabled", которая обрабатывается в коде патчера.
"todo_template": {
"enabled": true, // Пример описания тк этот текст просто пример
"example": true // Пример кастомного параметра
}
"""
priority = 0 # Приоритет патча, чем выше, тем раньше он будет применен
# imports
from tqdm import tqdm
from typing import Dict, List, Any
from pydantic import Field
from utils.config import PatchConfig
#Config
class Config(PatchConfig):
example: bool = Field(True, description="Пример кастомного параметра")
# Patch
def apply(config: Config, base: Dict[str, Any]) -> bool: # Анотации типов для удобства, читаемости и поддержки IDE
tqdm.write("Вывод информации через tqdm, чтобы не мешать прогресс-бару")
tqdm.write("Пример включен" if config.example else "Пример отключен")
if base["verbose"]:
tqdm.write("Для вывода подробной и отладочной информации используйте флаг --verbose")
return True
+56
View File
@@ -0,0 +1,56 @@
"""Добавляет всплывающее окно при первом входе
"welcome": {
"enabled": true,
"title": "Anixarty",
"description": "Описание",
"link_text": "МЫ В TELEGRAM",
"link_url": "https://t.me/http_teapod",
"skip_text": "Пропустить",
"title_bg_color": "#FFFFFF"
}
"""
priority = 0
# imports
import os
import shutil
from pydantic import Field
from typing import Dict, Any
from utils.config import PatchConfig
from utils.smali_parser import (
find_and_replace_smali_line,
get_smali_lines,
save_smali_lines
)
#Config
class Config(PatchConfig):
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="Цвет фона заголовка")
# Patch
def apply(config: Config, base: Dict[str, Any]) -> bool:
# Добавление ресурсов окна первого входа
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/"
)
file_path = "./decompiled/smali_classes2/com/swiftsoft/anixartd/ui/activity/MainActivity.smali"
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
+9
View File
@@ -0,0 +1,9 @@
typer[all]>=0.16.0
rich>=14.1.0
httpx>=0.28.1
pydantic>=2.11.7
plumbum>=1.9.0
lxml>=6.0.1
PyYAML>=6.0.2
tqdm>=4.67.1
requests>=2.32.5

Before

Width:  |  Height:  |  Size: 26 MiB

After

Width:  |  Height:  |  Size: 26 MiB

Binary file not shown.
+11
View File
@@ -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>
+12
View File
@@ -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>
+11
View File
@@ -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>
+12
View File
@@ -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>
+27 -10
View File
@@ -1,10 +1,27 @@
/res/layout/monetization_ads_internal_rewarded_close_verification.xml /res/layout/release_info.xml
diff a.txt b.txt <TextView android:textColor="@color/light_md_blue_500" android:id="@id/note" android:background="@drawable/bg_release_note" android:paddingTop="12.0dip" android:paddingBottom="12.0dip" android:layout_width="fill_parent" android:layout_height="wrap_content" android:textIsSelectable="true" android:paddingStart="16.0dip" android:paddingEnd="16.0dip" style="?textAppearanceBodyMedium" />
4c4 </FrameLayout>
< android:background="@drawable/monetization_ads_internal_rewarded_close_verification_button_close_background" <LinearLayout android:orientation="vertical" android:layout_width="fill_parent" android:layout_height="fill_parent">
--- <LinearLayout android:gravity="center_vertical" android:orientation="horizontal" android:id="@id/countryLayout" android:visibility="gone" android:layout_width="fill_parent" android:layout_height="fill_parent">
> android:background="@drawable/draw030e" <androidx.appcompat.widget.AppCompatImageView android:id="@id/iconCountry" android:layout_width="wrap_content" android:layout_height="wrap_content" app:srcCompat="@drawable/ic_flag_japan" />
16c16 <TextView android:textColor="?primaryTextColor" android:textColorLink="?primaryTextColor" android:gravity="center_vertical" android:linksClickable="true" android:id="@id/tvCountry" android:layout_width="fill_parent" android:layout_height="wrap_content" android:textIsSelectable="true" android:layout_marginStart="8.0dip" style="?textAppearanceBodyMedium" />
< android:background="@drawable/monetization_ads_internal_rewarded_close_verification_button_dismiss_background" </LinearLayout>
--- <LinearLayout android:gravity="center_vertical" android:orientation="horizontal" android:id="@id/episodesLayout" android:visibility="gone" android:layout_width="fill_parent" android:layout_height="fill_parent" android:layout_marginTop="2.0dip">
> android:background="@drawable/draw030f" <androidx.appcompat.widget.AppCompatImageView android:layout_width="wrap_content" android:layout_height="wrap_content" app:srcCompat="@drawable/ic_episodes" />
<TextView android:textColor="?primaryTextColor" android:textColorLink="?primaryTextColor" android:gravity="center_vertical" android:linksClickable="true" android:id="@id/tvEpisodes" android:layout_width="fill_parent" android:layout_height="wrap_content" android:textIsSelectable="true" android:layout_marginStart="8.0dip" style="?textAppearanceBodyMedium" />
</LinearLayout>
<LinearLayout android:gravity="center_vertical" android:orientation="horizontal" android:id="@id/scheduleLayout" android:visibility="gone" android:layout_width="fill_parent" android:layout_height="fill_parent" android:layout_marginTop="2.0dip">
<androidx.appcompat.widget.AppCompatImageView android:layout_width="wrap_content" android:layout_height="wrap_content" app:srcCompat="@drawable/ic_schedule" />
<TextView android:textColor="?primaryTextColor" android:textColorLink="?primaryTextColor" android:gravity="center_vertical" android:linksClickable="true" android:id="@id/tvSchedule" android:layout_width="fill_parent" android:layout_height="wrap_content" android:textIsSelectable="true" android:layout_marginStart="8.0dip" style="?textAppearanceBodyMedium" />
</LinearLayout>
<LinearLayout android:gravity="center_vertical" android:orientation="horizontal" android:id="@id/sourceLayout" android:visibility="gone" android:layout_width="fill_parent" android:layout_height="fill_parent" android:layout_marginTop="2.0dip">
<androidx.appcompat.widget.AppCompatImageView android:layout_width="wrap_content" android:layout_height="wrap_content" app:srcCompat="@drawable/ic_source" />
<TextView android:textColor="?primaryTextColor" android:textColorLink="?primaryTextColor" android:gravity="center_vertical" android:linksClickable="true" android:id="@id/tvSource" android:layout_width="fill_parent" android:layout_height="wrap_content" android:lineSpacingExtra="4.0sp" android:textIsSelectable="true" android:layout_marginStart="8.0dip" style="?textAppearanceBodyMedium" />
</LinearLayout>
<LinearLayout android:orientation="horizontal" android:id="@id/studioLayout" android:visibility="gone" android:layout_width="fill_parent" android:layout_height="wrap_content" android:layout_marginTop="2.0dip">
<androidx.appcompat.widget.AppCompatImageView android:layout_width="wrap_content" android:layout_height="wrap_content" app:srcCompat="@drawable/ic_studio" />
<TextView android:textColor="?primaryTextColor" android:textColorLink="?primaryTextColor" android:gravity="center_vertical" android:linksClickable="true" android:id="@id/tvStudio" android:layout_width="fill_parent" android:layout_height="wrap_content" android:lineSpacingExtra="4.0sp" android:textIsSelectable="true" android:layout_marginStart="8.0dip" style="?textAppearanceBodyMedium" />
</LinearLayout>
</LinearLayout>
<TextView android:textColor="?primaryTextColor" android:textColorLink="?primaryTextColor" android:linksClickable="true" android:id="@id/tvGenres" android:visibility="gone" android:layout_width="fill_parent" android:layout_height="wrap_content" android:layout_marginTop="16.0dip" android:layout_marginBottom="2.0dip" android:lineSpacingExtra="4.0sp" android:textIsSelectable="true" style="?textAppearanceBodyMedium" />
<at.blogc.android.views.ExpandableTextView android:textColor="?primaryTextColor" android:id="@id/description" android:layout_width="fill_parent" android:layout_height="wrap_content" android:layout_marginTop="8.0dip" android:layout_marginBottom="8.0dip" android:maxLines="5" android:lineSpacingExtra="4.0sp" android:textIsSelectable="true" app:animation_duration="225" style="?textAppearanceBodyMedium" />
-27
View File
@@ -1,27 +0,0 @@
/res/layout/release_info.xml
<TextView android:textColor="@color/light_md_blue_500" android:id="@id/note" android:background="@drawable/bg_release_note" android:paddingTop="12.0dip" android:paddingBottom="12.0dip" android:layout_width="fill_parent" android:layout_height="wrap_content" android:textIsSelectable="true" android:paddingStart="16.0dip" android:paddingEnd="16.0dip" style="?textAppearanceBodyMedium" />
</FrameLayout>
<LinearLayout android:orientation="vertical" android:layout_width="fill_parent" android:layout_height="fill_parent">
<LinearLayout android:gravity="center_vertical" android:orientation="horizontal" android:id="@id/countryLayout" android:visibility="gone" android:layout_width="fill_parent" android:layout_height="fill_parent">
<androidx.appcompat.widget.AppCompatImageView android:id="@id/iconCountry" android:layout_width="wrap_content" android:layout_height="wrap_content" app:srcCompat="@drawable/ic_flag_japan" />
<TextView android:textColor="?primaryTextColor" android:textColorLink="?primaryTextColor" android:gravity="center_vertical" android:linksClickable="true" android:id="@id/tvCountry" android:layout_width="fill_parent" android:layout_height="wrap_content" android:textIsSelectable="true" android:layout_marginStart="8.0dip" style="?textAppearanceBodyMedium" />
</LinearLayout>
<LinearLayout android:gravity="center_vertical" android:orientation="horizontal" android:id="@id/episodesLayout" android:visibility="gone" android:layout_width="fill_parent" android:layout_height="fill_parent" android:layout_marginTop="2.0dip">
<androidx.appcompat.widget.AppCompatImageView android:layout_width="wrap_content" android:layout_height="wrap_content" app:srcCompat="@drawable/ic_episodes" />
<TextView android:textColor="?primaryTextColor" android:textColorLink="?primaryTextColor" android:gravity="center_vertical" android:linksClickable="true" android:id="@id/tvEpisodes" android:layout_width="fill_parent" android:layout_height="wrap_content" android:textIsSelectable="true" android:layout_marginStart="8.0dip" style="?textAppearanceBodyMedium" />
</LinearLayout>
<LinearLayout android:gravity="center_vertical" android:orientation="horizontal" android:id="@id/scheduleLayout" android:visibility="gone" android:layout_width="fill_parent" android:layout_height="fill_parent" android:layout_marginTop="2.0dip">
<androidx.appcompat.widget.AppCompatImageView android:layout_width="wrap_content" android:layout_height="wrap_content" app:srcCompat="@drawable/ic_schedule" />
<TextView android:textColor="?primaryTextColor" android:textColorLink="?primaryTextColor" android:gravity="center_vertical" android:linksClickable="true" android:id="@id/tvSchedule" android:layout_width="fill_parent" android:layout_height="wrap_content" android:textIsSelectable="true" android:layout_marginStart="8.0dip" style="?textAppearanceBodyMedium" />
</LinearLayout>
<LinearLayout android:gravity="center_vertical" android:orientation="horizontal" android:id="@id/sourceLayout" android:visibility="gone" android:layout_width="fill_parent" android:layout_height="fill_parent" android:layout_marginTop="2.0dip">
<androidx.appcompat.widget.AppCompatImageView android:layout_width="wrap_content" android:layout_height="wrap_content" app:srcCompat="@drawable/ic_source" />
<TextView android:textColor="?primaryTextColor" android:textColorLink="?primaryTextColor" android:gravity="center_vertical" android:linksClickable="true" android:id="@id/tvSource" android:layout_width="fill_parent" android:layout_height="wrap_content" android:lineSpacingExtra="4.0sp" android:textIsSelectable="true" android:layout_marginStart="8.0dip" style="?textAppearanceBodyMedium" />
</LinearLayout>
<LinearLayout android:orientation="horizontal" android:id="@id/studioLayout" android:visibility="gone" android:layout_width="fill_parent" android:layout_height="wrap_content" android:layout_marginTop="2.0dip">
<androidx.appcompat.widget.AppCompatImageView android:layout_width="wrap_content" android:layout_height="wrap_content" app:srcCompat="@drawable/ic_studio" />
<TextView android:textColor="?primaryTextColor" android:textColorLink="?primaryTextColor" android:gravity="center_vertical" android:linksClickable="true" android:id="@id/tvStudio" android:layout_width="fill_parent" android:layout_height="wrap_content" android:lineSpacingExtra="4.0sp" android:textIsSelectable="true" android:layout_marginStart="8.0dip" style="?textAppearanceBodyMedium" />
</LinearLayout>
</LinearLayout>
<TextView android:textColor="?primaryTextColor" android:textColorLink="?primaryTextColor" android:linksClickable="true" android:id="@id/tvGenres" android:visibility="gone" android:layout_width="fill_parent" android:layout_height="wrap_content" android:layout_marginTop="16.0dip" android:layout_marginBottom="2.0dip" android:lineSpacingExtra="4.0sp" android:textIsSelectable="true" style="?textAppearanceBodyMedium" />
<at.blogc.android.views.ExpandableTextView android:textColor="?primaryTextColor" android:id="@id/description" android:layout_width="fill_parent" android:layout_height="wrap_content" android:layout_marginTop="8.0dip" android:layout_marginBottom="8.0dip" android:maxLines="5" android:lineSpacingExtra="4.0sp" android:textIsSelectable="true" app:animation_duration="225" style="?textAppearanceBodyMedium" />
-2
View File
@@ -1,2 +0,0 @@
/res/menu/bottom.xml
replace lines
-1
View File
@@ -1 +0,0 @@
<color name="ic_launcher_background">#ff000000</color>
+30
View File
@@ -0,0 +1,30 @@
from pydantic import BaseModel, Field, ValidationError
from rich.console import Console
from typing import Dict, Any
from pathlib import Path
import typer
class ToolsConfig(BaseModel):
apktool_jar_url: str
apktool_wrapper_url: str
class Config(BaseModel):
tools: ToolsConfig
base: Dict[str, Any]
class PatchConfig(BaseModel):
enabled: bool = Field(True, description="Включить или отключить патч")
def load_config(console: Console) -> Config:
try:
return Config.model_validate_json(Path("config.json").read_text())
except FileNotFoundError:
console.print("[red]Файл config.json не найден")
raise typer.Exit(1)
except ValidationError as e:
console.print("[red]Ошибка валидации config.json:", e)
raise typer.Exit(1)
-6
View File
@@ -1,6 +0,0 @@
import struct
def float_to_hex(f):
b = struct.pack(">f", f)
return b.hex()
+13 -5
View File
@@ -1,8 +1,9 @@
from lxml import etree from typing_extensions import Optional
from copy import deepcopy from copy import deepcopy
from lxml import etree
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 +20,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 +50,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 +65,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 +91,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 +107,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)
+33 -14
View File
@@ -1,27 +1,41 @@
import struct
def get_smali_lines(file: str) -> list[str]: def get_smali_lines(file: str) -> list[str]:
lines = [] lines = []
with open(file, "r", encoding="utf-8") as smali: with open(file, "r", encoding="utf-8") as smali:
lines = smali.readlines() lines = smali.readlines()
return lines return lines
def save_smali_lines(file: str, lines: list[str]) -> None:
with open(file, "w", encoding="utf-8") as f:
f.writelines(lines)
def find_smali_method_start(lines: list[str], index: int) -> int: def find_smali_method_start(lines: list[str], index: int) -> int:
while True: while True:
index -= 1 index -= 1
if lines[index].find(".method") >= 0: if lines[index].find(".method") >= 0:
return index return index
def find_smali_method_end(lines: list[str], index: int) -> int: def find_smali_method_end(lines: list[str], index: int) -> int:
while True: while True:
index += 1 index += 1
if lines[index].find(".end method") >= 0: if lines[index].find(".end method") >= 0:
return index return index
def debug_print_smali_method(lines: list[str], start: int, end: int) -> None: def debug_print_smali_method(lines: list[str], start: int, end: int) -> None:
while start != (end + 1): while start != (end + 1):
print(start, lines[start]) print(start, lines[start])
start += 1 start += 1
def replace_smali_method_body(lines: list[str], start: int, end: int, new_lines: list[str]) -> list[str]:
def replace_smali_method_body(
lines: list[str], start: int, end: int, new_lines: list[str]
) -> list[str]:
new_content = [] new_content = []
index = 0 index = 0
skip = end - start - 1 skip = end - start - 1
@@ -38,21 +52,26 @@ def replace_smali_method_body(lines: list[str], start: int, end: int, new_lines:
new_content.append(lines[index]) new_content.append(lines[index])
index += 1 index += 1
return new_content return new_content
# example i guess
# if __name__ == "__main__":
# lines = get_smali_lines("./decompiled/smali_classes2/com/radiquum/anixart/Prefs.smali")
# for index, line in enumerate(lines): def find_and_replace_smali_line(
# if line.find("IS_SPONSOR") >= 0: lines: list[str], search: str, replace: str
# method_start = find_smali_method_start(lines, index) ) -> list[str]:
# method_end = find_smali_method_end(lines, index) for index, line in enumerate(lines):
# new_content = replace_smali_method_body(lines, method_start, method_end, c) if line.find(search) >= 0:
lines[index] = lines[index].replace(search, replace)
return lines
# with open("./help/Prefs_orig.smali", "w", encoding="utf-8") as file:
# file.writelines(lines)
# with open("./help/Prefs_modified.smali", "w", encoding="utf-8") as file:
# file.writelines(new_content)
def float_to_hex(f):
b = struct.pack(">f", f)
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)
+47
View File
@@ -0,0 +1,47 @@
from plumbum import local, ProcessExecutionError
from rich.progress import Progress
from rich.console import Console
from pathlib import Path
from typing import List
import httpx
import typer
TOOLS = Path("tools")
ORIGINAL = Path("original")
MODIFIED = Path("modified")
DECOMPILED = Path("decompiled")
PATCHES = Path("patches")
CONFIGS = Path("configs")
def ensure_dirs():
for d in [TOOLS, ORIGINAL, MODIFIED, DECOMPILED, PATCHES, CONFIGS]:
d.mkdir(exist_ok=True)
def run(console: Console, cmd: List[str], hide_output=True):
prog = local[cmd[0]][cmd[1:]]
try:
prog() if hide_output else prog & FG # type: ignore [reportUndefinedVariable]
except ProcessExecutionError as e:
console.print(f"[red]Ошибка при выполнении команды: {' '.join(cmd)}")
console.print(e.stderr)
raise typer.Exit(1)
def download(console: Console, url: str, dest: Path):
console.print(f"[cyan]Скачивание {url}{dest.name}")
with httpx.Client(follow_redirects=True, timeout=60.0) as client:
with client.stream("GET", url) as response:
response.raise_for_status()
total = int(response.headers.get("Content-Length", 0))
dest.parent.mkdir(parents=True, exist_ok=True)
with open(dest, "wb") as f, Progress(console=console) as progress:
task = progress.add_task("Загрузка", total=total if total else None)
for chunk in response.iter_bytes(chunk_size=8192):
f.write(chunk)
progress.update(task, advance=len(chunk))