Compare commits
39 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 2dceb75262 | |||
| 70337ee3ec | |||
| ec047cd3a5 | |||
| 2fe61c1445 | |||
| 28c60aa7a3 | |||
| b646dbf6fe | |||
| f46425b169 | |||
| 19e1ce2f45 | |||
| fbc8b3e017 | |||
| 5ba590cc31 | |||
| 0a4aa544a2 | |||
| 40f9cf0307 | |||
| 8b8ca63bb1 | |||
| 670c53ba69 | |||
| 5ff882a8d5 | |||
| 66336f3a5c | |||
| 85aef3d997 | |||
| 41399eca2c | |||
| 137c939e1d | |||
| 18ad769d33 | |||
| 5b39aec161 | |||
| c1bb2f8845 | |||
| 630ab0d094 | |||
| 0f9f6f2932 | |||
| a09181fe5a | |||
| 77694ec4b7 | |||
| b8ab508dfb | |||
| debf561cf9 | |||
| f7e186d5db | |||
| 24a8a1d4d3 | |||
| 550427338a | |||
| ac241e1189 | |||
| c22ef507ba | |||
| 9da9e98547 | |||
| 48953a857b | |||
|
871ec11f7e
|
|||
| 9453b3b50b | |||
| 48ea732d77 | |||
| e2614990df |
@@ -1,10 +1,10 @@
|
|||||||
name: Build mod
|
name: Сборка мода
|
||||||
|
|
||||||
on:
|
on:
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
#schedule: # раз в 36 часов
|
push:
|
||||||
# - cron: "0 0 */3 * *" # каждые 3 дня в 00:00
|
tags:
|
||||||
# - cron: "0 12 */3 * *" # каждые 3 дня в 12:00
|
- 'v*'
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
@@ -13,18 +13,25 @@ jobs:
|
|||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v3
|
||||||
|
|
||||||
- name: Download APK
|
- name: Скачивание APK
|
||||||
run: |
|
run: |
|
||||||
curl -L -o app.apk "https://mirror-dl.anixart-app.com/anixart-beta.apk"
|
curl -L -o app.apk "https://mirror-dl.anixart-app.com/anixart-beta.apk"
|
||||||
|
|
||||||
- name: Ensure aapt is installed
|
- name: Проверка наличия aapt
|
||||||
run: |
|
run: |
|
||||||
if ! command -v aapt &> /dev/null; then
|
if ! command -v aapt &> /dev/null; then
|
||||||
echo "aapt не найден, устанавливаем..."
|
echo "aapt не найден, устанавливаем..."
|
||||||
sudo apt-get update && sudo apt-get install -y --no-install-recommends android-sdk-build-tools
|
sudo apt-get update && sudo apt-get install -y --no-install-recommends android-sdk-build-tools
|
||||||
fi
|
fi
|
||||||
|
|
||||||
- name: 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 +40,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.md
|
||||||
id: get_title
|
id: get_title
|
||||||
run: |
|
run: |
|
||||||
TITLE=$(head -n 1 modified/report.log)
|
TITLE=$(head -n 1 modified/report.md)
|
||||||
echo "title=${TITLE}" >> $GITHUB_OUTPUT
|
echo "title=${TITLE}" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
- name: Setup go
|
- name: Чтение body из report.md
|
||||||
|
id: get_body
|
||||||
|
run: |
|
||||||
|
BODY=$(tail -n +3 modified/report.md)
|
||||||
|
echo "body<<EOF" >> $GITHUB_OUTPUT
|
||||||
|
echo "$BODY" >> $GITHUB_OUTPUT
|
||||||
|
echo "EOF" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
|
- name: Установка go
|
||||||
if: steps.build.outputs.BUILD_EXIT == '0'
|
if: steps.build.outputs.BUILD_EXIT == '0'
|
||||||
uses: actions/setup-go@v4
|
uses: actions/setup-go@v4
|
||||||
with:
|
with:
|
||||||
go-version: '>=1.20'
|
go-version: '>=1.20'
|
||||||
|
|
||||||
- name: Make release
|
- name: Создание релиза
|
||||||
if: steps.build.outputs.BUILD_EXIT == '0'
|
if: steps.build.outputs.BUILD_EXIT == '0'
|
||||||
uses: https://gitea.com/actions/release-action@main
|
uses: https://gitea.com/actions/release-action@main
|
||||||
with:
|
with:
|
||||||
title: ${{ steps.get_title.outputs.title }}
|
title: ${{ steps.get_title.outputs.title }}
|
||||||
body_path: modified/report.log
|
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
|
||||||
@@ -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)]
|
||||||
|
|||||||
+5
-56
@@ -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",
|
"xml_ns": {
|
||||||
"theme": {
|
"android": "http://schemas.android.com/apk/res/android",
|
||||||
"colors": {
|
"app": "http://schemas.android.com/apk/res-auto"
|
||||||
"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": {
|
|
||||||
"android": "http://schemas.android.com/apk/res/android",
|
|
||||||
"app": "http://schemas.android.com/apk/res-auto"
|
|
||||||
},
|
|
||||||
"speeds": [9.0]
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"enabled": false,
|
||||||
|
"server": "https://anixarty.0x174.su/patch"
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
{
|
||||||
|
"enabled": true,
|
||||||
|
"logo": {
|
||||||
|
"gradient": {
|
||||||
|
"angle": 0.0,
|
||||||
|
"start_color": "#ffccff00",
|
||||||
|
"end_color": "#ffcccc00"
|
||||||
|
},
|
||||||
|
"ears_color": "#ffd0d0d0"
|
||||||
|
},
|
||||||
|
"colors": {
|
||||||
|
"primary": "#ccff00",
|
||||||
|
"secondary": "#ffcccc00",
|
||||||
|
"background": "#ffffff",
|
||||||
|
"text": "#000000"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"enabled": true,
|
||||||
|
"replace": true,
|
||||||
|
"custom_icons": true,
|
||||||
|
"icon_size": "18.0dip"
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
{
|
||||||
|
"enabled": true,
|
||||||
|
"remove_language_files": true,
|
||||||
|
"remove_AI_voiceover": true,
|
||||||
|
"remove_debug_lines": false,
|
||||||
|
"remove_drawable_files": false,
|
||||||
|
"remove_unknown_files": true,
|
||||||
|
"remove_unknown_files_keep_dirs": [
|
||||||
|
"META-INF",
|
||||||
|
"kotlin"
|
||||||
|
],
|
||||||
|
"compress_png_files": true
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"enabled": true
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"enabled": true
|
||||||
|
}
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"enabled": true,
|
||||||
|
"package_name": "com.wowlikon.anixart"
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"enabled": true,
|
||||||
|
"items": [
|
||||||
|
"home",
|
||||||
|
"discover",
|
||||||
|
"feed",
|
||||||
|
"bookmarks",
|
||||||
|
"profile"
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"enabled": true
|
||||||
|
}
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
{
|
||||||
|
"enabled": true,
|
||||||
|
"version": " by wowlikon",
|
||||||
|
"menu": {
|
||||||
|
"Мы в социальных сетях": [
|
||||||
|
{
|
||||||
|
"title": "wowlikon",
|
||||||
|
"description": "Разработчик",
|
||||||
|
"url": "https://t.me/wowlikon",
|
||||||
|
"icon": "@drawable/ic_custom_telegram",
|
||||||
|
"icon_space_reserved": "false"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Kentai Radiquum",
|
||||||
|
"description": "Разработчик",
|
||||||
|
"url": "https://t.me/radiquum",
|
||||||
|
"icon": "@drawable/ic_custom_telegram",
|
||||||
|
"icon_space_reserved": "false"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Мы в Telegram",
|
||||||
|
"description": "Подпишитесь на канал, чтобы быть в курсе последних новостей.",
|
||||||
|
"url": "https://t.me/http_teapod",
|
||||||
|
"icon": "@drawable/ic_custom_telegram",
|
||||||
|
"icon_space_reserved": "false"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"Прочее": [
|
||||||
|
{
|
||||||
|
"title": "Помочь проекту",
|
||||||
|
"description": "Вы можете помочь нам с идеями, написанием кода или тестированием.",
|
||||||
|
"url": "https://git.wowlikon.tech/anixart-mod",
|
||||||
|
"icon": "@drawable/ic_custom_crown",
|
||||||
|
"icon_space_reserved": "false"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"enabled": true,
|
||||||
|
"format": {
|
||||||
|
"share_channel_text": "Канал: «%1$s»\n%2$schannel/%3$d",
|
||||||
|
"share_collection_text": "Коллекция: «%1$s»\n%2$scollection/%3$d",
|
||||||
|
"share_profile_text": "Профиль пользователя «%1$s»\n%2$sprofile/%3$d",
|
||||||
|
"share_release_text": "Релиз: «%1$s»\n%2$srelease/%3$d"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
{
|
||||||
|
"enabled": true,
|
||||||
|
"title": "Anixarty",
|
||||||
|
"title_color": "#FF252525",
|
||||||
|
"title_bg_color": "#FFCFF04D",
|
||||||
|
"body_bg_color": "#FF252525",
|
||||||
|
"description": "Описание",
|
||||||
|
"description_color": "#FFFFFFFF",
|
||||||
|
"skip_text": "Пропустить",
|
||||||
|
"skip_color": "#FFFFFFFF",
|
||||||
|
"link_text": "МЫ В TELEGRAM",
|
||||||
|
"link_color": "#FFCFF04D",
|
||||||
|
"link_url": "https://t.me/http_teapod"
|
||||||
|
}
|
||||||
@@ -1,230 +1,474 @@
|
|||||||
import os
|
__version__ = "1.0.0"
|
||||||
import sys
|
import shutil
|
||||||
import json
|
from functools import wraps
|
||||||
import yaml
|
from pathlib import Path
|
||||||
import requests
|
from typing import Any, Dict, List
|
||||||
import argparse
|
|
||||||
import colorama
|
|
||||||
import importlib
|
|
||||||
import traceback
|
|
||||||
import subprocess
|
|
||||||
from tqdm import tqdm
|
|
||||||
|
|
||||||
def init() -> dict:
|
import typer
|
||||||
for directory in ["original", "modified", "patches", "tools", "decompiled"]:
|
from plumbum import ProcessExecutionError, local
|
||||||
if not os.path.exists(directory):
|
from rich.console import Console
|
||||||
os.makedirs(directory)
|
from rich.progress import Progress
|
||||||
|
from rich.prompt import Prompt
|
||||||
|
from rich.table import Table
|
||||||
|
|
||||||
with open("./config.json", "r") as config_file:
|
from utils.apk import APKMeta, APKProcessor
|
||||||
conf = json.load(config_file)
|
from utils.config import Config, PatchTemplate, load_config
|
||||||
|
from utils.info import print_model_fields, print_model_table
|
||||||
|
from utils.patch_manager import (BuildError, ConfigError, PatcherError,
|
||||||
|
PatchManager, handle_errors)
|
||||||
|
from utils.tools import (CONFIGS, DECOMPILED, MODIFIED, ORIGINAL, PATCHES,
|
||||||
|
TOOLS, download, ensure_dirs, run, select_apk)
|
||||||
|
|
||||||
if not os.path.exists("./tools/apktool.jar"):
|
console = Console()
|
||||||
|
app = typer.Typer(
|
||||||
|
name="anixarty-patcher",
|
||||||
|
help="Инструмент для модификации Anixarty APK",
|
||||||
|
add_completion=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
|
||||||
|
def generate_report(
|
||||||
|
apk_path: Path,
|
||||||
|
meta: APKMeta,
|
||||||
|
patches: List[PatchTemplate],
|
||||||
|
manager: PatchManager,
|
||||||
|
) -> None:
|
||||||
|
"""Генерирует отчёт о сборке в формате Markdown"""
|
||||||
|
report_path = MODIFIED / "report.md"
|
||||||
|
|
||||||
|
applied_count = sum(1 for p in patches if p.applied)
|
||||||
|
applied_patches = [p for p in patches if p.applied]
|
||||||
|
failed_patches = [p for p in patches if not p.applied]
|
||||||
|
|
||||||
|
applied_patches.sort(key=lambda p: p.priority, reverse=True)
|
||||||
|
failed_patches.sort(key=lambda p: p.priority, reverse=True)
|
||||||
|
|
||||||
|
def get_patch_info(patch: PatchTemplate) -> Dict[str, str]:
|
||||||
|
"""Получает описание и автора патча из модуля"""
|
||||||
|
info = {"doc": "", "author": "-"}
|
||||||
try:
|
try:
|
||||||
print("Скачивание Apktool...")
|
patch_module = manager.load_patch_module(patch.name)
|
||||||
jar_response = requests.get(conf["tools"]["apktool_jar_url"], stream=True)
|
doc = patch_module.__doc__
|
||||||
jar_path = "tools/apktool.jar"
|
if doc:
|
||||||
with open(jar_path, "wb") as f:
|
info["doc"] = doc.strip().split("\n")[0]
|
||||||
for chunk in jar_response.iter_content(chunk_size=8192):
|
author = getattr(patch_module, "__author__", "")
|
||||||
f.write(chunk)
|
if author:
|
||||||
|
info["author"] = f"`{author}`"
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return info
|
||||||
|
|
||||||
wrapper_response = requests.get(conf["tools"]["apktool_wrapper_url"])
|
lines = []
|
||||||
wrapper_path = "tools/apktool"
|
lines.append(f"Anixarty {meta.version_name} (build {meta.version_code})")
|
||||||
with open(wrapper_path, "w") as f:
|
lines.append("")
|
||||||
f.write(wrapper_response.text)
|
|
||||||
os.chmod(wrapper_path, 0o755)
|
|
||||||
|
|
||||||
except Exception as e:
|
lines.append("## 📦 Информация о сборке")
|
||||||
print(f"Ошибка при скачивании Apktool: {e}")
|
lines.append("")
|
||||||
exit(1)
|
lines.append("| Параметр | Значение |")
|
||||||
|
lines.append("|----------|----------|")
|
||||||
|
lines.append(f"| Версия | `{meta.version_name}` |")
|
||||||
|
lines.append(f"| Код версии | `{meta.version_code}` |")
|
||||||
|
lines.append(f"| Пакет | `{meta.package}` |")
|
||||||
|
lines.append(f"| Файл | `{apk_path.name}` |")
|
||||||
|
lines.append(f"| Дата сборки | {datetime.now().strftime('%Y-%m-%d %H:%M:%S')} |")
|
||||||
|
lines.append("")
|
||||||
|
|
||||||
try:
|
lines.append("## 🔧 Применённые патчи")
|
||||||
result = subprocess.run(
|
lines.append("")
|
||||||
["java", "-version"], capture_output=True, text=True, check=True
|
|
||||||
|
if applied_patches:
|
||||||
|
lines.append(
|
||||||
|
f"> ✅ Успешно применено: **{applied_count}** из **{len(patches)}**"
|
||||||
)
|
)
|
||||||
|
lines.append("")
|
||||||
|
lines.append("| Патч | Приоритет | Автор | Описание |")
|
||||||
|
lines.append("|------|:---------:|-------|----------|")
|
||||||
|
|
||||||
version_line = result.stderr.splitlines()[0]
|
for p in applied_patches:
|
||||||
if "1.8" in version_line or any(f"{i}." in version_line for i in range(9, 100)):
|
info = get_patch_info(p)
|
||||||
print("Java 8 или более поздняя версия установлена.")
|
lines.append(
|
||||||
else:
|
f"| ✅ `{p.name}` | {p.priority} | {info['author']} | {info['doc']} |"
|
||||||
print("Java 8 или более поздняя версия не установлена.")
|
)
|
||||||
sys.exit(1)
|
else:
|
||||||
except subprocess.CalledProcessError:
|
lines.append("> ⚠️ Нет применённых патчей")
|
||||||
print("Java не установлена. Установите Java 8 или более позднюю версию.")
|
|
||||||
exit(1)
|
|
||||||
|
|
||||||
return conf
|
lines.append("")
|
||||||
|
|
||||||
def select_apk() -> str:
|
if failed_patches:
|
||||||
apks = []
|
lines.append("## ❌ Ошибки")
|
||||||
for file in os.listdir("original"):
|
lines.append("")
|
||||||
if file.endswith(".apk") and os.path.isfile(os.path.join("original", file)):
|
lines.append("| Патч | Приоритет | Автор | Описание |")
|
||||||
apks.append(file)
|
lines.append("|------|:---------:|-------|----------|")
|
||||||
|
|
||||||
if not apks:
|
for p in failed_patches:
|
||||||
print("Нет файлов .apk в текущей директории")
|
info = get_patch_info(p)
|
||||||
sys.exit(1)
|
lines.append(
|
||||||
|
f"| ❌ `{p.name}` | {p.priority} | {info['author']} | {info['doc']} |"
|
||||||
|
)
|
||||||
|
|
||||||
if len(apks) == 1:
|
lines.append("")
|
||||||
apk = apks[0]
|
|
||||||
print(f"Выбран файл {apk}")
|
|
||||||
return apk
|
|
||||||
|
|
||||||
while True:
|
lines.append("---")
|
||||||
print("Выберете файл для модификации")
|
lines.append("")
|
||||||
for index, apk in enumerate(apks):
|
lines.append(
|
||||||
print(f"{index + 1}. {apk}")
|
"*Собрано с помощью [anixarty-patcher](https://git.0x174.su/anixart-mod/patcher)*"
|
||||||
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):
|
|
||||||
print("Декомпилируем apk...")
|
|
||||||
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)
|
|
||||||
|
|
||||||
|
|
||||||
class Patch:
|
|
||||||
def __init__(self, name, pkg):
|
|
||||||
self.name = name
|
|
||||||
self.package = pkg
|
|
||||||
self.applied = False
|
|
||||||
try:
|
|
||||||
self.priority = pkg.priority
|
|
||||||
except AttributeError:
|
|
||||||
self.priority = 0
|
|
||||||
|
|
||||||
def apply(self, conf: dict) -> bool:
|
|
||||||
try:
|
|
||||||
self.applied = self.package.apply(conf)
|
|
||||||
return True
|
|
||||||
except Exception as e:
|
|
||||||
print(f"Ошибка при применении патча {self.name}: {e}")
|
|
||||||
print(type(e), e.args)
|
|
||||||
traceback.print_exc()
|
|
||||||
return False
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
parser = argparse.ArgumentParser(
|
|
||||||
description="Автоматический патчер anixart"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
parser.add_argument("-v", "--verbose",
|
report_path.write_text("\n".join(lines), encoding="utf-8")
|
||||||
action="store_true",
|
console.print(f"[dim]Отчёт сохранён: {report_path}[/dim]")
|
||||||
help="Выводить подробные сообщения")
|
|
||||||
|
|
||||||
parser.add_argument("-f", "--force",
|
|
||||||
action="store_true",
|
|
||||||
help="Принудительно собрать APK")
|
|
||||||
|
|
||||||
args = parser.parse_args()
|
# ========================= COMMANDS =========================
|
||||||
|
@app.command()
|
||||||
|
@handle_errors
|
||||||
|
def init():
|
||||||
|
"""Инициализация: создание директорий и скачивание инструментов"""
|
||||||
|
ensure_dirs()
|
||||||
|
|
||||||
conf = init()
|
conf = load_config(console)
|
||||||
apk = select_apk()
|
|
||||||
patch = decompile_apk(apk)
|
|
||||||
|
|
||||||
if args.verbose: conf["verbose"] = True
|
# Проверка Java
|
||||||
|
console.print("[cyan]Проверка Java...")
|
||||||
|
try:
|
||||||
|
local["java"]["-version"].run(retcode=None)
|
||||||
|
console.print("[green]✔ Java найдена")
|
||||||
|
except ProcessExecutionError:
|
||||||
|
raise PatcherError("Java не установлена. Установите JDK 11+")
|
||||||
|
|
||||||
patches = []
|
# Скачивание apktool
|
||||||
for filename in os.listdir("patches/"):
|
apktool_jar = TOOLS / "apktool.jar"
|
||||||
if filename.endswith(".py") and filename != "__init__.py" and not filename.startswith("todo_"):
|
if not apktool_jar.exists():
|
||||||
module_name = filename[:-3]
|
download(console, conf.tools.apktool_jar_url, apktool_jar)
|
||||||
module = importlib.import_module(f"patches.{module_name}")
|
|
||||||
patches.append(Patch(module_name, module))
|
|
||||||
|
|
||||||
patches.sort(key=lambda x: x.package.priority, reverse=True)
|
|
||||||
|
|
||||||
for patch in tqdm(patches, colour="green", desc="Применение патчей"):
|
|
||||||
tqdm.write(f"Применение патча: {patch.name}")
|
|
||||||
patch.apply(conf)
|
|
||||||
|
|
||||||
statuses = {}
|
|
||||||
for patch in patches:
|
|
||||||
statuses[patch.name] = patch.applied
|
|
||||||
marker = colorama.Fore.GREEN + "✔" if patch.applied else colorama.Fore.RED + "✘"
|
|
||||||
print(f"{marker}{colorama.Style.RESET_ALL} {patch.name}")
|
|
||||||
|
|
||||||
if all(statuses.values()):
|
|
||||||
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:
|
|
||||||
print(colorama.Fore.RED + "Операция отменена" + colorama.Style.RESET_ALL)
|
|
||||||
else:
|
else:
|
||||||
print(f"{colorama.Fore.RED}Ни один патч не был успешно применен{colorama.Style.RESET_ALL}")
|
console.print(f"[dim]✔ {apktool_jar.name} уже существует[/dim]")
|
||||||
sys.exit(1)
|
|
||||||
|
# Скачивание apktool wrapper
|
||||||
|
apktool_wrapper = TOOLS / "apktool"
|
||||||
|
if not apktool_wrapper.exists():
|
||||||
|
download(console, conf.tools.apktool_wrapper_url, apktool_wrapper)
|
||||||
|
apktool_wrapper.chmod(0o755)
|
||||||
|
else:
|
||||||
|
console.print(f"[dim]✔ {apktool_wrapper.name} уже существует[/dim]")
|
||||||
|
|
||||||
|
# Проверка zipalign и apksigner
|
||||||
|
for tool in ["zipalign", "apksigner"]:
|
||||||
|
try:
|
||||||
|
local[tool]["--version"].run(retcode=None)
|
||||||
|
console.print(f"[green]✔ {tool} найден")
|
||||||
|
except Exception:
|
||||||
|
console.print(f"[yellow]⚠ {tool} не найден в PATH")
|
||||||
|
|
||||||
|
# Проверка keystore
|
||||||
|
if not Path("keystore.jks").exists():
|
||||||
|
console.print("[yellow]⚠ keystore.jks не найден. Создайте его командой:")
|
||||||
|
console.print(
|
||||||
|
"[dim] keytool -genkey -v -keystore keystore.jks -keyalg RSA "
|
||||||
|
"-keysize 2048 -validity 10000 -alias key[/dim]"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Инициализация конфигов патчей
|
||||||
|
console.print("\n[cyan]Инициализация конфигураций патчей...")
|
||||||
|
manager = PatchManager(console)
|
||||||
|
|
||||||
|
for name in manager.discover_patches():
|
||||||
|
patch = manager.load_patch(name)
|
||||||
|
config_path = CONFIGS / f"{name}.json"
|
||||||
|
if not config_path.exists():
|
||||||
|
patch.save_config()
|
||||||
|
console.print(f" [green]✔ {name}.json создан")
|
||||||
|
else:
|
||||||
|
console.print(f" [dim]✔ {name}.json существует[/dim]")
|
||||||
|
|
||||||
|
console.print("\n[green]✔ Инициализация завершена")
|
||||||
|
|
||||||
|
|
||||||
|
@app.command("list")
|
||||||
|
@handle_errors
|
||||||
|
def list_patches():
|
||||||
|
"""Показать список всех патчей"""
|
||||||
|
manager = PatchManager(console)
|
||||||
|
all_patches = manager.discover_all()
|
||||||
|
|
||||||
|
table = Table(title="Доступные патчи")
|
||||||
|
table.add_column("Приоритет", justify="center", style="cyan")
|
||||||
|
table.add_column("Название", style="yellow")
|
||||||
|
table.add_column("Статус", justify="center")
|
||||||
|
table.add_column("Автор", style="magenta")
|
||||||
|
table.add_column("Версия", style="yellow")
|
||||||
|
table.add_column("Описание")
|
||||||
|
|
||||||
|
patch_rows = []
|
||||||
|
|
||||||
|
for name in all_patches["ready"]:
|
||||||
|
try:
|
||||||
|
patch = manager.load_patch(name)
|
||||||
|
status = "[green]✔ вкл[/green]" if patch.enabled else "[red]✘ выкл[/red]"
|
||||||
|
patch_class = manager.load_patch_class(name)
|
||||||
|
priority = getattr(patch_class, "priority", 0)
|
||||||
|
patch_module = manager.load_patch_module(name)
|
||||||
|
author = getattr(patch_module, "__author__", "")
|
||||||
|
version = getattr(patch_module, "__version__", "")
|
||||||
|
description = (patch_module.__doc__ or "").strip().split("\n")[0]
|
||||||
|
patch_rows.append(
|
||||||
|
(patch.priority, name, status, author, version, description)
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
raise e
|
||||||
|
patch_rows.append((0, name, "[red]⚠ ошибка[/red]", "", "", str(e)[:40]))
|
||||||
|
|
||||||
|
for name in all_patches["todo"]:
|
||||||
|
try:
|
||||||
|
patch_class = manager.load_patch_class(name)
|
||||||
|
priority = getattr(patch_class, "priority", 0)
|
||||||
|
patch_module = manager.load_patch_module(name)
|
||||||
|
author = getattr(patch_module, "__author__", "")
|
||||||
|
version = getattr(patch_module, "__version__", "")
|
||||||
|
description = (patch_module.__doc__ or "").strip().split("\n")[0]
|
||||||
|
patch_rows.append(
|
||||||
|
(
|
||||||
|
priority,
|
||||||
|
name,
|
||||||
|
"[yellow]⚠ todo[/yellow]",
|
||||||
|
author,
|
||||||
|
version,
|
||||||
|
description,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
except Exception:
|
||||||
|
patch_rows.append((0, name, "[yellow]⚠ todo[/yellow]", "", "", ""))
|
||||||
|
|
||||||
|
patch_rows.sort(key=lambda x: x[0], reverse=True)
|
||||||
|
|
||||||
|
for priority, name, status, author, version, desc in patch_rows:
|
||||||
|
table.add_row(str(priority), name, status, author, version, desc[:50])
|
||||||
|
|
||||||
|
console.print(table)
|
||||||
|
|
||||||
|
|
||||||
|
@app.command()
|
||||||
|
@handle_errors
|
||||||
|
def info(
|
||||||
|
patch_name: str = typer.Argument(..., help="Имя патча"),
|
||||||
|
tree: bool = typer.Option(False, "--tree", "-t", help="Древовидный вывод полей"),
|
||||||
|
):
|
||||||
|
"""Показать подробную информацию о патче"""
|
||||||
|
manager = PatchManager(console)
|
||||||
|
|
||||||
|
all_patches = manager.discover_all()
|
||||||
|
all_names = all_patches["ready"] + all_patches["todo"]
|
||||||
|
|
||||||
|
if patch_name not in all_names:
|
||||||
|
raise PatcherError(f"Патч '{patch_name}' не найден")
|
||||||
|
|
||||||
|
patch_class = manager.load_patch_class(patch_name)
|
||||||
|
|
||||||
|
console.print(f"\n[bold cyan]Патч: {patch_name}[/bold cyan]")
|
||||||
|
console.print("-" * 50)
|
||||||
|
|
||||||
|
if patch_class.__doc__:
|
||||||
|
console.print(f"[white]{patch_class.__doc__.strip()}[/white]\n")
|
||||||
|
|
||||||
|
is_todo = patch_name in all_patches["todo"]
|
||||||
|
if is_todo:
|
||||||
|
console.print("[yellow]Статус: в разработке[/yellow]\n")
|
||||||
|
else:
|
||||||
|
patch = manager.load_patch(patch_name)
|
||||||
|
status = "[green]включён[/green]" if patch.enabled else "[red]выключен[/red]"
|
||||||
|
console.print(f"Статус: {status}")
|
||||||
|
console.print(f"Приоритет: [cyan]{patch.priority}[/cyan]\n")
|
||||||
|
|
||||||
|
console.print("[bold]Поля конфигурации:[/bold]")
|
||||||
|
|
||||||
|
if tree:
|
||||||
|
print_model_fields(console, patch_class)
|
||||||
|
else:
|
||||||
|
table = print_model_table(console, patch_class)
|
||||||
|
console.print(table)
|
||||||
|
|
||||||
|
table = Table(show_header=True)
|
||||||
|
table.add_column("Поле", style="yellow")
|
||||||
|
table.add_column("Тип", style="cyan")
|
||||||
|
table.add_column("По умолчанию")
|
||||||
|
table.add_column("Описание")
|
||||||
|
|
||||||
|
for field_name, field_info in patch_class.model_fields.items():
|
||||||
|
field_type = getattr(
|
||||||
|
field_info.annotation, "__name__", str(field_info.annotation)
|
||||||
|
)
|
||||||
|
default = str(field_info.default) if field_info.default is not None else "-"
|
||||||
|
description = field_info.description or ""
|
||||||
|
table.add_row(field_name, field_type, default, description)
|
||||||
|
|
||||||
|
console.print(table)
|
||||||
|
|
||||||
|
if not is_todo:
|
||||||
|
config_path = CONFIGS / f"{patch_name}.json"
|
||||||
|
if config_path.exists():
|
||||||
|
console.print(f"\n[bold]Текущая конфигурация[/bold] ({config_path}):")
|
||||||
|
console.print(config_path.read_text())
|
||||||
|
|
||||||
|
|
||||||
|
@app.command()
|
||||||
|
@handle_errors
|
||||||
|
def enable(patch_name: str = typer.Argument(..., help="Имя патча")):
|
||||||
|
"""Включить патч"""
|
||||||
|
manager = PatchManager(console)
|
||||||
|
|
||||||
|
if patch_name not in manager.discover_patches():
|
||||||
|
raise PatcherError(f"Патч '{patch_name}' не найден")
|
||||||
|
|
||||||
|
patch = manager.load_patch(patch_name)
|
||||||
|
patch.enabled = True
|
||||||
|
patch.save_config()
|
||||||
|
|
||||||
|
console.print(f"[green]✔ Патч {patch_name} включён")
|
||||||
|
|
||||||
|
|
||||||
|
@app.command()
|
||||||
|
@handle_errors
|
||||||
|
def disable(patch_name: str = typer.Argument(..., help="Имя патча")):
|
||||||
|
"""Выключить патч"""
|
||||||
|
manager = PatchManager(console)
|
||||||
|
|
||||||
|
if patch_name not in manager.discover_patches():
|
||||||
|
raise PatcherError(f"Патч '{patch_name}' не найден")
|
||||||
|
|
||||||
|
patch = manager.load_patch(patch_name)
|
||||||
|
patch.enabled = False
|
||||||
|
patch.save_config()
|
||||||
|
|
||||||
|
console.print(f"[yellow]✔ Патч {patch_name} выключен")
|
||||||
|
|
||||||
|
|
||||||
|
@app.command()
|
||||||
|
@handle_errors
|
||||||
|
def build(
|
||||||
|
force: bool = typer.Option(
|
||||||
|
False, "--force", "-f", help="Принудительная сборка при ошибках"
|
||||||
|
),
|
||||||
|
verbose: bool = typer.Option(False, "--verbose", "-v", help="Подробный вывод"),
|
||||||
|
skip_compile: bool = typer.Option(
|
||||||
|
False, "--skip-compile", "-s", help="Пропустить компиляцию (только патчи)"
|
||||||
|
),
|
||||||
|
):
|
||||||
|
"""Декомпиляция, применение патчей и сборка APK"""
|
||||||
|
conf = load_config(console)
|
||||||
|
|
||||||
|
apk_processor = APKProcessor(console, TOOLS)
|
||||||
|
|
||||||
|
apk = select_apk(console)
|
||||||
|
apk_processor.decompile(apk, DECOMPILED)
|
||||||
|
|
||||||
|
manager = PatchManager(console)
|
||||||
|
patches = manager.load_enabled_patches()
|
||||||
|
|
||||||
|
if not patches:
|
||||||
|
console.print("[yellow]Нет включённых патчей")
|
||||||
|
if not force:
|
||||||
|
raise typer.Exit(0)
|
||||||
|
|
||||||
|
base_config = conf.base.copy()
|
||||||
|
base_config["verbose"] = verbose
|
||||||
|
base_config["decompiled"] = str(DECOMPILED)
|
||||||
|
|
||||||
|
console.print(f"\n[cyan]Применение патчей ({len(patches)})...[/cyan]")
|
||||||
|
|
||||||
|
with Progress(console=console) as progress:
|
||||||
|
task = progress.add_task("Патчи", total=len(patches))
|
||||||
|
|
||||||
|
for patch in patches:
|
||||||
|
success = patch.safe_apply(base_config)
|
||||||
|
status = "[green]✔[/green]" if success else "[red]✘[/red]"
|
||||||
|
progress.console.print(f" {status} [{patch.priority:2d}] {patch.name}")
|
||||||
|
progress.advance(task)
|
||||||
|
|
||||||
|
applied = sum(1 for p in patches if p.applied)
|
||||||
|
failed = len(patches) - applied
|
||||||
|
|
||||||
|
console.print()
|
||||||
|
if failed == 0:
|
||||||
|
console.print(f"[green]✔ Все патчи применены ({applied}/{len(patches)})")
|
||||||
|
else:
|
||||||
|
console.print(
|
||||||
|
f"[yellow]⚠ Применено: {applied}/{len(patches)}, ошибок: {failed}"
|
||||||
|
)
|
||||||
|
|
||||||
|
if skip_compile:
|
||||||
|
console.print("[yellow]Компиляция пропущена (--skip-compile)")
|
||||||
|
return
|
||||||
|
|
||||||
|
should_compile = (
|
||||||
|
failed == 0
|
||||||
|
or force
|
||||||
|
or Prompt.ask(
|
||||||
|
"\nПродолжить сборку несмотря на ошибки?", choices=["y", "n"], default="n"
|
||||||
|
)
|
||||||
|
== "y"
|
||||||
|
)
|
||||||
|
|
||||||
|
if should_compile:
|
||||||
|
console.print()
|
||||||
|
signed_apk, meta = apk_processor.build_and_sign(
|
||||||
|
source=DECOMPILED,
|
||||||
|
output_dir=MODIFIED,
|
||||||
|
)
|
||||||
|
generate_report(signed_apk, meta, patches, manager)
|
||||||
|
else:
|
||||||
|
console.print("[red]Сборка отменена")
|
||||||
|
raise typer.Exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
@app.command()
|
||||||
|
@handle_errors
|
||||||
|
def clean(
|
||||||
|
all_dirs: bool = typer.Option(
|
||||||
|
False, "--all", "-a", help="Очистить все директории включая modified и configs"
|
||||||
|
)
|
||||||
|
):
|
||||||
|
"""Очистка временных файлов"""
|
||||||
|
dirs_to_clean = [DECOMPILED]
|
||||||
|
|
||||||
|
if all_dirs:
|
||||||
|
dirs_to_clean.extend([MODIFIED, CONFIGS])
|
||||||
|
|
||||||
|
for d in dirs_to_clean:
|
||||||
|
if d.exists():
|
||||||
|
shutil.rmtree(d)
|
||||||
|
d.mkdir()
|
||||||
|
console.print(f"[yellow]✔ Очищено: {d}")
|
||||||
|
else:
|
||||||
|
console.print(f"[dim]≫ Пропущено (не существует): {d}[/dim]")
|
||||||
|
|
||||||
|
console.print("[green]✔ Очистка завершена")
|
||||||
|
|
||||||
|
|
||||||
|
@app.command()
|
||||||
|
@handle_errors
|
||||||
|
def config():
|
||||||
|
"""Показать текущую конфигурацию"""
|
||||||
|
conf = load_config(console)
|
||||||
|
|
||||||
|
console.print("[bold cyan]Конфигурация (config.json):[/bold cyan]\n")
|
||||||
|
|
||||||
|
console.print("[yellow]Tools:[/yellow]")
|
||||||
|
console.print(f" apktool_jar_url: {conf.tools.apktool_jar_url}")
|
||||||
|
console.print(f" apktool_wrapper_url: {conf.tools.apktool_wrapper_url}")
|
||||||
|
|
||||||
|
if conf.base:
|
||||||
|
console.print("\n[yellow]Base:[/yellow]")
|
||||||
|
for key, value in conf.base.items():
|
||||||
|
console.print(f" {key}: {value}")
|
||||||
|
|
||||||
|
|
||||||
|
@app.command()
|
||||||
|
@handle_errors
|
||||||
|
def version():
|
||||||
|
"""Показать версию инструмента"""
|
||||||
|
console.print(f"[cyan]anixarty-patcher[/cyan] v{__version__}")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
app()
|
||||||
|
|||||||
+70
-32
@@ -1,40 +1,78 @@
|
|||||||
"""Change api server"""
|
"""Заменяет сервер api
|
||||||
priority = 0
|
|
||||||
|
"change_server": {
|
||||||
|
"enabled": true,
|
||||||
|
"server": "https://anixarty.0x174.su/patch"
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
|
||||||
|
__author__ = "wowlikon <wowlikon@gmail.com>"
|
||||||
|
__version__ = "1.0.0"
|
||||||
|
import json
|
||||||
|
from typing import Any, Dict
|
||||||
|
|
||||||
|
import requests
|
||||||
|
from pydantic import Field
|
||||||
from tqdm import tqdm
|
from tqdm import tqdm
|
||||||
|
|
||||||
import json
|
from utils.config import PatchTemplate
|
||||||
import requests
|
|
||||||
|
|
||||||
|
|
||||||
def apply(config: dict) -> bool:
|
class Patch(PatchTemplate):
|
||||||
response = requests.get(config['server'])
|
priority: int = Field(frozen=True, exclude=True, default=0)
|
||||||
assert response.status_code == 200, f"Failed to fetch data {response.status_code} {response.text}"
|
server: str = Field("https://anixarty.0x174.su/patch", description="URL сервера")
|
||||||
new_api = json.loads(response.text)
|
|
||||||
for item in new_api['modifications']:
|
def apply(self, base: Dict[str, Any]) -> bool:
|
||||||
tqdm.write(f"Изменение {item['file']}")
|
response = requests.get(self.server) # Получаем данные для патча
|
||||||
filepath = './decompiled/smali_classes2/com/swiftsoft/anixartd/network/api/'+item['file']
|
assert (
|
||||||
with open(filepath, 'r') as f:
|
response.status_code == 200
|
||||||
|
), f"Failed to fetch data {response.status_code} {response.text}"
|
||||||
|
|
||||||
|
new_api = json.loads(response.text)
|
||||||
|
for item in new_api["modifications"]: # Применяем замены API
|
||||||
|
tqdm.write(f"Изменение {item['file']}")
|
||||||
|
filepath = (
|
||||||
|
"./decompiled/smali_classes2/com/swiftsoft/anixartd/network/api/"
|
||||||
|
+ item["file"]
|
||||||
|
)
|
||||||
|
|
||||||
|
with open(filepath, "r") as f:
|
||||||
|
content = f.read()
|
||||||
|
|
||||||
|
with open(filepath, "w") as f:
|
||||||
|
if content.count(item["src"]) == 0:
|
||||||
|
tqdm.write(f"⚠ Не найдено {item['src']}")
|
||||||
|
f.write(content.replace(item["src"], item["dst"]))
|
||||||
|
|
||||||
|
tqdm.write(
|
||||||
|
f"Изменение Github ссылки"
|
||||||
|
) # Обновление ссылки на поиск серверов в Github
|
||||||
|
filepath = "./decompiled/smali_classes2/com/swiftsoft/anixartd/utils/anixnet/GithubPagesNetFetcher.smali"
|
||||||
|
|
||||||
|
with open(filepath, "r") as f:
|
||||||
content = f.read()
|
content = f.read()
|
||||||
with open(filepath, 'w') as f:
|
|
||||||
if content.count(item['src']) == 0:
|
|
||||||
tqdm.write(f"⚠ Не найдено {item['src']}")
|
|
||||||
f.write(content.replace(item['src'], item['dst']))
|
|
||||||
|
|
||||||
tqdm.write(f"Изменение Github ссылки")
|
with open(filepath, "w") as f:
|
||||||
filepath = './decompiled/smali_classes2/com/swiftsoft/anixartd/utils/anixnet/GithubPagesNetFetcher.smali'
|
f.write(
|
||||||
with open(filepath, 'r') as f:
|
content.replace(
|
||||||
content = f.read()
|
'const-string v1, "https://anixhelper.github.io/pages/urls.json"',
|
||||||
with open(filepath, 'w') as f:
|
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'
|
) # Отключение автовыбора сервера
|
||||||
with open(filepath, 'r') as f:
|
filepath = "./decompiled/smali_classes2/com/swiftsoft/anixartd/DaggerApp_HiltComponents_SingletonC$SingletonCImpl$SwitchingProvider.smali"
|
||||||
for line in f.readlines():
|
|
||||||
if "addInterceptor" in line: continue
|
|
||||||
content += line
|
|
||||||
with open(filepath, 'w') as f:
|
|
||||||
f.write(content)
|
|
||||||
|
|
||||||
return True
|
content = ""
|
||||||
|
with open(filepath, "r") as f:
|
||||||
|
for line in f.readlines():
|
||||||
|
if "addInterceptor" in line:
|
||||||
|
continue
|
||||||
|
content += line
|
||||||
|
|
||||||
|
with open(filepath, "w") as f:
|
||||||
|
f.write(content)
|
||||||
|
|
||||||
|
return True
|
||||||
|
|||||||
@@ -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
|
|
||||||
+182
-86
@@ -1,111 +1,207 @@
|
|||||||
"""Change application theme"""
|
"""Изменяет цветовую тему приложения и иконку
|
||||||
priority = 0
|
|
||||||
|
"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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
|
||||||
|
__author__ = "wowlikon <wowlikon@gmail.com>"
|
||||||
|
__version__ = "1.0.0"
|
||||||
|
from typing import Any, Dict
|
||||||
|
|
||||||
from lxml import etree
|
from lxml import etree
|
||||||
|
from pydantic import BaseModel, Field, model_validator
|
||||||
|
|
||||||
from utils.public import (
|
from utils.config import PatchTemplate
|
||||||
insert_after_public,
|
from utils.public import change_color, insert_after_color, insert_after_public
|
||||||
insert_after_color,
|
|
||||||
insert_after_id,
|
|
||||||
change_color,
|
|
||||||
)
|
|
||||||
|
|
||||||
def apply(config: dict) -> bool:
|
|
||||||
main_color = config["theme"]["colors"]["primary"]
|
|
||||||
splash_color = config["theme"]["colors"]["secondary"]
|
|
||||||
gradient_angle = config["theme"]["gradient"]["angle"]
|
|
||||||
gradient_from = config["theme"]["gradient"]["from"]
|
|
||||||
gradient_to = config["theme"]["gradient"]["to"]
|
|
||||||
|
|
||||||
# No connection alert coolor
|
class Gradient(BaseModel):
|
||||||
with open("./decompiled/assets/no_connection.html", "r", encoding="utf-8") as file:
|
priority: int = Field(frozen=True, exclude=True, default=0)
|
||||||
file_contents = file.read()
|
angle: float = Field(0.0, description="Угол градиента")
|
||||||
|
start_color: str = Field("#ffccff00", description="Начальный цвет градиента")
|
||||||
|
end_color: str = Field("#ffcccc00", description="Конечный цвет градиента")
|
||||||
|
|
||||||
new_contents = file_contents.replace("#f04e4e", main_color)
|
|
||||||
|
|
||||||
with open("./decompiled/assets/no_connection.html", "w", encoding="utf-8") as file:
|
class Logo(BaseModel):
|
||||||
file.write(new_contents)
|
gradient: Gradient = Field(
|
||||||
|
default_factory=Gradient, description="Настройки градиента"
|
||||||
|
)
|
||||||
|
ears_color: str = Field("#ffd0d0d0", description="Цвет ушей логотипа")
|
||||||
|
|
||||||
# For logo
|
|
||||||
drawable_types = ["", "-night"]
|
|
||||||
|
|
||||||
for drawable_type in drawable_types:
|
class Colors(BaseModel):
|
||||||
# Application logo gradient colors
|
primary: str = Field("#ccff00", description="Основной цвет")
|
||||||
file_path = f"./decompiled/res/drawable{drawable_type}/$logo__0.xml"
|
secondary: str = Field("#ffcccc00", description="Вторичный цвет")
|
||||||
|
background: str = Field("#ffffff", description="Фоновый цвет")
|
||||||
|
text: str = Field("#000000", description="Цвет текста")
|
||||||
|
|
||||||
parser = etree.XMLParser(remove_blank_text=True)
|
|
||||||
tree = etree.parse(file_path, parser)
|
|
||||||
root = tree.getroot()
|
|
||||||
|
|
||||||
# Change attributes with namespace
|
class Patch(PatchTemplate):
|
||||||
root.set(f"{{{config['xml_ns']['android']}}}angle", gradient_angle)
|
priority: int = Field(frozen=True, exclude=True, default=0)
|
||||||
root.set(f"{{{config['xml_ns']['android']}}}startColor", gradient_from)
|
logo: Logo = Field(default_factory=Logo, description="Настройки цветов логотипа")
|
||||||
root.set(f"{{{config['xml_ns']['android']}}}endColor", gradient_to)
|
colors: Colors = Field(default_factory=Colors, description="Настройки цветов")
|
||||||
|
|
||||||
# Save back
|
@model_validator(mode="before")
|
||||||
tree.write(file_path, pretty_print=True, xml_declaration=True, encoding="utf-8")
|
@classmethod
|
||||||
|
def validate_nested(cls, data):
|
||||||
|
if isinstance(data, dict):
|
||||||
|
if "logo" in data and isinstance(data["logo"], dict):
|
||||||
|
data["logo"] = Logo(**data["logo"])
|
||||||
|
if "colors" in data and isinstance(data["colors"], dict):
|
||||||
|
data["colors"] = Colors(**data["colors"])
|
||||||
|
return data
|
||||||
|
|
||||||
# Application logo anim color
|
def hex_to_lottie(hex_color: str) -> tuple[float, float, float]:
|
||||||
file_path = f"./decompiled/res/drawable{drawable_type}/$logo_splash_anim__0.xml"
|
hex_color = hex_color.lstrip("#")
|
||||||
|
hex_color = hex_color[2:] if len(hex_color) == 8 else hex_color
|
||||||
|
return (
|
||||||
|
int(hex_color[:2], 16) / 255.0,
|
||||||
|
int(hex_color[2:4], 16) / 255.0,
|
||||||
|
int(hex_color[4:6], 16) / 255.0,
|
||||||
|
)
|
||||||
|
|
||||||
parser = etree.XMLParser(remove_blank_text=True)
|
def apply(self, base: Dict[str, Any]) -> bool:
|
||||||
tree = etree.parse(file_path, parser)
|
main_color = self.colors.primary
|
||||||
root = tree.getroot()
|
splash_color = self.colors.secondary
|
||||||
|
|
||||||
# Finding "path"
|
# Обновление сообщения об отсутствии подключения
|
||||||
for el in root.findall("path", namespaces=config["xml_ns"]):
|
with open(
|
||||||
name = el.get(f"{{{config['xml_ns']['android']}}}name")
|
"./decompiled/assets/no_connection.html", "r", encoding="utf-8"
|
||||||
if name == "path":
|
) as file:
|
||||||
el.set(f"{{{config['xml_ns']['android']}}}fillColor", splash_color)
|
file_contents = file.read()
|
||||||
|
|
||||||
# Save back
|
new_contents = file_contents.replace("#f04e4e", main_color)
|
||||||
tree.write(file_path, pretty_print=True, xml_declaration=True, encoding="utf-8")
|
|
||||||
|
|
||||||
for filename in ["$ic_launcher_foreground__0", "$ic_banner_foreground__0"]:
|
with open(
|
||||||
file_path = f"./decompiled/res/drawable-v24/{filename}.xml"
|
"./decompiled/assets/no_connection.html", "w", encoding="utf-8"
|
||||||
|
) as file:
|
||||||
|
file.write(new_contents)
|
||||||
|
|
||||||
parser = etree.XMLParser(remove_blank_text=True)
|
# Суффиксы лого
|
||||||
tree = etree.parse(file_path, parser)
|
drawable_types = ["", "-night"]
|
||||||
root = tree.getroot()
|
|
||||||
|
|
||||||
# Change attributes with namespace
|
for drawable_type in drawable_types:
|
||||||
root.set(f"{{{config['xml_ns']['android']}}}angle", gradient_angle)
|
# Градиент лого приложения
|
||||||
items = root.findall("item", namespaces=config['xml_ns'])
|
file_path = f"./decompiled/res/drawable{drawable_type}/$logo__0.xml"
|
||||||
assert len(items) == 2
|
|
||||||
items[0].set(f"{{{config['xml_ns']['android']}}}color", gradient_from)
|
|
||||||
items[1].set(f"{{{config['xml_ns']['android']}}}color", gradient_to)
|
|
||||||
|
|
||||||
# Save back
|
parser = etree.XMLParser(remove_blank_text=True)
|
||||||
tree.write(file_path, pretty_print=True, xml_declaration=True, encoding="utf-8")
|
tree = etree.parse(file_path, parser)
|
||||||
|
root = tree.getroot()
|
||||||
|
|
||||||
insert_after_public("carmine", "custom_color")
|
# Замена атрибутов значениями из конфигурации
|
||||||
insert_after_public("carmine_alpha_10", "custom_color_alpha_10")
|
root.set(
|
||||||
insert_after_color("carmine", "custom_color", main_color[0]+'ff'+main_color[1:])
|
f"{{{base['xml_ns']['android']}}}angle", str(self.logo.gradient.angle)
|
||||||
insert_after_color("carmine_alpha_10", "custom_color_alpha_10", main_color[0]+'1a'+main_color[1:])
|
)
|
||||||
|
root.set(
|
||||||
|
f"{{{base['xml_ns']['android']}}}startColor",
|
||||||
|
self.logo.gradient.start_color,
|
||||||
|
)
|
||||||
|
root.set(
|
||||||
|
f"{{{base['xml_ns']['android']}}}endColor", self.logo.gradient.end_color
|
||||||
|
)
|
||||||
|
|
||||||
change_color("accent_alpha_10", main_color[0]+'1a'+main_color[1:])
|
# Сохранение
|
||||||
change_color("accent_alpha_20", main_color[0]+'33'+main_color[1:])
|
tree.write(
|
||||||
change_color("accent_alpha_50", main_color[0]+'80'+main_color[1:])
|
file_path, pretty_print=True, xml_declaration=True, encoding="utf-8"
|
||||||
change_color("accent_alpha_70", main_color[0]+'b3'+main_color[1:])
|
)
|
||||||
change_color("colorAccent", main_color[0]+'ff'+main_color[1:])
|
|
||||||
change_color("link_color", main_color[0]+'ff'+main_color[1:])
|
|
||||||
change_color("link_color_alpha_70", main_color[0]+'b3'+main_color[1:])
|
|
||||||
change_color("refresh_progress", main_color[0]+'ff'+main_color[1:])
|
|
||||||
|
|
||||||
change_color("ic_launcher_background", "#ff000000")
|
# Замена анимации лого
|
||||||
change_color("bottom_nav_indicator_active", "#ffffffff")
|
file_path = (
|
||||||
change_color("bottom_nav_indicator_icon_checked", main_color[0]+'ff'+main_color[1:])
|
f"./decompiled/res/drawable{drawable_type}/$logo_splash_anim__0.xml"
|
||||||
change_color("bottom_nav_indicator_label_checked", main_color[0]+'ff'+main_color[1:])
|
)
|
||||||
|
|
||||||
insert_after_public("warning_error_counter_background", "ic_custom_telegram")
|
parser = etree.XMLParser(remove_blank_text=True)
|
||||||
insert_after_public("warning_error_counter_background", "ic_custom_crown")
|
tree = etree.parse(file_path, parser)
|
||||||
|
root = tree.getroot()
|
||||||
|
|
||||||
try:
|
for el in root.findall("path", namespaces=base["xml_ns"]):
|
||||||
last = "speed75"
|
name = el.get(f"{{{base['xml_ns']['android']}}}name")
|
||||||
for speed in config.get("speeds", []):
|
if name == "path":
|
||||||
insert_after_public(last, f"speed{int(float(speed)*10)}")
|
el.set(
|
||||||
insert_after_id(last, f"speed{int(float(speed)*10)}")
|
f"{{{base['xml_ns']['android']}}}fillColor",
|
||||||
last = f"speed{int(float(speed)*10)}"
|
self.colors.secondary,
|
||||||
except Exception as e:
|
)
|
||||||
print(f"Error occurred while processing speeds: {e}")
|
elif name in ["path_1", "path_2"]:
|
||||||
return True
|
el.set(
|
||||||
|
f"{{{base['xml_ns']['android']}}}fillColor",
|
||||||
|
self.logo.ears_color,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Сохранение
|
||||||
|
tree.write(
|
||||||
|
file_path, pretty_print=True, xml_declaration=True, encoding="utf-8"
|
||||||
|
)
|
||||||
|
|
||||||
|
for filename in ["$ic_launcher_foreground__0", "$ic_banner_foreground__0"]:
|
||||||
|
file_path = f"./decompiled/res/drawable-v24/{filename}.xml"
|
||||||
|
|
||||||
|
parser = etree.XMLParser(remove_blank_text=True)
|
||||||
|
tree = etree.parse(file_path, parser)
|
||||||
|
root = tree.getroot()
|
||||||
|
|
||||||
|
# Замена атрибутов значениями из конфигурации
|
||||||
|
root.set(
|
||||||
|
f"{{{base['xml_ns']['android']}}}angle", str(self.logo.gradient.angle)
|
||||||
|
)
|
||||||
|
items = root.findall("item", namespaces=base["xml_ns"])
|
||||||
|
assert len(items) == 2
|
||||||
|
items[0].set(
|
||||||
|
f"{{{base['xml_ns']['android']}}}color", self.logo.gradient.start_color
|
||||||
|
)
|
||||||
|
items[1].set(
|
||||||
|
f"{{{base['xml_ns']['android']}}}color", self.logo.gradient.end_color
|
||||||
|
)
|
||||||
|
|
||||||
|
# Сохранение
|
||||||
|
tree.write(
|
||||||
|
file_path, pretty_print=True, xml_declaration=True, encoding="utf-8"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Добаление новых цветов для темы
|
||||||
|
insert_after_public("carmine", "custom_color")
|
||||||
|
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_alpha_10",
|
||||||
|
"custom_color_alpha_10",
|
||||||
|
main_color[0] + "1a" + main_color[1:],
|
||||||
|
)
|
||||||
|
|
||||||
|
# Замена цветов
|
||||||
|
change_color("accent_alpha_10", main_color[0] + "1a" + main_color[1:])
|
||||||
|
change_color("accent_alpha_20", main_color[0] + "33" + main_color[1:])
|
||||||
|
change_color("accent_alpha_50", main_color[0] + "80" + main_color[1:])
|
||||||
|
change_color("accent_alpha_70", main_color[0] + "b3" + main_color[1:])
|
||||||
|
|
||||||
|
change_color("colorAccent", main_color[0] + "ff" + main_color[1:])
|
||||||
|
change_color("link_color", main_color[0] + "ff" + main_color[1:])
|
||||||
|
change_color("link_color_alpha_70", main_color[0] + "b3" + main_color[1:])
|
||||||
|
change_color("refresh_progress", main_color[0] + "ff" + main_color[1:])
|
||||||
|
|
||||||
|
change_color("ic_launcher_background", "#ff000000")
|
||||||
|
change_color("bottom_nav_indicator_active", "#ffffffff")
|
||||||
|
change_color(
|
||||||
|
"bottom_nav_indicator_icon_checked", main_color[0] + "ff" + main_color[1:]
|
||||||
|
)
|
||||||
|
change_color(
|
||||||
|
"bottom_nav_indicator_label_checked", main_color[0] + "ff" + main_color[1:]
|
||||||
|
)
|
||||||
|
|
||||||
|
return True
|
||||||
|
|||||||
@@ -0,0 +1,107 @@
|
|||||||
|
"""Меняет местами кнопки лайка и дизлайка у коментария и иконки
|
||||||
|
|
||||||
|
"comment_vote": {
|
||||||
|
"enabled": true,
|
||||||
|
"replace": true,
|
||||||
|
"custom_icons": true,
|
||||||
|
"icons_size": "14.0dip"
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
|
||||||
|
__author__ = "wowlikon <wowlikon@gmail.com>"
|
||||||
|
__version__ = "1.0.0"
|
||||||
|
import os
|
||||||
|
import shutil
|
||||||
|
from typing import Any, Dict
|
||||||
|
|
||||||
|
from lxml import etree
|
||||||
|
from pydantic import Field
|
||||||
|
from tqdm import tqdm
|
||||||
|
|
||||||
|
from utils.config import PatchTemplate
|
||||||
|
|
||||||
|
|
||||||
|
class Patch(PatchTemplate):
|
||||||
|
priority: int = Field(frozen=True, exclude=True, default=0)
|
||||||
|
replace: bool = Field(True, description="Менять местами лайк/дизлайк")
|
||||||
|
custom_icons: bool = Field(True, description="Кастомные иконки")
|
||||||
|
icon_size: str = Field("18.0dip", description="Размер иконки")
|
||||||
|
|
||||||
|
def apply(self, base: Dict[str, Any]) -> bool:
|
||||||
|
file_path = "./decompiled/res/layout/item_comment.xml"
|
||||||
|
parser = etree.XMLParser(remove_blank_text=True)
|
||||||
|
tree = etree.parse(file_path, parser)
|
||||||
|
root = tree.getroot()
|
||||||
|
|
||||||
|
tqdm.write("Меняем размер иконок лайка и дизлайка...")
|
||||||
|
for icon in root.xpath(
|
||||||
|
".//*[@android:id='@id/votePlusInactive']//ImageView | "
|
||||||
|
".//*[@android:id='@id/votePlusActive']//ImageView | "
|
||||||
|
".//*[@android:id='@id/voteMinusInactive']//ImageView | "
|
||||||
|
".//*[@android:id='@id/voteMinusActive']//ImageView",
|
||||||
|
namespaces=base["xml_ns"],
|
||||||
|
):
|
||||||
|
icon.set(f"{{{base['xml_ns']['android']}}}layout_width", self.icon_size)
|
||||||
|
# icon.set(f"{{{base['xml_ns']['android']}}}layout_height", self.icon_size)
|
||||||
|
|
||||||
|
if self.replace:
|
||||||
|
tqdm.write("Меняем местами лайк и дизлайк комментария...")
|
||||||
|
|
||||||
|
containers = root.xpath(
|
||||||
|
".//LinearLayout[.//*[@android:id='@id/voteMinus'] and .//*[@android:id='@id/votePlus']]",
|
||||||
|
namespaces=base["xml_ns"],
|
||||||
|
)
|
||||||
|
|
||||||
|
found = False
|
||||||
|
for container in containers:
|
||||||
|
children = list(container)
|
||||||
|
vote_plus = None
|
||||||
|
vote_minus = None
|
||||||
|
|
||||||
|
for ch in children:
|
||||||
|
cid = ch.get(f'{{{base["xml_ns"]["android"]}}}id')
|
||||||
|
if cid == "@id/votePlus":
|
||||||
|
vote_plus = ch
|
||||||
|
elif cid == "@id/voteMinus":
|
||||||
|
vote_minus = ch
|
||||||
|
|
||||||
|
if vote_plus is not None and vote_minus is not None:
|
||||||
|
found = True
|
||||||
|
i_plus = children.index(vote_plus)
|
||||||
|
i_minus = children.index(vote_minus)
|
||||||
|
children[i_plus], children[i_minus] = (
|
||||||
|
children[i_minus],
|
||||||
|
children[i_plus],
|
||||||
|
)
|
||||||
|
container[:] = children
|
||||||
|
tqdm.write("Кнопки лайк и дизлайк поменялись местами.")
|
||||||
|
break
|
||||||
|
|
||||||
|
if not found:
|
||||||
|
tqdm.write(
|
||||||
|
"Не удалось найти оба узла votePlus/voteMinus даже в общих LinearLayout."
|
||||||
|
)
|
||||||
|
|
||||||
|
if self.custom_icons:
|
||||||
|
tqdm.write("Заменяем иконки лайка и дизлайка на кастомные...")
|
||||||
|
for suffix in ["up", "up_40", "down", "down_40"]:
|
||||||
|
shutil.copy(
|
||||||
|
f"./resources/ic_chevron_{suffix}.xml",
|
||||||
|
f"./decompiled/res/drawable/ic_chevron_{suffix}.xml",
|
||||||
|
)
|
||||||
|
|
||||||
|
for inactive in root.xpath(
|
||||||
|
".//*[@android:id='@id/votePlusInactive'] | .//*[@android:id='@id/voteMinusInactive']",
|
||||||
|
namespaces=base["xml_ns"],
|
||||||
|
):
|
||||||
|
for img in inactive.xpath(
|
||||||
|
".//ImageView[@android:src]", namespaces=base["xml_ns"]
|
||||||
|
):
|
||||||
|
src = img.get(f'{{{base["xml_ns"]["android"]}}}src', "")
|
||||||
|
if src.startswith("@drawable/") and not src.endswith("_40"):
|
||||||
|
img.set(f'{{{base["xml_ns"]["android"]}}}src', src + "_40")
|
||||||
|
|
||||||
|
# Сохраняем
|
||||||
|
tree.write(file_path, encoding="utf-8", xml_declaration=True, pretty_print=True)
|
||||||
|
|
||||||
|
return True
|
||||||
@@ -0,0 +1,333 @@
|
|||||||
|
"""Удаляет ненужное и сжимает ресурсы что-бы уменьшить размер АПК
|
||||||
|
|
||||||
|
Эффективность на проверена на версии 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
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
|
||||||
|
__author__ = "Kentai Radiquum <radiquum@gmail.com>"
|
||||||
|
__version__ = "1.0.0"
|
||||||
|
import os
|
||||||
|
import shutil
|
||||||
|
import subprocess
|
||||||
|
from typing import Any, Dict, List
|
||||||
|
|
||||||
|
from pydantic import Field
|
||||||
|
from tqdm import tqdm
|
||||||
|
|
||||||
|
from utils.config import PatchTemplate
|
||||||
|
from utils.smali_parser import get_smali_lines, save_smali_lines
|
||||||
|
|
||||||
|
|
||||||
|
class Patch(PatchTemplate):
|
||||||
|
priority: int = Field(frozen=True, exclude=True, default=-1)
|
||||||
|
remove_language_files: bool = Field(
|
||||||
|
True, description="Удаляет все языки кроме русского и английского"
|
||||||
|
)
|
||||||
|
remove_AI_voiceover: bool = Field(
|
||||||
|
True, description="Заменяет ИИ озвучку маскота аниксы пустыми mp3 файлами"
|
||||||
|
)
|
||||||
|
remove_debug_lines: bool = Field(
|
||||||
|
False,
|
||||||
|
description="Удаляет строки `.line n` из smali файлов использованные для дебага",
|
||||||
|
)
|
||||||
|
remove_drawable_files: bool = Field(
|
||||||
|
False,
|
||||||
|
description="Удаляет неиспользованные drawable-* из директории decompiled/res",
|
||||||
|
)
|
||||||
|
remove_unknown_files: bool = Field(
|
||||||
|
True, description="Удаляет файлы из директории decompiled/unknown"
|
||||||
|
)
|
||||||
|
remove_unknown_files_keep_dirs: List[str] = Field(
|
||||||
|
["META-INF", "kotlin"],
|
||||||
|
description="Оставляет указанные директории в decompiled/unknown",
|
||||||
|
)
|
||||||
|
compress_png_files: bool = Field(
|
||||||
|
True, description="Сжимает PNG в директории decompiled/res"
|
||||||
|
)
|
||||||
|
|
||||||
|
def do_remove_unknown_files(self, base: Dict[str, Any]):
|
||||||
|
path = "./decompiled/unknown"
|
||||||
|
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 self.remove_unknown_files_keep_dirs:
|
||||||
|
shutil.rmtree(item_path)
|
||||||
|
if base.get("verbose", False):
|
||||||
|
tqdm.write(f"Удалёна директория: {item_path}")
|
||||||
|
return True
|
||||||
|
|
||||||
|
def do_remove_debug_lines(self, config: Dict[str, Any]):
|
||||||
|
for root, _, files in os.walk("./decompiled"):
|
||||||
|
for filename in files:
|
||||||
|
file_path = os.path.join(root, filename)
|
||||||
|
if os.path.isfile(file_path) and filename.endswith(".smali"):
|
||||||
|
file_content = get_smali_lines(file_path)
|
||||||
|
new_content = []
|
||||||
|
for line in file_content:
|
||||||
|
if line.find(".line") >= 0:
|
||||||
|
continue
|
||||||
|
new_content.append(line)
|
||||||
|
save_smali_lines(file_path, new_content)
|
||||||
|
if config.get("verbose", False):
|
||||||
|
tqdm.write(f"Удалены дебаг линии из: {file_path}")
|
||||||
|
return True
|
||||||
|
|
||||||
|
def compress_png(self, config: Dict[str, Any], png_path: str):
|
||||||
|
try:
|
||||||
|
assert subprocess.run(
|
||||||
|
[
|
||||||
|
"pngquant",
|
||||||
|
"--force",
|
||||||
|
"--ext",
|
||||||
|
".png",
|
||||||
|
"--quality=65-90",
|
||||||
|
png_path,
|
||||||
|
],
|
||||||
|
capture_output=True,
|
||||||
|
).returncode in [0, 99]
|
||||||
|
if config.get("verbose", False):
|
||||||
|
tqdm.write(f"Сжат файл PNG: {png_path}")
|
||||||
|
return True
|
||||||
|
except subprocess.CalledProcessError as e:
|
||||||
|
tqdm.write(f"Ошибка при сжатии {png_path}: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def do_compress_png_files(self, config: Dict[str, Any]):
|
||||||
|
compressed = []
|
||||||
|
for root, _, files in os.walk("./decompiled"):
|
||||||
|
for file in files:
|
||||||
|
if file.lower().endswith(".png"):
|
||||||
|
self.compress_png(config, f"{root}/{file}")
|
||||||
|
compressed.append(f"{root}/{file}")
|
||||||
|
return len(compressed) > 0 and any(compressed)
|
||||||
|
|
||||||
|
def do_remove_AI_voiceover(self, config: Dict[str, Any]):
|
||||||
|
blank = "./resources/blank.mp3"
|
||||||
|
path = "./decompiled/res/raw"
|
||||||
|
files = [
|
||||||
|
"reputation_1.mp3",
|
||||||
|
"reputation_2.mp3",
|
||||||
|
"reputation_3.mp3",
|
||||||
|
"sound_beta_1.mp3",
|
||||||
|
"sound_create_blog_1.mp3",
|
||||||
|
"sound_create_blog_2.mp3",
|
||||||
|
"sound_create_blog_3.mp3",
|
||||||
|
"sound_create_blog_4.mp3",
|
||||||
|
"sound_create_blog_5.mp3",
|
||||||
|
"sound_create_blog_6.mp3",
|
||||||
|
"sound_create_blog_reputation_1.mp3",
|
||||||
|
"sound_create_blog_reputation_2.mp3",
|
||||||
|
"sound_create_blog_reputation_3.mp3",
|
||||||
|
"sound_create_blog_reputation_4.mp3",
|
||||||
|
"sound_create_blog_reputation_5.mp3",
|
||||||
|
"sound_create_blog_reputation_6.mp3",
|
||||||
|
]
|
||||||
|
|
||||||
|
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 do_remove_language_files(self, 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 do_remove_drawable_files(self, config: Dict[str, Any]):
|
||||||
|
path = "./decompiled/res"
|
||||||
|
folders = [
|
||||||
|
"drawable-en-hdpi",
|
||||||
|
"drawable-en-ldpi",
|
||||||
|
"drawable-en-mdpi",
|
||||||
|
"drawable-en-xhdpi",
|
||||||
|
"drawable-en-xxhdpi",
|
||||||
|
"drawable-en-xxxhdpi",
|
||||||
|
"drawable-ldrtl-hdpi",
|
||||||
|
"drawable-ldrtl-mdpi",
|
||||||
|
"drawable-ldrtl-xhdpi",
|
||||||
|
"drawable-ldrtl-xxhdpi",
|
||||||
|
"drawable-ldrtl-xxxhdpi",
|
||||||
|
"drawable-tr-anydpi",
|
||||||
|
"drawable-tr-hdpi",
|
||||||
|
"drawable-tr-ldpi",
|
||||||
|
"drawable-tr-mdpi",
|
||||||
|
"drawable-tr-xhdpi",
|
||||||
|
"drawable-tr-xxhdpi",
|
||||||
|
"drawable-tr-xxxhdpi",
|
||||||
|
"drawable-watch",
|
||||||
|
"layout-watch",
|
||||||
|
]
|
||||||
|
|
||||||
|
for folder in folders:
|
||||||
|
if os.path.exists(f"{path}/{folder}"):
|
||||||
|
shutil.rmtree(f"{path}/{folder}")
|
||||||
|
if config.get("verbose", False):
|
||||||
|
tqdm.write(f"Удалена директория: {path}/{folder}")
|
||||||
|
return True
|
||||||
|
|
||||||
|
def apply(self, base: Dict[str, Any]) -> bool:
|
||||||
|
actions = [
|
||||||
|
(
|
||||||
|
self.remove_unknown_files,
|
||||||
|
"Удаление неизвестных файлов...",
|
||||||
|
self.do_remove_unknown_files,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
self.remove_drawable_files,
|
||||||
|
"Удаление директорий drawable-xx...",
|
||||||
|
self.do_remove_drawable_files,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
self.compress_png_files,
|
||||||
|
"Сжатие PNG файлов...",
|
||||||
|
self.do_compress_png_files,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
self.remove_language_files,
|
||||||
|
"Удаление языков...",
|
||||||
|
self.do_remove_language_files,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
self.remove_AI_voiceover,
|
||||||
|
"Удаление ИИ озвучки...",
|
||||||
|
self.do_remove_AI_voiceover,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
self.remove_debug_lines,
|
||||||
|
"Удаление дебаг линий...",
|
||||||
|
self.do_remove_debug_lines,
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
||||||
|
for enabled, message, action in actions:
|
||||||
|
if enabled:
|
||||||
|
tqdm.write(message)
|
||||||
|
action(base)
|
||||||
|
|
||||||
|
return True
|
||||||
@@ -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)
|
|
||||||
+42
-28
@@ -1,33 +1,47 @@
|
|||||||
"""Disable ad banners"""
|
"""Удаляет баннеры рекламы
|
||||||
priority = 0
|
|
||||||
|
|
||||||
from utils.smali_parser import (
|
"disable_ad": {
|
||||||
find_smali_method_end,
|
"enabled": true
|
||||||
find_smali_method_start,
|
}
|
||||||
get_smali_lines,
|
|
||||||
replace_smali_method_body,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
replace = """ .locals 0
|
|
||||||
|
|
||||||
const/4 p0, 0x1
|
|
||||||
|
|
||||||
return p0
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
__author__ = "Kentai Radiquum <radiquum@gmail.com>"
|
||||||
|
__version__ = "1.0.0"
|
||||||
|
import textwrap
|
||||||
|
from typing import Any, Dict
|
||||||
|
|
||||||
def apply(config) -> bool:
|
from pydantic import Field
|
||||||
path = "./decompiled/smali_classes2/com/swiftsoft/anixartd/Prefs.smali"
|
|
||||||
lines = get_smali_lines(path)
|
|
||||||
for index, line in enumerate(lines):
|
|
||||||
if line.find("IS_SPONSOR") >= 0:
|
|
||||||
method_start = find_smali_method_start(lines, index)
|
|
||||||
method_end = find_smali_method_end(lines, index)
|
|
||||||
new_content = replace_smali_method_body(
|
|
||||||
lines, method_start, method_end, replace
|
|
||||||
)
|
|
||||||
|
|
||||||
with open(path, "w", encoding="utf-8") as file:
|
from utils.config import PatchTemplate
|
||||||
file.writelines(new_content)
|
from utils.smali_parser import (find_smali_method_end, find_smali_method_start,
|
||||||
return True
|
get_smali_lines, replace_smali_method_body)
|
||||||
|
|
||||||
|
|
||||||
|
class Patch(PatchTemplate):
|
||||||
|
priority: int = Field(frozen=True, exclude=True, default=0)
|
||||||
|
|
||||||
|
def apply(self, base: Dict[str, Any]) -> bool:
|
||||||
|
path = "./decompiled/smali_classes3/com/swiftsoft/anixartd/Prefs.smali"
|
||||||
|
replacement = [
|
||||||
|
f"\t{line}\n"
|
||||||
|
for line in textwrap.dedent(
|
||||||
|
"""\
|
||||||
|
.locals 0
|
||||||
|
const/4 p0, 0x1
|
||||||
|
return p0
|
||||||
|
"""
|
||||||
|
).splitlines()
|
||||||
|
]
|
||||||
|
|
||||||
|
lines = get_smali_lines(path)
|
||||||
|
for index, line in enumerate(lines):
|
||||||
|
if line.find("IS_SPONSOR") >= 0:
|
||||||
|
method_start = find_smali_method_start(lines, index)
|
||||||
|
method_end = find_smali_method_end(lines, index)
|
||||||
|
new_content = replace_smali_method_body(
|
||||||
|
lines, method_start, method_end, replacement
|
||||||
|
)
|
||||||
|
|
||||||
|
with open(path, "w", encoding="utf-8") as file:
|
||||||
|
file.writelines(new_content)
|
||||||
|
return True
|
||||||
|
|||||||
@@ -1,37 +1,56 @@
|
|||||||
"""Remove beta banner"""
|
"""Удаляет баннеры бета-версии
|
||||||
priority = 0
|
|
||||||
|
"disable_beta_banner": {
|
||||||
|
"enabled": true
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
|
||||||
|
__author__ = "Kentai Radiquum <radiquum@gmail.com>"
|
||||||
|
__version__ = "1.0.0"
|
||||||
|
import os
|
||||||
|
from typing import Any, Dict
|
||||||
|
|
||||||
|
from lxml import etree
|
||||||
|
from pydantic import Field
|
||||||
from tqdm import tqdm
|
from tqdm import tqdm
|
||||||
|
|
||||||
import os
|
from utils.config import PatchTemplate
|
||||||
from lxml import etree
|
from utils.smali_parser import get_smali_lines, save_smali_lines
|
||||||
|
|
||||||
|
|
||||||
def apply(config) -> bool:
|
class Patch(PatchTemplate):
|
||||||
attributes = [
|
priority: int = Field(frozen=True, exclude=True, default=0)
|
||||||
"paddingTop",
|
|
||||||
"paddingBottom",
|
|
||||||
"paddingStart",
|
|
||||||
"paddingEnd",
|
|
||||||
"layout_width",
|
|
||||||
"layout_height",
|
|
||||||
"layout_marginTop",
|
|
||||||
"layout_marginBottom",
|
|
||||||
"layout_marginStart",
|
|
||||||
"layout_marginEnd",
|
|
||||||
]
|
|
||||||
|
|
||||||
beta_banner_xml = "./decompiled/res/layout/item_beta.xml"
|
def apply(self, base: Dict[str, Any]) -> bool:
|
||||||
if os.path.exists(beta_banner_xml):
|
beta_banner_xml = "./decompiled/res/layout/item_beta.xml"
|
||||||
parser = etree.XMLParser(remove_blank_text=True)
|
attributes = [
|
||||||
tree = etree.parse(beta_banner_xml, parser)
|
"paddingTop",
|
||||||
root = tree.getroot()
|
"paddingBottom",
|
||||||
|
"paddingStart",
|
||||||
|
"paddingEnd",
|
||||||
|
"layout_width",
|
||||||
|
"layout_height",
|
||||||
|
"layout_marginTop",
|
||||||
|
"layout_marginBottom",
|
||||||
|
"layout_marginStart",
|
||||||
|
"layout_marginEnd",
|
||||||
|
]
|
||||||
|
|
||||||
for attr in attributes:
|
if os.path.exists(beta_banner_xml):
|
||||||
# tqdm.write(f"set {attr} = 0.0dip")
|
parser = etree.XMLParser(remove_blank_text=True)
|
||||||
root.set(f"{{{config["xml_ns"]['android']}}}{attr}", "0.0dip")
|
tree = etree.parse(beta_banner_xml, parser)
|
||||||
|
root = tree.getroot()
|
||||||
|
|
||||||
tree.write(
|
for attr in attributes:
|
||||||
beta_banner_xml, pretty_print=True, xml_declaration=True, encoding="utf-8"
|
if base.get("verbose", False):
|
||||||
)
|
tqdm.write(f"set {attr} = 0.0dip")
|
||||||
|
root.set(f"{{{base['xml_ns']['android']}}}{attr}", "0.0dip")
|
||||||
|
|
||||||
return True
|
tree.write(
|
||||||
|
beta_banner_xml,
|
||||||
|
pretty_print=True,
|
||||||
|
xml_declaration=True,
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
|
|
||||||
|
return True
|
||||||
|
|||||||
@@ -1,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
|
|
||||||
+101
-85
@@ -1,103 +1,119 @@
|
|||||||
"""Change package name of apk"""
|
"""Изменяет имя пакета в apk, удаляет вход по google и vk
|
||||||
priority = -1
|
|
||||||
|
|
||||||
|
"package_name": {
|
||||||
|
"enabled": true,
|
||||||
|
"new_package_name": "com.wowlikon.anixart"
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
|
||||||
|
__author__ = "wowlikon <wowlikon@gmail.com>"
|
||||||
|
__version__ = "1.0.0"
|
||||||
import os
|
import os
|
||||||
|
from typing import Any, Dict
|
||||||
|
|
||||||
from lxml import etree
|
from lxml import etree
|
||||||
|
from pydantic import Field
|
||||||
|
|
||||||
|
from utils.config import PatchTemplate
|
||||||
|
|
||||||
|
|
||||||
def rename_dir(src, dst):
|
class Patch(PatchTemplate):
|
||||||
os.makedirs(os.path.dirname(dst), exist_ok=True)
|
priority: int = Field(frozen=True, exclude=True, default=-1)
|
||||||
os.rename(src, dst)
|
package_name: str = Field("com.wowlikon.anixart", description="Название пакета")
|
||||||
|
|
||||||
|
def rename_dir(self, src, dst):
|
||||||
|
os.makedirs(os.path.dirname(dst), exist_ok=True)
|
||||||
|
os.rename(src, dst)
|
||||||
|
|
||||||
def apply(config: dict) -> bool:
|
def apply(self, 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 filename in files:
|
||||||
|
file_path = os.path.join(root, filename)
|
||||||
|
|
||||||
for root, dirs, files in os.walk("./decompiled"):
|
if os.path.isfile(file_path):
|
||||||
for filename in files:
|
try: # Изменяем имя пакета в файлах
|
||||||
file_path = os.path.join(root, filename)
|
with open(file_path, "r", encoding="utf-8") as file:
|
||||||
|
file_contents = file.read()
|
||||||
|
|
||||||
if os.path.isfile(file_path):
|
new_contents = file_contents.replace(
|
||||||
try:
|
"com.swiftsoft.anixartd", self.package_name
|
||||||
with open(file_path, "r", encoding="utf-8") as file:
|
)
|
||||||
file_contents = file.read()
|
new_contents = new_contents.replace(
|
||||||
|
"com/swiftsoft/anixartd",
|
||||||
|
self.package_name.replace(".", "/"),
|
||||||
|
).replace(
|
||||||
|
"com/swiftsoft",
|
||||||
|
"/".join(self.package_name.split(".")[:2]),
|
||||||
|
)
|
||||||
|
with open(file_path, "w", encoding="utf-8") as file:
|
||||||
|
file.write(new_contents)
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
new_contents = file_contents.replace(
|
# Изменяем названия папок
|
||||||
"com.swiftsoft.anixartd", config["new_package_name"]
|
if os.path.exists("./decompiled/smali/com/swiftsoft/anixartd"):
|
||||||
)
|
self.rename_dir(
|
||||||
new_contents = new_contents.replace(
|
"./decompiled/smali/com/swiftsoft/anixartd",
|
||||||
"com/swiftsoft/anixartd",
|
os.path.join(
|
||||||
config["new_package_name"].replace(".", "/"),
|
"./decompiled", "smali", self.package_name.replace(".", "/")
|
||||||
)
|
),
|
||||||
with open(file_path, "w", encoding="utf-8") as file:
|
)
|
||||||
file.write(new_contents)
|
if os.path.exists("./decompiled/smali_classes2/com/swiftsoft/anixartd"):
|
||||||
except:
|
self.rename_dir(
|
||||||
pass
|
"./decompiled/smali_classes2/com/swiftsoft/anixartd",
|
||||||
|
os.path.join(
|
||||||
|
"./decompiled",
|
||||||
|
"smali_classes2",
|
||||||
|
self.package_name.replace(".", "/"),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
if os.path.exists("./decompiled/smali_classes4/com/swiftsoft"):
|
||||||
|
self.rename_dir(
|
||||||
|
"./decompiled/smali_classes4/com/swiftsoft",
|
||||||
|
os.path.join(
|
||||||
|
"./decompiled",
|
||||||
|
"smali_classes4",
|
||||||
|
"/".join(self.package_name.split(".")[:2]),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
if os.path.exists("./decompiled/smali/com/swiftsoft/anixartd"):
|
# rename_dir(
|
||||||
rename_dir(
|
# "./decompiled/smali_classes3/com/swiftsoft/anixartd",
|
||||||
"./decompiled/smali/com/swiftsoft/anixartd",
|
# os.path.join(
|
||||||
os.path.join(
|
# "./decompiled",
|
||||||
"./decompiled", "smali", config["new_package_name"].replace(".", "/")
|
# "smali_classes3",
|
||||||
),
|
# config["new_package_name"].replace(".", "/"),
|
||||||
)
|
# ),
|
||||||
if os.path.exists("./decompiled/smali_classes2/com/swiftsoft/anixartd"):
|
# )
|
||||||
rename_dir(
|
|
||||||
"./decompiled/smali_classes2/com/swiftsoft/anixartd",
|
|
||||||
os.path.join(
|
|
||||||
"./decompiled",
|
|
||||||
"smali_classes2",
|
|
||||||
config["new_package_name"].replace(".", "/"),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
if os.path.exists("./decompiled/smali_classes4/com/swiftsoft"):
|
|
||||||
rename_dir(
|
|
||||||
"./decompiled/smali_classes4/com/swiftsoft",
|
|
||||||
os.path.join(
|
|
||||||
"./decompiled",
|
|
||||||
"smali_classes4",
|
|
||||||
"/".join(config["new_package_name"].split(".")[:-1]),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
# rename_dir(
|
# Замена названия пакета для smali_classes4
|
||||||
# "./decompiled/smali_classes3/com/swiftsoft/anixartd",
|
for root, dirs, files in os.walk("./decompiled/smali_classes4/"):
|
||||||
# os.path.join(
|
for filename in files:
|
||||||
# "./decompiled",
|
file_path = os.path.join(root, filename)
|
||||||
# "smali_classes3",
|
|
||||||
# config["new_package_name"].replace(".", "/"),
|
|
||||||
# ),
|
|
||||||
# )
|
|
||||||
|
|
||||||
for root, dirs, files in os.walk("./decompiled/smali_classes4/"):
|
if os.path.isfile(file_path):
|
||||||
for filename in files:
|
try:
|
||||||
file_path = os.path.join(root, filename)
|
with open(file_path, "r", encoding="utf-8") as file:
|
||||||
|
file_contents = file.read()
|
||||||
|
|
||||||
if os.path.isfile(file_path):
|
new_contents = file_contents.replace(
|
||||||
try:
|
"com/swiftsoft",
|
||||||
with open(file_path, "r", encoding="utf-8") as file:
|
"/".join(self.package_name.split(".")[:-1]),
|
||||||
file_contents = file.read()
|
)
|
||||||
|
with open(file_path, "w", encoding="utf-8") as file:
|
||||||
|
file.write(new_contents)
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
new_contents = file_contents.replace(
|
# Скрытие входа по Google и VK (НЕ РАБОТАЮТ В МОДАХ)
|
||||||
"com/swiftsoft",
|
file_path = "./decompiled/res/layout/fragment_sign_in.xml"
|
||||||
"/".join(config["new_package_name"].split(".")[:-1]),
|
parser = etree.XMLParser(remove_blank_text=True)
|
||||||
)
|
tree = etree.parse(file_path, parser)
|
||||||
with open(file_path, "w", encoding="utf-8") as file:
|
root = tree.getroot()
|
||||||
file.write(new_contents)
|
|
||||||
except:
|
|
||||||
pass
|
|
||||||
|
|
||||||
file_path = "./decompiled/res/layout/fragment_sign_in.xml"
|
last_linear = root.xpath("//LinearLayout/LinearLayout[4]")[0]
|
||||||
parser = etree.XMLParser(remove_blank_text=True)
|
last_linear.set(f"{{{base['xml_ns']['android']}}}visibility", "gone")
|
||||||
tree = etree.parse(file_path, parser)
|
|
||||||
root = tree.getroot()
|
|
||||||
|
|
||||||
last_linear = root.xpath("//LinearLayout/LinearLayout[4]")[0]
|
tree.write(file_path, pretty_print=True, xml_declaration=True, encoding="utf-8")
|
||||||
last_linear.set(f"{{{config['xml_ns']['android']}}}visibility", "gone")
|
|
||||||
|
|
||||||
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)"
|
|
||||||
|
|||||||
@@ -0,0 +1,96 @@
|
|||||||
|
"""
|
||||||
|
Меняет порядок вкладок в панели навигации
|
||||||
|
|
||||||
|
"replace_navbar": {
|
||||||
|
"enabled": true,
|
||||||
|
"items": ["home", "discover", "feed", "bookmarks", "profile"]
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
|
||||||
|
__author__ = "wowlikon <wowlikon@gmail.com>"
|
||||||
|
__version__ = "1.0.0"
|
||||||
|
from typing import Any, Dict, List
|
||||||
|
|
||||||
|
from lxml import etree
|
||||||
|
from pydantic import Field
|
||||||
|
from tqdm import tqdm
|
||||||
|
|
||||||
|
from utils.config import PatchTemplate
|
||||||
|
from utils.smali_parser import get_smali_lines, save_smali_lines, find_smali_line
|
||||||
|
|
||||||
|
class Patch(PatchTemplate):
|
||||||
|
priority: int = Field(frozen=True, exclude=True, default=0)
|
||||||
|
default_compact: bool = Field(True, description="Компактный вид по умолчанию")
|
||||||
|
items: List[str] = Field(
|
||||||
|
["home", "discover", "feed", "bookmarks", "profile"],
|
||||||
|
description="Список элементов в панели навигации",
|
||||||
|
)
|
||||||
|
|
||||||
|
def apply(self, 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 self.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 self.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")
|
||||||
|
|
||||||
|
# Изменение компактного вида
|
||||||
|
if self.default_compact:
|
||||||
|
main_file_path = "./decompiled/smali_classes3/com/swiftsoft/anixartd/ui/activity/MainActivity.smali"
|
||||||
|
main_lines = get_smali_lines(main_file_path)
|
||||||
|
|
||||||
|
preference_file_path = "./decompiled/smali_classes3/com/swiftsoft/anixartd/ui/fragment/main/preference/AppearancePreferenceFragment.smali"
|
||||||
|
preference_lines = get_smali_lines(preference_file_path)
|
||||||
|
|
||||||
|
main_const = find_smali_line(main_lines, "BOTTOM_NAVIGATION_COMPACT")[-1]
|
||||||
|
preference_const = find_smali_line(preference_lines, "BOTTOM_NAVIGATION_COMPACT")[-1]
|
||||||
|
|
||||||
|
main_invoke = find_smali_line(main_lines, "Landroid/content/SharedPreferences;->getBoolean(Ljava/lang/String;Z)Z")[0]
|
||||||
|
preference_invoke = find_smali_line(preference_lines, "Landroid/content/SharedPreferences;->getBoolean(Ljava/lang/String;Z)Z")[0]
|
||||||
|
|
||||||
|
main_value = set(find_smali_line(main_lines, "const/4 v7, 0x0"))
|
||||||
|
preference_value = set(find_smali_line(preference_lines, "const/4 v7, 0x0"))
|
||||||
|
|
||||||
|
main_target_line = main_value & set(range(main_const, main_invoke))
|
||||||
|
preference_tartget_line = preference_value & set(range(preference_const, preference_invoke))
|
||||||
|
|
||||||
|
assert len(main_target_line) == 1 and len(preference_tartget_line) == 1
|
||||||
|
|
||||||
|
main_lines[main_target_line.pop()] = "const/4 v7, 0x1"
|
||||||
|
preference_lines[preference_tartget_line.pop()] = "const/4 v7, 0x1"
|
||||||
|
|
||||||
|
save_smali_lines(main_file_path, main_lines)
|
||||||
|
save_smali_lines(preference_file_path, preference_lines)
|
||||||
|
|
||||||
|
return True
|
||||||
Binary file not shown.
@@ -0,0 +1,45 @@
|
|||||||
|
"""Делает текст в описании аниме копируемым
|
||||||
|
|
||||||
|
"selectable_text": {
|
||||||
|
"enabled": true
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
|
||||||
|
__author__ = "wowlikon <wowlikon@gmail.com>"
|
||||||
|
__version__ = "1.0.0"
|
||||||
|
from typing import Any, Dict
|
||||||
|
|
||||||
|
from lxml import etree
|
||||||
|
from pydantic import Field
|
||||||
|
|
||||||
|
from utils.config import PatchTemplate
|
||||||
|
|
||||||
|
|
||||||
|
class Patch(PatchTemplate):
|
||||||
|
priority: int = Field(frozen=True, exclude=True, default=0)
|
||||||
|
|
||||||
|
def apply(self, base: Dict[str, Any]) -> bool:
|
||||||
|
file_path = "./decompiled/res/layout/release_info.xml"
|
||||||
|
|
||||||
|
parser = etree.XMLParser(remove_blank_text=True)
|
||||||
|
tree = etree.parse(file_path, parser)
|
||||||
|
root = tree.getroot()
|
||||||
|
|
||||||
|
# Список тегов, к которым нужно добавить атрибут
|
||||||
|
tags = ["TextView", "at.blogc.android.views.ExpandableTextView"]
|
||||||
|
|
||||||
|
for tag in tags:
|
||||||
|
for element in root.findall(f".//{tag}", namespaces=base["xml_ns"]):
|
||||||
|
# Проверяем, нет ли уже атрибута
|
||||||
|
if (
|
||||||
|
f"{{{base['xml_ns']['android']}}}textIsSelectable"
|
||||||
|
not in element.attrib
|
||||||
|
):
|
||||||
|
element.set(
|
||||||
|
f"{{{base['xml_ns']['android']}}}textIsSelectable", "true"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Сохраняем
|
||||||
|
tree.write(file_path, encoding="utf-8", xml_declaration=True, pretty_print=True)
|
||||||
|
|
||||||
|
return True
|
||||||
+137
-31
@@ -1,41 +1,147 @@
|
|||||||
"""Add new settings"""
|
"""Добавляет в настройки ссылки и добвляет текст к версии приложения
|
||||||
priority = 0
|
|
||||||
|
"settings_urls": {
|
||||||
|
"enabled": true,
|
||||||
|
"menu": {
|
||||||
|
"Раздел": [
|
||||||
|
{
|
||||||
|
"title": "Заголовок",
|
||||||
|
"description": "Описание",
|
||||||
|
"url": "ссылка",
|
||||||
|
"icon": "@drawable/ic_custom_telegram",
|
||||||
|
"icon_space_reserved": "false"
|
||||||
|
},
|
||||||
|
...
|
||||||
|
],
|
||||||
|
...
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"version": " by wowlikon"
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
|
||||||
|
__author__ = "wowlikon <wowlikon@gmail.com>"
|
||||||
|
__version__ = "1.0.0"
|
||||||
|
import shutil
|
||||||
|
from typing import Any, Dict, List
|
||||||
|
|
||||||
from lxml import etree
|
from lxml import etree
|
||||||
|
from pydantic import Field
|
||||||
|
|
||||||
|
from utils.config import PatchTemplate
|
||||||
|
from utils.public import insert_after_public
|
||||||
|
|
||||||
|
# Config
|
||||||
|
DEFAULT_MENU = {
|
||||||
|
"Мы в социальных сетях": [
|
||||||
|
{
|
||||||
|
"title": "Мы в Telegram",
|
||||||
|
"description": "Подпишитесь на канал, чтобы быть в курсе последних новостей.",
|
||||||
|
"url": "https://t.me/http_teapod",
|
||||||
|
"icon": "@drawable/ic_custom_telegram",
|
||||||
|
"icon_space_reserved": "false",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"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": "Помочь проекту",
|
||||||
|
"description": "Вы можете помочь нам с идеями, написанием кода или тестированием.",
|
||||||
|
"url": "https://git.0x174.su/anixart-mod",
|
||||||
|
"icon": "@drawable/ic_custom_crown",
|
||||||
|
"icon_space_reserved": "false",
|
||||||
|
}
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def make_category(ns, name, items):
|
class Patch(PatchTemplate):
|
||||||
cat = etree.Element("PreferenceCategory", nsmap=ns)
|
priority: int = Field(frozen=True, exclude=True, default=0)
|
||||||
cat.set(f"{{{ns['android']}}}title", name)
|
version: str = Field(" by wowlikon", description="Суффикс версии")
|
||||||
cat.set(f"{{{ns['app']}}}iconSpaceReserved", "false")
|
menu: Dict[str, List[Dict[str, str]]] = Field(DEFAULT_MENU, description="Меню")
|
||||||
|
|
||||||
for item in items:
|
def make_category(self, ns, name, items):
|
||||||
pref = etree.SubElement(cat, "Preference", nsmap=ns)
|
cat = etree.Element("PreferenceCategory", nsmap=ns)
|
||||||
pref.set(f"{{{ns['android']}}}title", item["title"])
|
cat.set(f"{{{ns['android']}}}title", name)
|
||||||
pref.set(f"{{{ns['android']}}}summary", item["description"])
|
cat.set(f"{{{ns['app']}}}iconSpaceReserved", "false")
|
||||||
pref.set(f"{{{ns['app']}}}icon", item["icon"])
|
|
||||||
pref.set(f"{{{ns['app']}}}iconSpaceReserved", item["icon_space_reserved"])
|
|
||||||
|
|
||||||
intent = etree.SubElement(pref, "intent", nsmap=ns)
|
for item in items:
|
||||||
intent.set(f"{{{ns['android']}}}action", "android.intent.action.VIEW")
|
pref = etree.SubElement(cat, "Preference", nsmap=ns)
|
||||||
intent.set(f"{{{ns['android']}}}data", item["url"])
|
pref.set(f"{{{ns['android']}}}title", item["title"])
|
||||||
intent.set(f"{{{ns['app']}}}iconSpaceReserved", item["icon_space_reserved"])
|
pref.set(f"{{{ns['android']}}}summary", item["description"])
|
||||||
|
pref.set(f"{{{ns['app']}}}icon", item["icon"])
|
||||||
|
pref.set(f"{{{ns['app']}}}iconSpaceReserved", item["icon_space_reserved"])
|
||||||
|
|
||||||
return cat
|
intent = etree.SubElement(pref, "intent", nsmap=ns)
|
||||||
|
intent.set(f"{{{ns['android']}}}action", "android.intent.action.VIEW")
|
||||||
|
intent.set(f"{{{ns['android']}}}data", item["url"])
|
||||||
|
intent.set(f"{{{ns['app']}}}iconSpaceReserved", item["icon_space_reserved"])
|
||||||
|
|
||||||
def apply(config: dict) -> bool:
|
return cat
|
||||||
file_path = "./decompiled/res/xml/preference_main.xml"
|
|
||||||
parser = etree.XMLParser(remove_blank_text=True)
|
|
||||||
tree = etree.parse(file_path, parser)
|
|
||||||
root = tree.getroot()
|
|
||||||
|
|
||||||
# Insert new PreferenceCategory before the last element
|
def apply(self, base: Dict[str, Any]) -> bool:
|
||||||
last = root[-1] # last element
|
# Добавление кастомных иконок
|
||||||
pos = root.index(last)
|
shutil.copy(
|
||||||
for section, items in config["settings_urls"].items():
|
"./resources/ic_custom_crown.xml",
|
||||||
root.insert(pos, make_category(config["xml_ns"], section, items))
|
"./decompiled/res/drawable/ic_custom_crown.xml",
|
||||||
pos += 1
|
)
|
||||||
|
insert_after_public("warning_error_counter_background", "ic_custom_crown")
|
||||||
|
|
||||||
# Save back
|
shutil.copy(
|
||||||
tree.write(file_path, pretty_print=True, xml_declaration=True, encoding="utf-8")
|
"./resources/ic_custom_telegram.xml",
|
||||||
return True
|
"./decompiled/res/drawable/ic_custom_telegram.xml",
|
||||||
|
)
|
||||||
|
insert_after_public("warning_error_counter_background", "ic_custom_telegram")
|
||||||
|
|
||||||
|
file_path = "./decompiled/res/xml/preference_main.xml"
|
||||||
|
parser = etree.XMLParser(remove_blank_text=True)
|
||||||
|
tree = etree.parse(file_path, parser)
|
||||||
|
root = tree.getroot()
|
||||||
|
|
||||||
|
# Вставка новых пунктов перед последним
|
||||||
|
pos = root.index(root[-1])
|
||||||
|
for section, items in self.menu.items():
|
||||||
|
root.insert(pos, self.make_category(base["xml_ns"], section, items))
|
||||||
|
pos += 1
|
||||||
|
|
||||||
|
# Сохранение
|
||||||
|
tree.write(file_path, pretty_print=True, xml_declaration=True, encoding="utf-8")
|
||||||
|
|
||||||
|
# Добавление суффикса версии
|
||||||
|
filepaths = [
|
||||||
|
"./decompiled/smali_classes3/com/swiftsoft/anixartd/ui/activity/UpdateActivity.smali",
|
||||||
|
"./decompiled/smali_classes3/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('"')]
|
||||||
|
+ self.version
|
||||||
|
+ line[line.rindex('"') :]
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
content += line
|
||||||
|
with open(filepath, "w", encoding="utf-8") as file:
|
||||||
|
file.write(content)
|
||||||
|
return True
|
||||||
|
|||||||
@@ -0,0 +1,53 @@
|
|||||||
|
"""Изменяет формат "поделиться"
|
||||||
|
|
||||||
|
"selectable_text": {
|
||||||
|
"enabled": true,
|
||||||
|
"format": {
|
||||||
|
"share_channel_text": "Канал: «%1$s»\n%2$schannel/%3$d",
|
||||||
|
"share_collection_text": "Коллекция: «%1$s»\n%2$scollection/%3$d",
|
||||||
|
"share_profile_text": "Профиль пользователя «%1$s»\n%2$sprofile/%3$d",
|
||||||
|
"share_release_text": "Релиз: «%1$s»\n%2$srelease/%3$d"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
|
||||||
|
__author__ = "wowlikon <wowlikon@gmail.com>"
|
||||||
|
__version__ = "1.0.0"
|
||||||
|
from typing import Any, Dict
|
||||||
|
|
||||||
|
from lxml import etree
|
||||||
|
from pydantic import Field
|
||||||
|
|
||||||
|
from utils.config import PatchTemplate
|
||||||
|
|
||||||
|
DEFAULT_FORMATS = {
|
||||||
|
"share_channel_text": "Канал: «%1$s»\n%2$schannel/%3$d",
|
||||||
|
"share_collection_text": "Коллекция: «%1$s»\n%2$scollection/%3$d",
|
||||||
|
"share_profile_text": "Профиль пользователя «%1$s»\n%2$sprofile/%3$d",
|
||||||
|
"share_release_text": "Релиз: «%1$s»\n%2$srelease/%3$d",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class Patch(PatchTemplate):
|
||||||
|
priority: int = Field(frozen=True, exclude=True, default=0)
|
||||||
|
format: Dict[str, str] = Field(
|
||||||
|
DEFAULT_FORMATS, description="Строки для замены в `strings.xml`"
|
||||||
|
)
|
||||||
|
|
||||||
|
def apply(self, base: Dict[str, Any]) -> bool:
|
||||||
|
file_path = "./decompiled/res/values/strings.xml"
|
||||||
|
|
||||||
|
parser = etree.XMLParser(remove_blank_text=True)
|
||||||
|
tree = etree.parse(file_path, parser)
|
||||||
|
root = tree.getroot()
|
||||||
|
|
||||||
|
# Обновляем значения
|
||||||
|
for string in root.findall("string"):
|
||||||
|
name = string.get("name")
|
||||||
|
if name in self.format:
|
||||||
|
string.text = self.format[name]
|
||||||
|
|
||||||
|
# Сохраняем обратно
|
||||||
|
tree.write(file_path, encoding="utf-8", xml_declaration=True, pretty_print=True)
|
||||||
|
|
||||||
|
return True
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
"""Change application icon"""
|
|
||||||
priority = 0
|
|
||||||
|
|
||||||
|
|
||||||
def apply(config: dict) -> bool:
|
|
||||||
return False
|
|
||||||
@@ -1,14 +1,35 @@
|
|||||||
"""Change application icon"""
|
"""Добавляет пользовательские скорости воспроизведения видео
|
||||||
priority = 0
|
|
||||||
|
|
||||||
import struct
|
"custom_speed": {
|
||||||
|
"enabled": true,
|
||||||
|
"speeds": [9.0]
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
|
||||||
|
__author__ = "wowlikon <wowlikon@gmail.com>"
|
||||||
|
__version__ = "1.0.0"
|
||||||
|
from typing import Any, Dict, List
|
||||||
|
|
||||||
|
from pydantic import Field
|
||||||
|
|
||||||
|
from utils.config import PatchTemplate
|
||||||
|
from utils.public import insert_after_id, insert_after_public
|
||||||
|
from utils.smali_parser import float_to_hex
|
||||||
|
|
||||||
|
|
||||||
def float_to_hex(f):
|
class Patch(PatchTemplate):
|
||||||
b = struct.pack(">f", f)
|
priority: int = Field(frozen=True, exclude=True, default=0)
|
||||||
return b.hex()
|
speeds: List[float] = Field(
|
||||||
|
[9.0], description="Список пользовательских скоростей воспроизведения"
|
||||||
|
)
|
||||||
|
|
||||||
|
def apply(self, base: Dict[str, Any]) -> bool:
|
||||||
|
assert float_to_hex(1.5) == "0x3fc00000"
|
||||||
|
|
||||||
def apply(config: dict) -> bool:
|
last = "speed75"
|
||||||
assert float_to_hex(1.5) == "0x3fc00000"
|
for speed in self.speeds:
|
||||||
return False
|
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
|
||||||
|
|||||||
@@ -0,0 +1,53 @@
|
|||||||
|
"""Шаблон патча
|
||||||
|
|
||||||
|
Здесь вы можете добавить описание патча, его назначение и другие детали.
|
||||||
|
|
||||||
|
Каждый патч должен независеть от других патчей и проверять себя при применении. Он не должен вернуть True, если есть проблемы.
|
||||||
|
На данный момент каждый патч должен иметь функцию `apply`, которая принимает на вход конфигурацию и возвращает True или False.
|
||||||
|
И модель `Config`, которая наследуется от `PatchTemplate` (поле `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 // Пример кастомного параметра
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
|
||||||
|
__author__ = "wowlikon <wowlikon@gmail.com>"
|
||||||
|
__version__ = "1.0.0"
|
||||||
|
from typing import Any, Dict, List
|
||||||
|
|
||||||
|
from pydantic import Field
|
||||||
|
from tqdm import tqdm
|
||||||
|
|
||||||
|
from utils.config import PatchTemplate
|
||||||
|
|
||||||
|
|
||||||
|
class Patch(PatchTemplate):
|
||||||
|
example: bool = Field(True, description="Пример кастомного параметра")
|
||||||
|
|
||||||
|
def apply(
|
||||||
|
self, base: Dict[str, Any]
|
||||||
|
) -> bool: # Анотации типов для удобства, читаемости и поддержки IDE
|
||||||
|
priority: int = Field(
|
||||||
|
frozen=True, exclude=True, default=0
|
||||||
|
) # Приоритет патча, чем выше, тем раньше он будет применен
|
||||||
|
tqdm.write("Вывод информации через tqdm, чтобы не мешать прогресс-бару")
|
||||||
|
tqdm.write("Пример включен" if self.example else "Пример отключен")
|
||||||
|
if base["verbose"]:
|
||||||
|
tqdm.write(
|
||||||
|
"Для вывода подробной и отладочной информации используйте флаг --verbose"
|
||||||
|
)
|
||||||
|
return True
|
||||||
@@ -0,0 +1,98 @@
|
|||||||
|
"""Добавляет всплывающее окно при первом входе
|
||||||
|
|
||||||
|
"welcome": {
|
||||||
|
"enabled": true,
|
||||||
|
"title": "Anixarty",
|
||||||
|
"description": "Описание",
|
||||||
|
"link_text": "МЫ В TELEGRAM",
|
||||||
|
"link_url": "https://t.me/http_teapod",
|
||||||
|
"skip_text": "Пропустить",
|
||||||
|
"title_bg_color": "#FFFFFF"
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
|
||||||
|
__author__ = "wowlikon <wowlikon@gmail.com>"
|
||||||
|
__version__ = "1.0.0"
|
||||||
|
import shutil
|
||||||
|
from typing import Any, Dict
|
||||||
|
from urllib import parse
|
||||||
|
|
||||||
|
from pydantic import Field
|
||||||
|
|
||||||
|
from utils.config import PatchTemplate
|
||||||
|
from utils.smali_parser import (find_and_replace_smali_line, get_smali_lines,
|
||||||
|
save_smali_lines)
|
||||||
|
|
||||||
|
|
||||||
|
class Patch(PatchTemplate):
|
||||||
|
priority: int = Field(frozen=True, exclude=True, default=0)
|
||||||
|
|
||||||
|
title: str = Field("Anixarty", description="Заголовок")
|
||||||
|
title_color: str = Field("#FF252525", description="Цвет заголовка")
|
||||||
|
|
||||||
|
title_bg_color: str = Field("#FFCFF04D", description="Цвет фона заголовка")
|
||||||
|
body_bg_color: str = Field("#FF252525", description="Цвет фона окна")
|
||||||
|
|
||||||
|
description: str = Field("Описание", description="Описание")
|
||||||
|
description_color: str = Field("#FFFFFFFF", description="Цвет описания")
|
||||||
|
|
||||||
|
skip_text: str = Field("Пропустить", description="Текст кнопки пропустить")
|
||||||
|
skip_color: str = Field("#FFFFFFFF", description="Цвет кнопки пропустить")
|
||||||
|
|
||||||
|
link_text: str = Field("МЫ В TELEGRAM", description="Текст ссылки")
|
||||||
|
link_color: str = Field("#FFCFF04D", description="Цвет ссылки")
|
||||||
|
link_url: str = Field("https://t.me/http_teapod", description="Ссылка")
|
||||||
|
|
||||||
|
def encode_text(self, text: str) -> str:
|
||||||
|
return '+'.join([parse.quote(i) for i in text.split(' ')])
|
||||||
|
|
||||||
|
def apply(self, 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",
|
||||||
|
)
|
||||||
|
for subdir in ["about/", "authorization/"]:
|
||||||
|
shutil.copytree("./resources/smali_classes4/com/swiftsoft/"+subdir, "./decompiled/smali_classes4/com/swiftsoft/"+subdir)
|
||||||
|
|
||||||
|
# Привязка к первому запуску
|
||||||
|
file_path = "./decompiled/smali_classes3/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)
|
||||||
|
|
||||||
|
# Замена ссылки
|
||||||
|
file_path = "./decompiled/smali_classes4/com/swiftsoft/about/$4.smali"
|
||||||
|
lines = get_smali_lines(file_path)
|
||||||
|
lines = find_and_replace_smali_line(
|
||||||
|
lines,
|
||||||
|
"const-string v0, \"https://example.com\"",
|
||||||
|
'const-string v0, "' + self.link_url + '"',
|
||||||
|
)
|
||||||
|
save_smali_lines(file_path, lines)
|
||||||
|
|
||||||
|
# Настройка всплывающго окна
|
||||||
|
file_path = "./decompiled/smali_classes4/com/swiftsoft/about/$2.smali"
|
||||||
|
lines = get_smali_lines(file_path)
|
||||||
|
for replacement in [
|
||||||
|
('const-string v5, "#FF252525" # Title color', f'const-string v5, "{self.title_color}"'),
|
||||||
|
('const-string v7, "#FFFFFFFF" # Description color', f'const-string v7, "{self.description_color}"'),
|
||||||
|
('const-string v8, "#FFCFF04D" # Link color', f'const-string v8, "{self.link_color}"'),
|
||||||
|
('const-string v9, "#FFFFFFFF" # Skip color', f'const-string v9, "{self.skip_color}"'),
|
||||||
|
('const-string v5, "#FF252525" # Body background', f'const-string v5, "{self.body_bg_color}"'),
|
||||||
|
('const-string v10, "#FFCFF04D" # Title background', f'const-string v10, "{self.title_bg_color}"'),
|
||||||
|
('const-string v12, "Title"', f'const-string v12, "{self.encode_text(self.title)}"'),
|
||||||
|
('const-string v11, "Description"', f'const-string v11, "{self.encode_text(self.description)}"'),
|
||||||
|
('const-string v12, "URL"', f'const-string v12, "{self.link_text.encode('unicode-escape').decode()}"'),
|
||||||
|
('const-string v12, "Skip"', f'const-string v12, "{self.skip_text.encode('unicode-escape').decode()}"')
|
||||||
|
]: lines = find_and_replace_smali_line(lines, *replacement)
|
||||||
|
save_smali_lines(file_path, lines)
|
||||||
|
|
||||||
|
return True
|
||||||
@@ -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.
@@ -0,0 +1,11 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="24"
|
||||||
|
android:viewportHeight="24">
|
||||||
|
<path
|
||||||
|
android:pathData="M12.59 23.26l-0.01 0-0.07 0.03-0.02 0.01-0.01-0.01-0.07-0.03q-0.02-0.01-0.03 0l0 0.01-0.02 0.43 0.01 0.02 0.01 0.02 0.1 0.07 0.02 0 0.01 0 0.1-0.07 0.01-0.02 0.01-0.02-0.02-0.42q0-0.02-0.02-0.02m0.27-0.12l-0.01 0.01-0.19 0.09-0.01 0.01 0 0.01 0.02 0.43 0 0.01 0.01 0.01 0.2 0.09q0.02 0.01 0.03-0.01l0-0.01-0.03-0.61q-0.01-0.02-0.02-0.03m-0.72 0.01a0.02 0.02 0 0 0-0.02 0l-0.01 0.02-0.03 0.61q0 0.02 0.01 0.02l0.02 0 0.2-0.09 0.01-0.01 0-0.01 0.02-0.43 0-0.01-0.01-0.01z"/>
|
||||||
|
<path
|
||||||
|
android:fillColor="#FF000000"
|
||||||
|
android:pathData="M15 16h2.4a4 4 0 0 0 3.94-4.72l-0.91-5A4 4 0 0 0 16.5 3H8v12l1.82 5.79c0.3 0.69 1.06 1.32 2.02 1.13C13.37 21.63 15 20.43 15 18.5z m-9-1a3 3 0 0 1-3-3V6a3 3 0 0 1 3-3z"/>
|
||||||
|
</vector>
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="24"
|
||||||
|
android:viewportHeight="24">
|
||||||
|
<path
|
||||||
|
android:pathData="M12.59 23.26l-0.01 0-0.07 0.03-0.02 0.01-0.01-0.01-0.07-0.03q-0.02-0.01-0.03 0l0 0.01-0.02 0.43 0.01 0.02 0.01 0.02 0.1 0.07 0.02 0 0.01 0 0.1-0.07 0.01-0.02 0.01-0.02-0.02-0.42q0-0.02-0.02-0.02m0.27-0.12l-0.01 0.01-0.19 0.09-0.01 0.01 0 0.01 0.02 0.43 0 0.01 0.01 0.01 0.2 0.09q0.02 0.01 0.03-0.01l0-0.01-0.03-0.61q-0.01-0.02-0.02-0.03m-0.72 0.01a0.02 0.02 0 0 0-0.02 0l-0.01 0.02-0.03 0.61q0 0.02 0.01 0.02l0.02 0 0.2-0.09 0.01-0.01 0-0.01 0.02-0.43 0-0.01-0.01-0.01z"/>
|
||||||
|
<path
|
||||||
|
android:fillType="evenOdd"
|
||||||
|
android:fillColor="#FF000000"
|
||||||
|
android:pathData="M16.5 3a4 4 0 0 1 3.93 3.28l0.91 5a4 4 0 0 1-3.94 4.72H15v2.5c0 1.93-1.63 3.12-3.15 3.42-0.96 0.18-1.73-0.44-2.03-1.13l-2.48-5.79H6a3 3 0 0 1-3-3v-6a3 3 0 0 1 3-3z m0 2H9v8.59a1 1 0 0 0 0.08 0.39l2.54 5.94c0.88-0.22 1.38-0.83 1.38-1.42v-2.5a2 2 0 0 1 2-2h2.4a2 2 0 0 0 1.97-2.36l-0.91-5a2 2 0 0 0-1.96-1.64M7 5H6a1 1 0 0 0-0.99 0.88L5 6v6a1 1 0 0 0 0.88 0.99l0.12 0.01h1z"/>
|
||||||
|
</vector>
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="24"
|
||||||
|
android:viewportHeight="24">
|
||||||
|
<path
|
||||||
|
android:pathData="M12.59 23.26l-0.01 0-0.07 0.03-0.02 0.01-0.01-0.01-0.07-0.03q-0.02-0.01-0.03 0l0 0.01-0.02 0.43 0.01 0.02 0.01 0.02 0.1 0.07 0.02 0 0.01 0 0.1-0.07 0.01-0.02 0.01-0.02-0.02-0.42q0-0.02-0.02-0.02m0.27-0.12l-0.01 0.01-0.19 0.09-0.01 0.01 0 0.01 0.02 0.43 0 0.01 0.01 0.01 0.2 0.09q0.02 0.01 0.03-0.01l0-0.01-0.03-0.61q-0.01-0.02-0.02-0.03m-0.72 0.01a0.02 0.02 0 0 0-0.02 0l-0.01 0.02-0.03 0.61q0 0.02 0.01 0.02l0.02 0 0.2-0.09 0.01-0.01 0-0.01 0.02-0.43 0-0.01-0.01-0.01z"/>
|
||||||
|
<path
|
||||||
|
android:fillColor="#FF000000"
|
||||||
|
android:pathData="M15 8h2.4a4 4 0 0 1 3.94 4.72l-0.91 5A4 4 0 0 1 16.5 21H8V9l1.82-5.79c0.3-0.69 1.06-1.32 2.02-1.13C13.37 2.38 15 3.57 15 5.5zM6 9a3 3 0 0 0-3 3v6a3 3 0 0 0 3 3z"/>
|
||||||
|
</vector>
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="24"
|
||||||
|
android:viewportHeight="24">
|
||||||
|
<path
|
||||||
|
android:pathData="M12.59 23.26l-0.01 0-0.07 0.03-0.02 0.01-0.01-0.01-0.07-0.03q-0.02-0.01-0.03 0l0 0.01-0.02 0.43 0.01 0.02 0.01 0.02 0.1 0.07 0.02 0 0.01 0 0.1-0.07 0.01-0.02 0.01-0.02-0.02-0.42q0-0.02-0.02-0.02m0.27-0.12l-0.01 0.01-0.19 0.09-0.01 0.01 0 0.01 0.02 0.43 0 0.01 0.01 0.01 0.2 0.09q0.02 0.01 0.03-0.01l0-0.01-0.03-0.61q-0.01-0.02-0.02-0.03m-0.72 0.01a0.02 0.02 0 0 0-0.02 0l-0.01 0.02-0.03 0.61q0 0.02 0.01 0.02l0.02 0 0.2-0.09 0.01-0.01 0-0.01 0.02-0.43 0-0.01-0.01-0.01z"/>
|
||||||
|
<path
|
||||||
|
android:fillType="evenOdd"
|
||||||
|
android:fillColor="#FF000000"
|
||||||
|
android:pathData="M9.82 3.21c0.3-0.69 1.06-1.32 2.02-1.13 1.47 0.28 3.04 1.4 3.15 3.22L15 5.5V8h2.4a4 4 0 0 1 3.97 4.52l-0.03 0.2-0.91 5a4 4 0 0 1-3.74 3.28l-0.19 0H6a3 3 0 0 1-3-2.82L3 18v-6a3 3 0 0 1 2.82-3L6 9h1.34zM7 11H6a1 1 0 0 0-1 1v6a1 1 0 0 0 1 1h1z m4.63-6.92l-2.55 5.94a1 1 0 0 0-0.07 0.26L9 10.41V19h7.5a2 2 0 0 0 1.93-1.49l0.03-0.15 0.91-5a2 2 0 0 0-1.82-2.35L17.41 10H15a2 2 0 0 1-2-1.85L13 8V5.5c0-0.55-0.43-1.12-1.21-1.37z"/>
|
||||||
|
</vector>
|
||||||
+10
-10
@@ -95,28 +95,28 @@
|
|||||||
|
|
||||||
move-result-object v2
|
move-result-object v2
|
||||||
|
|
||||||
const-string v5, "#FF252525"
|
const-string v5, "#FF252525" # Title color
|
||||||
|
|
||||||
.line 43
|
.line 43
|
||||||
invoke-static {v5}, Landroid/graphics/Color;->parseColor(Ljava/lang/String;)I
|
invoke-static {v5}, Landroid/graphics/Color;->parseColor(Ljava/lang/String;)I
|
||||||
|
|
||||||
move-result v6
|
move-result v6
|
||||||
|
|
||||||
const-string v7, "#FFFFFFFF"
|
const-string v7, "#FFFFFFFF" # Description color
|
||||||
|
|
||||||
.line 44
|
.line 44
|
||||||
invoke-static {v7}, Landroid/graphics/Color;->parseColor(Ljava/lang/String;)I
|
invoke-static {v7}, Landroid/graphics/Color;->parseColor(Ljava/lang/String;)I
|
||||||
|
|
||||||
move-result v7
|
move-result v7
|
||||||
|
|
||||||
const-string v8, "#FFCFF04D"
|
const-string v8, "#FFCFF04D" # Link color
|
||||||
|
|
||||||
.line 45
|
.line 45
|
||||||
invoke-static {v8}, Landroid/graphics/Color;->parseColor(Ljava/lang/String;)I
|
invoke-static {v8}, Landroid/graphics/Color;->parseColor(Ljava/lang/String;)I
|
||||||
|
|
||||||
move-result v8
|
move-result v8
|
||||||
|
|
||||||
const-string v9, "#FFFFFFFF"
|
const-string v9, "#FFFFFFFF" # Skip color
|
||||||
|
|
||||||
.line 46
|
.line 46
|
||||||
invoke-static {v9}, Landroid/graphics/Color;->parseColor(Ljava/lang/String;)I
|
invoke-static {v9}, Landroid/graphics/Color;->parseColor(Ljava/lang/String;)I
|
||||||
@@ -124,13 +124,13 @@
|
|||||||
move-result v9
|
move-result v9
|
||||||
|
|
||||||
.line 47
|
.line 47
|
||||||
const-string v5, "#FF252525"
|
const-string v5, "#FF252525" # Body background
|
||||||
|
|
||||||
invoke-static {v5}, Landroid/graphics/Color;->parseColor(Ljava/lang/String;)I
|
invoke-static {v5}, Landroid/graphics/Color;->parseColor(Ljava/lang/String;)I
|
||||||
|
|
||||||
move-result v5
|
move-result v5
|
||||||
|
|
||||||
const-string v10, "#FFCFF04D"
|
const-string v10, "#FFCFF04D" # Title background
|
||||||
|
|
||||||
.line 48
|
.line 48
|
||||||
invoke-static {v10}, Landroid/graphics/Color;->parseColor(Ljava/lang/String;)I
|
invoke-static {v10}, Landroid/graphics/Color;->parseColor(Ljava/lang/String;)I
|
||||||
@@ -177,7 +177,7 @@
|
|||||||
|
|
||||||
invoke-direct {v11, v0, v12}, Landroid/app/AlertDialog$Builder;-><init>(Landroid/content/Context;I)V
|
invoke-direct {v11, v0, v12}, Landroid/app/AlertDialog$Builder;-><init>(Landroid/content/Context;I)V
|
||||||
|
|
||||||
const-string v12, "wowlikon+ID"
|
const-string v12, "Title"
|
||||||
|
|
||||||
.line 67
|
.line 67
|
||||||
invoke-virtual {v11, v12}, Landroid/app/AlertDialog$Builder;->setTitle(Ljava/lang/CharSequence;)Landroid/app/AlertDialog$Builder;
|
invoke-virtual {v11, v12}, Landroid/app/AlertDialog$Builder;->setTitle(Ljava/lang/CharSequence;)Landroid/app/AlertDialog$Builder;
|
||||||
@@ -189,7 +189,7 @@
|
|||||||
|
|
||||||
move-result-object v3
|
move-result-object v3
|
||||||
|
|
||||||
const-string v11, "%D0%9C%D0%BE%D0%B4+%D1%81%D0%B4%D0%B5%D0%BB%D0%B0%D0%BD+wowlikon+%D1%81+%D0%BD%D0%BE%D0%B2%D1%8B%D1%8B%D0%BC%D0%B8+%D1%84%D1%83%D0%BD%D0%BA%D1%86%D0%B8%D1%8F%D0%BC%D0%B8%21%0A%0A%D0%A1%D0%B4%D0%B5%D0%BB%D0%B0%D0%BD%D0%BE+%D1%81+%E2%9D%A4%EF%B8%8F+%D0%BE%D1%82+swiftsoft"
|
const-string v11, "Description"
|
||||||
|
|
||||||
.line 69
|
.line 69
|
||||||
invoke-virtual {v3, v11}, Landroid/app/AlertDialog$Builder;->setMessage(Ljava/lang/CharSequence;)Landroid/app/AlertDialog$Builder;
|
invoke-virtual {v3, v11}, Landroid/app/AlertDialog$Builder;->setMessage(Ljava/lang/CharSequence;)Landroid/app/AlertDialog$Builder;
|
||||||
@@ -200,7 +200,7 @@
|
|||||||
|
|
||||||
invoke-direct {v11}, Lcom/swiftsoft/about/$4;-><init>()V
|
invoke-direct {v11}, Lcom/swiftsoft/about/$4;-><init>()V
|
||||||
|
|
||||||
const-string v12, "\u041c\u044b \u0432 Telegram"
|
const-string v12, "URL"
|
||||||
|
|
||||||
.line 70
|
.line 70
|
||||||
invoke-virtual {v3, v12, v11}, Landroid/app/AlertDialog$Builder;->setPositiveButton(Ljava/lang/CharSequence;Landroid/content/DialogInterface$OnClickListener;)Landroid/app/AlertDialog$Builder;
|
invoke-virtual {v3, v12, v11}, Landroid/app/AlertDialog$Builder;->setPositiveButton(Ljava/lang/CharSequence;Landroid/content/DialogInterface$OnClickListener;)Landroid/app/AlertDialog$Builder;
|
||||||
@@ -211,7 +211,7 @@
|
|||||||
|
|
||||||
invoke-direct {v11}, Lcom/swiftsoft/about/$3;-><init>()V
|
invoke-direct {v11}, Lcom/swiftsoft/about/$3;-><init>()V
|
||||||
|
|
||||||
const-string v12, "\u041f\u043e\u043d\u044f\u0442\u043d\u043e"
|
const-string v12, "Skip"
|
||||||
|
|
||||||
.line 71
|
.line 71
|
||||||
invoke-virtual {v3, v12, v11}, Landroid/app/AlertDialog$Builder;->setNeutralButton(Ljava/lang/CharSequence;Landroid/content/DialogInterface$OnClickListener;)Landroid/app/AlertDialog$Builder;
|
invoke-virtual {v3, v12, v11}, Landroid/app/AlertDialog$Builder;->setNeutralButton(Ljava/lang/CharSequence;Landroid/content/DialogInterface$OnClickListener;)Landroid/app/AlertDialog$Builder;
|
||||||
+1
-1
@@ -32,7 +32,7 @@
|
|||||||
|
|
||||||
new-instance p2, Landroid/content/Intent;
|
new-instance p2, Landroid/content/Intent;
|
||||||
|
|
||||||
const-string v0, "https://t.me/wowlikon"
|
const-string v0, "https://example.com"
|
||||||
|
|
||||||
invoke-static {v0}, Landroid/net/Uri;->parse(Ljava/lang/String;)Landroid/net/Uri;
|
invoke-static {v0}, Landroid/net/Uri;->parse(Ljava/lang/String;)Landroid/net/Uri;
|
||||||
|
|
||||||
+27
-10
@@ -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" />
|
||||||
|
|||||||
@@ -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" />
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
/res/menu/bottom.xml
|
|
||||||
replace lines
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
<color name="ic_launcher_background">#ff000000</color>
|
|
||||||
+228
@@ -0,0 +1,228 @@
|
|||||||
|
import xml.etree.ElementTree as ET
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
import yaml
|
||||||
|
from pydantic import BaseModel, Field, computed_field
|
||||||
|
from rich.console import Console
|
||||||
|
|
||||||
|
from utils.tools import DECOMPILED, MODIFIED, TOOLS, run
|
||||||
|
|
||||||
|
|
||||||
|
class APKMeta(BaseModel):
|
||||||
|
"""Метаданные APK файла"""
|
||||||
|
|
||||||
|
version_code: int = Field(default=0)
|
||||||
|
version_name: str = Field(default="unknown")
|
||||||
|
package: str = Field(default="unknown")
|
||||||
|
path: Path
|
||||||
|
|
||||||
|
@computed_field
|
||||||
|
@property
|
||||||
|
def safe_version(self) -> str:
|
||||||
|
"""Версия, безопасная для использования в именах файлов"""
|
||||||
|
return self.version_name.lower().replace(" ", "-").replace(".", "-")
|
||||||
|
|
||||||
|
@computed_field
|
||||||
|
@property
|
||||||
|
def output_name(self) -> str:
|
||||||
|
"""Имя выходного файла"""
|
||||||
|
return f"Anixarty-v{self.safe_version}.apk"
|
||||||
|
|
||||||
|
@computed_field
|
||||||
|
@property
|
||||||
|
def aligned_name(self) -> str:
|
||||||
|
"""Имя выровненного файла"""
|
||||||
|
return f"Anixarty-v{self.safe_version}-aligned.apk"
|
||||||
|
|
||||||
|
@computed_field
|
||||||
|
@property
|
||||||
|
def signed_name(self) -> str:
|
||||||
|
"""Имя подписанного файла"""
|
||||||
|
return f"Anixarty-v{self.safe_version}-mod.apk"
|
||||||
|
|
||||||
|
|
||||||
|
class SigningConfig(BaseModel):
|
||||||
|
"""Конфигурация подписи APK"""
|
||||||
|
|
||||||
|
keystore: Path = Field(default=Path("keystore.jks"))
|
||||||
|
keystore_pass_file: Path = Field(default=Path("keystore.pass"))
|
||||||
|
v1_signing: bool = Field(default=False)
|
||||||
|
v2_signing: bool = Field(default=True)
|
||||||
|
v3_signing: bool = Field(default=True)
|
||||||
|
|
||||||
|
|
||||||
|
class APKProcessor:
|
||||||
|
"""Класс для работы с APK файлами"""
|
||||||
|
|
||||||
|
def __init__(self, console: Console, tools_dir: Path = TOOLS):
|
||||||
|
self.console = console
|
||||||
|
self.tools_dir = tools_dir
|
||||||
|
self.apktool_jar = tools_dir / "apktool.jar"
|
||||||
|
|
||||||
|
def decompile(self, apk: Path, output: Path = DECOMPILED) -> None:
|
||||||
|
"""Декомпилирует APK файл"""
|
||||||
|
self.console.print("[yellow]Декомпиляция APK...")
|
||||||
|
run(
|
||||||
|
self.console,
|
||||||
|
[
|
||||||
|
"java",
|
||||||
|
"-jar",
|
||||||
|
str(self.apktool_jar),
|
||||||
|
"d",
|
||||||
|
"-f",
|
||||||
|
"-o",
|
||||||
|
str(output),
|
||||||
|
str(apk),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
self.console.print("[green]✔ Декомпиляция завершена")
|
||||||
|
|
||||||
|
def compile(self, source: Path, output: Path) -> None:
|
||||||
|
"""Компилирует APK из исходников"""
|
||||||
|
self.console.print("[yellow]Сборка APK...")
|
||||||
|
run(
|
||||||
|
self.console,
|
||||||
|
[
|
||||||
|
"java",
|
||||||
|
"-jar",
|
||||||
|
str(self.apktool_jar),
|
||||||
|
"b",
|
||||||
|
str(source),
|
||||||
|
"-o",
|
||||||
|
str(output),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
self.console.print("[green]✔ Сборка завершена")
|
||||||
|
|
||||||
|
def align(self, input_apk: Path, output_apk: Path) -> None:
|
||||||
|
"""Выравнивает APK с помощью zipalign"""
|
||||||
|
self.console.print("[yellow]Выравнивание APK...")
|
||||||
|
run(
|
||||||
|
self.console, ["zipalign", "-f", "-v", "4", str(input_apk), str(output_apk)]
|
||||||
|
)
|
||||||
|
self.console.print("[green]✔ Выравнивание завершено")
|
||||||
|
|
||||||
|
def sign(
|
||||||
|
self,
|
||||||
|
input_apk: Path,
|
||||||
|
output_apk: Path,
|
||||||
|
config: Optional[SigningConfig] = None,
|
||||||
|
) -> None:
|
||||||
|
"""Подписывает APK"""
|
||||||
|
if config is None:
|
||||||
|
config = SigningConfig()
|
||||||
|
|
||||||
|
self.console.print("[yellow]Подпись APK...")
|
||||||
|
run(
|
||||||
|
self.console,
|
||||||
|
[
|
||||||
|
"apksigner",
|
||||||
|
"sign",
|
||||||
|
"--v1-signing-enabled",
|
||||||
|
str(config.v1_signing).lower(),
|
||||||
|
"--v2-signing-enabled",
|
||||||
|
str(config.v2_signing).lower(),
|
||||||
|
"--v3-signing-enabled",
|
||||||
|
str(config.v3_signing).lower(),
|
||||||
|
"--ks",
|
||||||
|
str(config.keystore),
|
||||||
|
"--ks-pass",
|
||||||
|
f"file:{config.keystore_pass_file}",
|
||||||
|
"--out",
|
||||||
|
str(output_apk),
|
||||||
|
str(input_apk),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
self.console.print("[green]✔ Подпись завершена")
|
||||||
|
|
||||||
|
def _get_package_name_from_manifest(self, decompiled_path: Path) -> str:
|
||||||
|
"""Читает имя пакета напрямую из AndroidManifest.xml"""
|
||||||
|
manifest_path = decompiled_path / "AndroidManifest.xml"
|
||||||
|
if not manifest_path.exists():
|
||||||
|
return "unknown"
|
||||||
|
|
||||||
|
try:
|
||||||
|
tree = ET.parse(manifest_path)
|
||||||
|
root = tree.getroot()
|
||||||
|
return root.get("package", "unknown")
|
||||||
|
except Exception:
|
||||||
|
return "unknown"
|
||||||
|
|
||||||
|
def get_meta(self, decompiled: Path = DECOMPILED) -> APKMeta:
|
||||||
|
"""Извлекает метаданные из декомпилированного APK"""
|
||||||
|
apktool_yml = decompiled / "apktool.yml"
|
||||||
|
|
||||||
|
if not apktool_yml.exists():
|
||||||
|
raise FileNotFoundError(f"Файл {apktool_yml} не найден")
|
||||||
|
|
||||||
|
with open(apktool_yml, encoding="utf-8") as f:
|
||||||
|
meta = yaml.safe_load(f)
|
||||||
|
|
||||||
|
version_info = meta.get("versionInfo", {})
|
||||||
|
|
||||||
|
package_name = self._get_package_name_from_manifest(decompiled)
|
||||||
|
|
||||||
|
if package_name == "unknown":
|
||||||
|
package_info = meta_yaml.get("packageInfo", {})
|
||||||
|
package_name = package_info.get("renameManifestPackage") or "unknown"
|
||||||
|
|
||||||
|
return APKMeta(
|
||||||
|
version_code=version_info.get("versionCode", 0),
|
||||||
|
version_name=version_info.get("versionName", "unknown"),
|
||||||
|
package=package_name,
|
||||||
|
path=decompiled,
|
||||||
|
)
|
||||||
|
|
||||||
|
def _extract_package_from_manifest(self, decompiled: Path) -> str | None:
|
||||||
|
"""Извлекает имя пакета из AndroidManifest.xml"""
|
||||||
|
manifest = decompiled / "AndroidManifest.xml"
|
||||||
|
|
||||||
|
if not manifest.exists():
|
||||||
|
return None
|
||||||
|
|
||||||
|
try:
|
||||||
|
import re
|
||||||
|
|
||||||
|
content = manifest.read_text(encoding="utf-8")
|
||||||
|
match = re.search(r'package="([^"]+)"', content)
|
||||||
|
if match:
|
||||||
|
return match.group(1)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
def build_and_sign(
|
||||||
|
self,
|
||||||
|
source: Path = DECOMPILED,
|
||||||
|
output_dir: Path = MODIFIED,
|
||||||
|
signing_config: Optional[SigningConfig] = None,
|
||||||
|
cleanup: bool = True,
|
||||||
|
) -> tuple[Path, APKMeta]:
|
||||||
|
"""
|
||||||
|
Полный цикл сборки: компиляция, выравнивание, подпись.
|
||||||
|
Возвращает путь к подписанному APK и метаданные.
|
||||||
|
"""
|
||||||
|
meta = self.get_meta(source)
|
||||||
|
|
||||||
|
out_apk = output_dir / meta.output_name
|
||||||
|
aligned_apk = output_dir / meta.aligned_name
|
||||||
|
signed_apk = output_dir / meta.signed_name
|
||||||
|
|
||||||
|
for f in [out_apk, aligned_apk, signed_apk]:
|
||||||
|
f.unlink(missing_ok=True)
|
||||||
|
|
||||||
|
self.compile(source, out_apk)
|
||||||
|
self.align(out_apk, aligned_apk)
|
||||||
|
self.sign(aligned_apk, signed_apk, signing_config)
|
||||||
|
|
||||||
|
if cleanup:
|
||||||
|
out_apk.unlink(missing_ok=True)
|
||||||
|
aligned_apk.unlink(missing_ok=True)
|
||||||
|
idsig = signed_apk.with_suffix(".apk.idsig")
|
||||||
|
idsig.unlink(missing_ok=True)
|
||||||
|
|
||||||
|
self.console.print(f"[green]✔ APK готов: {signed_apk.name}")
|
||||||
|
|
||||||
|
return signed_apk, meta
|
||||||
+137
@@ -0,0 +1,137 @@
|
|||||||
|
import json
|
||||||
|
import traceback
|
||||||
|
from abc import ABC, abstractmethod
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Any, Dict, Literal
|
||||||
|
|
||||||
|
from pydantic import BaseModel, ConfigDict, Field, PrivateAttr, field_validator
|
||||||
|
from rich.console import Console
|
||||||
|
|
||||||
|
from utils.tools import CONFIGS
|
||||||
|
|
||||||
|
|
||||||
|
class ToolsConfig(BaseModel):
|
||||||
|
apktool_jar_url: str
|
||||||
|
apktool_wrapper_url: str
|
||||||
|
|
||||||
|
@field_validator("apktool_jar_url", "apktool_wrapper_url")
|
||||||
|
@classmethod
|
||||||
|
def validate_url(cls, v: str) -> str:
|
||||||
|
if not v.startswith(("http://", "https://")):
|
||||||
|
raise ValueError("URL должен начинаться с http:// или https://")
|
||||||
|
return v
|
||||||
|
|
||||||
|
|
||||||
|
class SigningConfig(BaseModel):
|
||||||
|
keystore: Path = Field(default=Path("keystore.jks"))
|
||||||
|
keystore_pass_file: Path = Field(default=Path("keystore.pass"))
|
||||||
|
v1_signing: bool = False
|
||||||
|
v2_signing: bool = True
|
||||||
|
v3_signing: bool = True
|
||||||
|
|
||||||
|
|
||||||
|
class BuildConfig(BaseModel):
|
||||||
|
verbose: bool = False
|
||||||
|
force: bool = False
|
||||||
|
clean_after_build: bool = True
|
||||||
|
|
||||||
|
|
||||||
|
class Config(BaseModel):
|
||||||
|
tools: ToolsConfig
|
||||||
|
signing: SigningConfig = Field(default_factory=SigningConfig)
|
||||||
|
build: BuildConfig = Field(default_factory=BuildConfig)
|
||||||
|
base: Dict[str, Any] = Field(default_factory=dict)
|
||||||
|
|
||||||
|
|
||||||
|
def load_config(console: Console) -> Config:
|
||||||
|
"""Загружает и валидирует конфигурацию"""
|
||||||
|
config_path = Path("config.json")
|
||||||
|
|
||||||
|
if not config_path.exists():
|
||||||
|
console.print("[red]Файл config.json не найден")
|
||||||
|
raise typer.Exit(1)
|
||||||
|
|
||||||
|
try:
|
||||||
|
return Config.model_validate_json(config_path.read_text())
|
||||||
|
except ValidationError as e:
|
||||||
|
console.print(f"[red]Ошибка валидации config.json:\n{e}")
|
||||||
|
raise typer.Exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
class PatchTemplate(BaseModel, ABC):
|
||||||
|
model_config = ConfigDict(arbitrary_types_allowed=True, validate_default=True)
|
||||||
|
|
||||||
|
enabled: bool = Field(default=True, description="Включить или отключить патч")
|
||||||
|
priority: int = Field(default=0, description="Приоритет применения патча")
|
||||||
|
|
||||||
|
_name: str = PrivateAttr()
|
||||||
|
_applied: bool = PrivateAttr(default=False)
|
||||||
|
_console: Console | None = PrivateAttr(default=None)
|
||||||
|
|
||||||
|
def __init__(self, name: str, console: Console, **data):
|
||||||
|
loaded_data = self._load_config_static(name, console)
|
||||||
|
|
||||||
|
merged_data = {**loaded_data, **data}
|
||||||
|
|
||||||
|
valid_fields = set(self.model_fields.keys())
|
||||||
|
filtered_data = {k: v for k, v in merged_data.items() if k in valid_fields}
|
||||||
|
|
||||||
|
super().__init__(**filtered_data)
|
||||||
|
self._name = name
|
||||||
|
self._console = console
|
||||||
|
self._applied = False
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _load_config_static(name: str, console: Console | None) -> Dict[str, Any]:
|
||||||
|
"""Загружает конфигурацию из файла (статический метод)"""
|
||||||
|
config_path = CONFIGS / f"{name}.json"
|
||||||
|
try:
|
||||||
|
if config_path.exists():
|
||||||
|
return json.loads(config_path.read_text())
|
||||||
|
except Exception as e:
|
||||||
|
if console:
|
||||||
|
console.print(
|
||||||
|
f"[red]Ошибка при загрузке конфигурации патча {name}: {e}"
|
||||||
|
)
|
||||||
|
console.print(f"[yellow]Используются значения по умолчанию")
|
||||||
|
return {}
|
||||||
|
|
||||||
|
def save_config(self) -> None:
|
||||||
|
"""Сохраняет конфигурацию в файл"""
|
||||||
|
config_path = CONFIGS / f"{self._name}.json"
|
||||||
|
config_path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
config_path.write_text(self.model_dump_json(indent=2))
|
||||||
|
|
||||||
|
@property
|
||||||
|
def name(self) -> str:
|
||||||
|
return self._name
|
||||||
|
|
||||||
|
@property
|
||||||
|
def applied(self) -> bool:
|
||||||
|
return self._applied
|
||||||
|
|
||||||
|
@applied.setter
|
||||||
|
def applied(self, value: bool) -> None:
|
||||||
|
self._applied = value
|
||||||
|
|
||||||
|
@property
|
||||||
|
def console(self) -> Console | None:
|
||||||
|
return self._console
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def apply(self, base: Dict[str, Any]) -> Any:
|
||||||
|
raise NotImplementedError(
|
||||||
|
"Попытка применения шаблона патча, а не его реализации"
|
||||||
|
)
|
||||||
|
|
||||||
|
def safe_apply(self, base: Dict[str, Any]) -> bool:
|
||||||
|
"""Безопасно применяет патч с обработкой ошибок"""
|
||||||
|
try:
|
||||||
|
self._applied = self.apply(base)
|
||||||
|
return self._applied
|
||||||
|
except Exception as e:
|
||||||
|
if self._console:
|
||||||
|
self._console.print(f"[red]Ошибка в патче {self._name}: {e}")
|
||||||
|
if base.get("verbose"):
|
||||||
|
self._console.print_exception()
|
||||||
|
return False
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
import struct
|
|
||||||
|
|
||||||
|
|
||||||
def float_to_hex(f):
|
|
||||||
b = struct.pack(">f", f)
|
|
||||||
return b.hex()
|
|
||||||
+159
@@ -0,0 +1,159 @@
|
|||||||
|
from typing import Any, get_args, get_origin
|
||||||
|
|
||||||
|
from pydantic import BaseModel
|
||||||
|
from pydantic.fields import FieldInfo
|
||||||
|
from rich.console import Console
|
||||||
|
from rich.table import Table
|
||||||
|
|
||||||
|
|
||||||
|
def format_field_type(annotation: Any) -> str:
|
||||||
|
"""Форматирует тип поля для отображения"""
|
||||||
|
if annotation is None:
|
||||||
|
return "None"
|
||||||
|
|
||||||
|
origin = get_origin(annotation)
|
||||||
|
|
||||||
|
if origin is not None:
|
||||||
|
args = get_args(annotation)
|
||||||
|
origin_name = getattr(origin, "__name__", str(origin))
|
||||||
|
|
||||||
|
if origin_name == "UnionType" or str(origin) == "typing.Union":
|
||||||
|
args_str = " | ".join(format_field_type(a) for a in args)
|
||||||
|
return args_str
|
||||||
|
|
||||||
|
if args:
|
||||||
|
args_str = ", ".join(format_field_type(a) for a in args)
|
||||||
|
return f"{origin_name}[{args_str}]"
|
||||||
|
return origin_name
|
||||||
|
|
||||||
|
if isinstance(annotation, type) and issubclass(annotation, BaseModel):
|
||||||
|
return f"[magenta]{annotation.__name__}[/magenta]"
|
||||||
|
|
||||||
|
return getattr(annotation, "__name__", str(annotation))
|
||||||
|
|
||||||
|
|
||||||
|
def print_model_fields(
|
||||||
|
console: Console,
|
||||||
|
model_class: type[BaseModel],
|
||||||
|
indent: int = 0,
|
||||||
|
visited: set | None = None,
|
||||||
|
) -> None:
|
||||||
|
"""Рекурсивно выводит поля модели с поддержкой вложенных моделей"""
|
||||||
|
if visited is None:
|
||||||
|
visited = set()
|
||||||
|
|
||||||
|
if model_class in visited:
|
||||||
|
console.print(
|
||||||
|
f"{' ' * indent}[dim](циклическая ссылка на {model_class.__name__})[/dim]"
|
||||||
|
)
|
||||||
|
return
|
||||||
|
visited.add(model_class)
|
||||||
|
|
||||||
|
prefix = " " * indent
|
||||||
|
|
||||||
|
for field_name, field_info in model_class.model_fields.items():
|
||||||
|
annotation = field_info.annotation
|
||||||
|
field_type = format_field_type(annotation)
|
||||||
|
default = field_info.default
|
||||||
|
description = field_info.description or ""
|
||||||
|
|
||||||
|
if default is None:
|
||||||
|
default_str = "[dim]None[/dim]"
|
||||||
|
elif default is ...:
|
||||||
|
default_str = "[red]required[/red]"
|
||||||
|
elif isinstance(default, bool):
|
||||||
|
default_str = "[green]true[/green]" if default else "[red]false[/red]"
|
||||||
|
else:
|
||||||
|
default_str = str(default)
|
||||||
|
|
||||||
|
console.print(
|
||||||
|
f"{prefix}[yellow]{field_name}[/yellow]: {field_type} = {default_str}"
|
||||||
|
+ (f" [dim]# {description}[/dim]" if description else "")
|
||||||
|
)
|
||||||
|
|
||||||
|
nested_model = None
|
||||||
|
|
||||||
|
if isinstance(annotation, type) and issubclass(annotation, BaseModel):
|
||||||
|
nested_model = annotation
|
||||||
|
else:
|
||||||
|
origin = get_origin(annotation)
|
||||||
|
if origin is not None:
|
||||||
|
for arg in get_args(annotation):
|
||||||
|
if isinstance(arg, type) and issubclass(arg, BaseModel):
|
||||||
|
nested_model = arg
|
||||||
|
break
|
||||||
|
|
||||||
|
if nested_model is not None:
|
||||||
|
console.print(f"{prefix} [dim]└─ {nested_model.__name__}:[/dim]")
|
||||||
|
print_model_fields(console, nested_model, indent + 2, visited.copy())
|
||||||
|
|
||||||
|
|
||||||
|
def print_model_table(
|
||||||
|
console: Console,
|
||||||
|
model_class: type[BaseModel],
|
||||||
|
prefix: str = "",
|
||||||
|
visited: set | None = None,
|
||||||
|
) -> Table:
|
||||||
|
"""Выводит поля модели в виде таблицы с вложенными моделями"""
|
||||||
|
if visited is None:
|
||||||
|
visited = set()
|
||||||
|
|
||||||
|
table = Table(show_header=True, box=None if prefix else None)
|
||||||
|
table.add_column("Поле", style="yellow")
|
||||||
|
table.add_column("Тип", style="cyan")
|
||||||
|
table.add_column("По умолчанию")
|
||||||
|
table.add_column("Описание", style="dim")
|
||||||
|
|
||||||
|
_add_model_rows(table, model_class, prefix, visited)
|
||||||
|
|
||||||
|
return table
|
||||||
|
|
||||||
|
|
||||||
|
def _add_model_rows(
|
||||||
|
table: Table,
|
||||||
|
model_class: type[BaseModel],
|
||||||
|
prefix: str = "",
|
||||||
|
visited: set | None = None,
|
||||||
|
) -> None:
|
||||||
|
"""Добавляет строки модели в таблицу рекурсивно"""
|
||||||
|
if visited is None:
|
||||||
|
visited = set()
|
||||||
|
|
||||||
|
if model_class in visited:
|
||||||
|
return
|
||||||
|
visited.add(model_class)
|
||||||
|
|
||||||
|
for field_name, field_info in model_class.model_fields.items():
|
||||||
|
annotation = field_info.annotation
|
||||||
|
field_type = format_field_type(annotation)
|
||||||
|
default = field_info.default
|
||||||
|
description = field_info.description or ""
|
||||||
|
|
||||||
|
if default is None:
|
||||||
|
default_str = "-"
|
||||||
|
elif default is ...:
|
||||||
|
default_str = "[red]required[/red]"
|
||||||
|
elif isinstance(default, bool):
|
||||||
|
default_str = "true" if default else "false"
|
||||||
|
elif isinstance(default, BaseModel):
|
||||||
|
default_str = "{...}"
|
||||||
|
else:
|
||||||
|
default_str = str(default)[:20]
|
||||||
|
|
||||||
|
full_name = f"{prefix}{field_name}" if prefix else field_name
|
||||||
|
table.add_row(full_name, field_type, default_str, description[:40])
|
||||||
|
|
||||||
|
nested_model = None
|
||||||
|
|
||||||
|
if isinstance(annotation, type) and issubclass(annotation, BaseModel):
|
||||||
|
nested_model = annotation
|
||||||
|
else:
|
||||||
|
origin = get_origin(annotation)
|
||||||
|
if origin is not None:
|
||||||
|
for arg in get_args(annotation):
|
||||||
|
if isinstance(arg, type) and issubclass(arg, BaseModel):
|
||||||
|
nested_model = arg
|
||||||
|
break
|
||||||
|
|
||||||
|
if nested_model is not None and nested_model not in visited:
|
||||||
|
_add_model_rows(table, nested_model, f" {full_name}.", visited.copy())
|
||||||
@@ -0,0 +1,107 @@
|
|||||||
|
import importlib
|
||||||
|
from contextlib import contextmanager
|
||||||
|
from functools import wraps
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Dict, List, Type
|
||||||
|
|
||||||
|
import typer
|
||||||
|
from rich.console import Console
|
||||||
|
|
||||||
|
from utils.config import PatchTemplate
|
||||||
|
from utils.tools import PATCHES
|
||||||
|
|
||||||
|
|
||||||
|
class PatcherError(Exception):
|
||||||
|
"""Базовое исключение патчера"""
|
||||||
|
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class ConfigError(PatcherError):
|
||||||
|
"""Ошибка конфигурации"""
|
||||||
|
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class BuildError(PatcherError):
|
||||||
|
"""Ошибка сборки"""
|
||||||
|
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def handle_errors(func):
|
||||||
|
"""Декоратор для обработки ошибок CLI"""
|
||||||
|
|
||||||
|
@wraps(func)
|
||||||
|
def wrapper(*args, **kwargs):
|
||||||
|
try:
|
||||||
|
return func(*args, **kwargs)
|
||||||
|
except PatcherError as e:
|
||||||
|
Console().print(f"[red]Ошибка: {e}")
|
||||||
|
raise typer.Exit(1)
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
Console().print("\n[yellow]Прервано пользователем")
|
||||||
|
raise typer.Exit(130)
|
||||||
|
|
||||||
|
return wrapper
|
||||||
|
|
||||||
|
|
||||||
|
class PatchManager:
|
||||||
|
"""Менеджер для работы с патчами"""
|
||||||
|
|
||||||
|
def __init__(self, console: Console, patches_dir: Path = PATCHES):
|
||||||
|
self.console = console
|
||||||
|
self.patches_dir = patches_dir
|
||||||
|
|
||||||
|
def discover_patches(self, include_todo: bool = False) -> List[str]:
|
||||||
|
"""Находит все доступные патчи"""
|
||||||
|
patches = []
|
||||||
|
for f in self.patches_dir.glob("*.py"):
|
||||||
|
if f.name == "__init__.py":
|
||||||
|
continue
|
||||||
|
if f.name.startswith("todo_") and not include_todo:
|
||||||
|
continue
|
||||||
|
patches.append(f.stem)
|
||||||
|
return patches
|
||||||
|
|
||||||
|
def discover_all(self) -> Dict[str, List[str]]:
|
||||||
|
"""Находит все патчи, разделяя на готовые и в разработке"""
|
||||||
|
ready = []
|
||||||
|
todo = []
|
||||||
|
|
||||||
|
for f in self.patches_dir.glob("*.py"):
|
||||||
|
if f.name == "__init__.py":
|
||||||
|
continue
|
||||||
|
if f.name.startswith("todo_"):
|
||||||
|
todo.append(f.stem)
|
||||||
|
else:
|
||||||
|
ready.append(f.stem)
|
||||||
|
|
||||||
|
return {"ready": ready, "todo": todo}
|
||||||
|
|
||||||
|
def load_patch_module(self, name: str) -> type:
|
||||||
|
"""Загружает модуль патча"""
|
||||||
|
module = importlib.import_module(f"patches.{name}")
|
||||||
|
return module
|
||||||
|
|
||||||
|
def load_patch_class(self, name: str) -> type:
|
||||||
|
"""Загружает класс патча"""
|
||||||
|
module = importlib.import_module(f"patches.{name}")
|
||||||
|
return module.Patch
|
||||||
|
|
||||||
|
def load_patch(self, name: str) -> PatchTemplate:
|
||||||
|
"""Загружает экземпляр патча"""
|
||||||
|
module = importlib.import_module(f"patches.{name}")
|
||||||
|
return module.Patch(name=name, console=self.console)
|
||||||
|
|
||||||
|
def load_enabled_patches(self) -> List[PatchTemplate]:
|
||||||
|
"""Загружает все включённые патчи, отсортированные по приоритету"""
|
||||||
|
patches = []
|
||||||
|
for name in self.discover_patches():
|
||||||
|
patch = self.load_patch(name)
|
||||||
|
if patch.enabled:
|
||||||
|
patches.append(patch)
|
||||||
|
else:
|
||||||
|
self.console.print(f"[dim]≫ Пропускаем {name}[/dim]")
|
||||||
|
|
||||||
|
return sorted(patches, key=lambda p: p.priority, reverse=True)
|
||||||
+14
-5
@@ -1,8 +1,10 @@
|
|||||||
from lxml import etree
|
|
||||||
from copy import deepcopy
|
from copy import deepcopy
|
||||||
|
|
||||||
|
from lxml import etree
|
||||||
|
from typing_extensions import Optional
|
||||||
|
|
||||||
def insert_after_public(anchor_name: str, elem_name: str):
|
|
||||||
|
def insert_after_public(anchor_name: str, elem_name: str) -> Optional[int]:
|
||||||
file_path = "./decompiled/res/values/public.xml"
|
file_path = "./decompiled/res/values/public.xml"
|
||||||
parser = etree.XMLParser(remove_blank_text=True)
|
parser = etree.XMLParser(remove_blank_text=True)
|
||||||
tree = etree.parse(file_path, parser)
|
tree = etree.parse(file_path, parser)
|
||||||
@@ -19,6 +21,8 @@ def insert_after_public(anchor_name: str, elem_name: str):
|
|||||||
anchor = (elem, attrs)
|
anchor = (elem, attrs)
|
||||||
types[attrs["type"]] = types.get(attrs["type"], []) + [int(attrs["id"], 16)]
|
types[attrs["type"]] = types.get(attrs["type"], []) + [int(attrs["id"], 16)]
|
||||||
|
|
||||||
|
assert anchor != None
|
||||||
|
|
||||||
free_ids = set()
|
free_ids = set()
|
||||||
group = types[anchor[1]["type"]]
|
group = types[anchor[1]["type"]]
|
||||||
for i in range(min(group), max(group) + 1):
|
for i in range(min(group), max(group) + 1):
|
||||||
@@ -47,7 +51,7 @@ def insert_after_public(anchor_name: str, elem_name: str):
|
|||||||
return new_id
|
return new_id
|
||||||
|
|
||||||
|
|
||||||
def insert_after_id(anchor_name: str, elem_name: str):
|
def insert_after_id(anchor_name: str, elem_name: str) -> None:
|
||||||
file_path = "./decompiled/res/values/ids.xml"
|
file_path = "./decompiled/res/values/ids.xml"
|
||||||
parser = etree.XMLParser(remove_blank_text=True)
|
parser = etree.XMLParser(remove_blank_text=True)
|
||||||
tree = etree.parse(file_path, parser)
|
tree = etree.parse(file_path, parser)
|
||||||
@@ -62,13 +66,15 @@ def insert_after_id(anchor_name: str, elem_name: str):
|
|||||||
assert anchor == None
|
assert anchor == None
|
||||||
anchor = (elem, attrs)
|
anchor = (elem, attrs)
|
||||||
|
|
||||||
|
assert anchor != None
|
||||||
|
|
||||||
new_elem = deepcopy(anchor[0])
|
new_elem = deepcopy(anchor[0])
|
||||||
new_elem.set("name", elem_name)
|
new_elem.set("name", elem_name)
|
||||||
anchor[0].addnext(new_elem)
|
anchor[0].addnext(new_elem)
|
||||||
tree.write(file_path, pretty_print=True, xml_declaration=True, encoding="utf-8")
|
tree.write(file_path, pretty_print=True, xml_declaration=True, encoding="utf-8")
|
||||||
|
|
||||||
|
|
||||||
def change_color(name: str, value: str):
|
def change_color(name: str, value: str) -> None:
|
||||||
file_path = "./decompiled/res/values/colors.xml"
|
file_path = "./decompiled/res/values/colors.xml"
|
||||||
parser = etree.XMLParser(remove_blank_text=True)
|
parser = etree.XMLParser(remove_blank_text=True)
|
||||||
tree = etree.parse(file_path, parser)
|
tree = etree.parse(file_path, parser)
|
||||||
@@ -86,7 +92,8 @@ def change_color(name: str, value: str):
|
|||||||
assert replacements >= 1
|
assert replacements >= 1
|
||||||
tree.write(file_path, pretty_print=True, xml_declaration=True, encoding="utf-8")
|
tree.write(file_path, pretty_print=True, xml_declaration=True, encoding="utf-8")
|
||||||
|
|
||||||
def insert_after_color(anchor_name: str, elem_name: str, elem_value: str):
|
|
||||||
|
def insert_after_color(anchor_name: str, elem_name: str, elem_value: str) -> None:
|
||||||
file_path = "./decompiled/res/values/colors.xml"
|
file_path = "./decompiled/res/values/colors.xml"
|
||||||
parser = etree.XMLParser(remove_blank_text=True)
|
parser = etree.XMLParser(remove_blank_text=True)
|
||||||
tree = etree.parse(file_path, parser)
|
tree = etree.parse(file_path, parser)
|
||||||
@@ -101,6 +108,8 @@ def insert_after_color(anchor_name: str, elem_name: str, elem_value: str):
|
|||||||
assert anchor == None
|
assert anchor == None
|
||||||
anchor = (elem, attrs)
|
anchor = (elem, attrs)
|
||||||
|
|
||||||
|
assert anchor != None
|
||||||
|
|
||||||
new_elem = deepcopy(anchor[0])
|
new_elem = deepcopy(anchor[0])
|
||||||
new_elem.set("name", elem_name)
|
new_elem.set("name", elem_name)
|
||||||
anchor[0].addnext(new_elem)
|
anchor[0].addnext(new_elem)
|
||||||
|
|||||||
+43
-14
@@ -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,36 @@ 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 find_smali_line(
|
||||||
|
lines: list[str], search: str
|
||||||
|
) -> list[int]:
|
||||||
|
result = []
|
||||||
|
for index, line in enumerate(lines):
|
||||||
|
if line.find(search) >= 0:
|
||||||
|
result.append(index)
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def float_to_hex(f):
|
||||||
|
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)
|
||||||
|
|||||||
@@ -0,0 +1,68 @@
|
|||||||
|
from pathlib import Path
|
||||||
|
from typing import List
|
||||||
|
|
||||||
|
import httpx
|
||||||
|
import typer
|
||||||
|
from plumbum import FG, ProcessExecutionError, local
|
||||||
|
from rich.console import Console
|
||||||
|
from rich.progress import Progress
|
||||||
|
|
||||||
|
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
|
||||||
|
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):
|
||||||
|
"""Скачивание файла по URL"""
|
||||||
|
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))
|
||||||
|
|
||||||
|
|
||||||
|
def select_apk(console) -> Path:
|
||||||
|
"""Выбор APK файла из папки original"""
|
||||||
|
apks = list(ORIGINAL.glob("*.apk"))
|
||||||
|
|
||||||
|
if not apks:
|
||||||
|
raise BuildError("Нет apk-файлов в папке original")
|
||||||
|
|
||||||
|
if len(apks) == 1:
|
||||||
|
console.print(f"[green]Выбран {apks[0].name}")
|
||||||
|
return apks[0]
|
||||||
|
|
||||||
|
console.print("[cyan]Доступные APK файлы:")
|
||||||
|
options = {str(i): apk for i, apk in enumerate(apks, 1)}
|
||||||
|
for k, v in options.items():
|
||||||
|
console.print(f" {k}. {v.name}")
|
||||||
|
|
||||||
|
choice = Prompt.ask("Выберите номер", choices=list(options.keys()))
|
||||||
|
return options[choice]
|
||||||
Reference in New Issue
Block a user