Compare commits
36 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 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,7 +1,10 @@
|
|||||||
name: Build mod
|
name: Сборка мода
|
||||||
|
|
||||||
on:
|
on:
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
push:
|
||||||
|
tags:
|
||||||
|
- 'v*'
|
||||||
#schedule: # раз в 36 часов
|
#schedule: # раз в 36 часов
|
||||||
# - cron: "0 0 */3 * *" # каждые 3 дня в 00:00
|
# - cron: "0 0 */3 * *" # каждые 3 дня в 00:00
|
||||||
# - cron: "0 12 */3 * *" # каждые 3 дня в 12:00
|
# - cron: "0 12 */3 * *" # каждые 3 дня в 12:00
|
||||||
@@ -13,18 +16,25 @@ jobs:
|
|||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v3
|
||||||
|
|
||||||
- name: Download APK
|
- name: Скачивание APK
|
||||||
run: |
|
run: |
|
||||||
curl -L -o app.apk "https://mirror-dl.anixart-app.com/anixart-beta.apk"
|
curl -L -o app.apk "https://mirror-dl.anixart-app.com/anixart-beta.apk"
|
||||||
|
|
||||||
- name: Ensure aapt is installed
|
- name: Проверка наличия aapt
|
||||||
run: |
|
run: |
|
||||||
if ! command -v aapt &> /dev/null; then
|
if ! command -v aapt &> /dev/null; then
|
||||||
echo "aapt не найден, устанавливаем..."
|
echo "aapt не найден, устанавливаем..."
|
||||||
sudo apt-get update && sudo apt-get install -y --no-install-recommends android-sdk-build-tools
|
sudo apt-get update && sudo apt-get install -y --no-install-recommends android-sdk-build-tools
|
||||||
fi
|
fi
|
||||||
|
|
||||||
- name: Export secrets
|
- name: Проверка наличия pngquant
|
||||||
|
run: |
|
||||||
|
if ! command -v pngquant &> /dev/null; then
|
||||||
|
echo "pngquant не найден, устанавливаем..."
|
||||||
|
sudo apt-get update && sudo apt-get install -y --no-install-recommends pngquant
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Извлечение хранилища ключей
|
||||||
env:
|
env:
|
||||||
KEYSTORE: ${{ secrets.KEYSTORE }}
|
KEYSTORE: ${{ secrets.KEYSTORE }}
|
||||||
KEYSTORE_PASS: ${{ secrets.KEYSTORE_PASS }}
|
KEYSTORE_PASS: ${{ secrets.KEYSTORE_PASS }}
|
||||||
@@ -33,33 +43,40 @@ 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: Чтение report.log
|
||||||
id: get_title
|
id: get_title
|
||||||
run: |
|
run: |
|
||||||
TITLE=$(head -n 1 modified/report.log)
|
TITLE=$(head -n 1 modified/report.log)
|
||||||
|
tail -n +2 modified/report.log > modified/report.log.tmp
|
||||||
echo "title=${TITLE}" >> $GITHUB_OUTPUT
|
echo "title=${TITLE}" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
- name: Setup go
|
- name: Установка go
|
||||||
if: steps.build.outputs.BUILD_EXIT == '0'
|
if: steps.build.outputs.BUILD_EXIT == '0'
|
||||||
uses: actions/setup-go@v4
|
uses: actions/setup-go@v4
|
||||||
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_path: modified/report.log.tmp
|
||||||
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 @@
|
|||||||
|
{"enabled":true,"server":"https://anixarty.0x174.su/patch"}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{"enabled":true,"logo":{"gradient":{"angle":0.0,"start_color":"#ffccff00","end_color":"#ffcccc00"},"ears_color":"#ffd0d0d0"},"colors":{"primary":"#ccff00","secondary":"#ffcccc00","background":"#ffffff","text":"#000000"}}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{"enabled":true,"replace":true,"custom_icons":true,"icon_size":"18.0dip"}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{"enabled":true,"remove_language_files":true,"remove_AI_voiceover":true,"remove_debug_lines":false,"remove_drawable_files":false,"remove_unknown_files":true,"remove_unknown_files_keep_dirs":["META-INF","kotlin"],"compress_png_files":true}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{"enabled":true}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{"enabled":true}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{"enabled":true,"package_name":"com.wowlikon.anixart"}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{"enabled":true,"items":["home","discover","feed","bookmarks","profile"]}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{"enabled":true}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{"enabled":true,"version":" by wowlikon","menu":{"Мы в социальных сетях":[{"title":"wowlikon","description":"Разработчик","url":"https://t.me/wowlikon","icon":"@drawable/ic_custom_telegram","icon_space_reserved":"false"},{"title":"Kentai Radiquum","description":"Разработчик","url":"https://t.me/radiquum","icon":"@drawable/ic_custom_telegram","icon_space_reserved":"false"},{"title":"Мы в Telegram","description":"Подпишитесь на канал, чтобы быть в курсе последних новостей.","url":"https://t.me/http_teapod","icon":"@drawable/ic_custom_telegram","icon_space_reserved":"false"}],"Прочее":[{"title":"Помочь проекту","description":"Вы можете помочь нам в разработке мода, написании кода или тестировании.","url":"https://git.wowlikon.tech/anixart-mod","icon":"@drawable/ic_custom_crown","icon_space_reserved":"false"}]}}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{"enabled":true,"format":{"share_channel_text":"Канал: «%1$s»\n%2$schannel/%3$d","share_collection_text":"Коллекция: «%1$s»\n%2$scollection/%3$d","share_profile_text":"Профиль пользователя «%1$s»\n%2$sprofile/%3$d","share_release_text":"Релиз: «%1$s»\n%2$srelease/%3$d"}}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{"enabled":true,"title":"Anixarty","description":"Описание","link_text":"МЫ В TELEGRAM","link_url":"https://t.me/http_teapod","skip_text":"Пропустить","title_bg_color":"#FFFFFF"}
|
||||||
@@ -1,230 +1,241 @@
|
|||||||
import os
|
from typing import List, Dict, Any
|
||||||
import sys
|
|
||||||
import json
|
import typer
|
||||||
import yaml
|
|
||||||
import requests
|
|
||||||
import argparse
|
|
||||||
import colorama
|
|
||||||
import importlib
|
import importlib
|
||||||
import traceback
|
import traceback
|
||||||
import subprocess
|
import yaml
|
||||||
from tqdm import tqdm
|
|
||||||
|
|
||||||
def init() -> dict:
|
from plumbum import local, ProcessExecutionError
|
||||||
for directory in ["original", "modified", "patches", "tools", "decompiled"]:
|
from rich.console import Console
|
||||||
if not os.path.exists(directory):
|
from rich.progress import Progress
|
||||||
os.makedirs(directory)
|
from rich.prompt import Prompt
|
||||||
|
|
||||||
with open("./config.json", "r") as config_file:
|
from utils.config import *
|
||||||
conf = json.load(config_file)
|
from utils.tools import *
|
||||||
|
|
||||||
if not os.path.exists("./tools/apktool.jar"):
|
|
||||||
try:
|
|
||||||
print("Скачивание Apktool...")
|
|
||||||
jar_response = requests.get(conf["tools"]["apktool_jar_url"], stream=True)
|
|
||||||
jar_path = "tools/apktool.jar"
|
|
||||||
with open(jar_path, "wb") as f:
|
|
||||||
for chunk in jar_response.iter_content(chunk_size=8192):
|
|
||||||
f.write(chunk)
|
|
||||||
|
|
||||||
wrapper_response = requests.get(conf["tools"]["apktool_wrapper_url"])
|
|
||||||
wrapper_path = "tools/apktool"
|
|
||||||
with open(wrapper_path, "w") as f:
|
|
||||||
f.write(wrapper_response.text)
|
|
||||||
os.chmod(wrapper_path, 0o755)
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
print(f"Ошибка при скачивании Apktool: {e}")
|
|
||||||
exit(1)
|
|
||||||
|
|
||||||
try:
|
|
||||||
result = subprocess.run(
|
|
||||||
["java", "-version"], capture_output=True, text=True, check=True
|
|
||||||
)
|
|
||||||
|
|
||||||
version_line = result.stderr.splitlines()[0]
|
|
||||||
if "1.8" in version_line or any(f"{i}." in version_line for i in range(9, 100)):
|
|
||||||
print("Java 8 или более поздняя версия установлена.")
|
|
||||||
else:
|
|
||||||
print("Java 8 или более поздняя версия не установлена.")
|
|
||||||
sys.exit(1)
|
|
||||||
except subprocess.CalledProcessError:
|
|
||||||
print("Java не установлена. Установите Java 8 или более позднюю версию.")
|
|
||||||
exit(1)
|
|
||||||
|
|
||||||
return conf
|
|
||||||
|
|
||||||
def select_apk() -> str:
|
|
||||||
apks = []
|
|
||||||
for file in os.listdir("original"):
|
|
||||||
if file.endswith(".apk") and os.path.isfile(os.path.join("original", file)):
|
|
||||||
apks.append(file)
|
|
||||||
|
|
||||||
if not apks:
|
|
||||||
print("Нет файлов .apk в текущей директории")
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
if len(apks) == 1:
|
|
||||||
apk = apks[0]
|
|
||||||
print(f"Выбран файл {apk}")
|
|
||||||
return apk
|
|
||||||
|
|
||||||
while True:
|
|
||||||
print("Выберете файл для модификации")
|
|
||||||
for index, apk in enumerate(apks):
|
|
||||||
print(f"{index + 1}. {apk}")
|
|
||||||
print("0. Exit")
|
|
||||||
|
|
||||||
try:
|
|
||||||
selected_index = int(input("\nВведите номер файла: "))
|
|
||||||
if selected_index == 0:
|
|
||||||
sys.exit(0)
|
|
||||||
elif selected_index > len(apks):
|
|
||||||
print("Неверный номер файла")
|
|
||||||
else:
|
|
||||||
apk = apks[selected_index - 1]
|
|
||||||
print(f"Выбран файл {apk}")
|
|
||||||
return apk
|
|
||||||
except ValueError:
|
|
||||||
print("Неверный формат ввода")
|
|
||||||
except KeyboardInterrupt:
|
|
||||||
print("Прервано пользователем")
|
|
||||||
sys.exit(0)
|
|
||||||
|
|
||||||
|
|
||||||
def decompile_apk(apk: str):
|
console = Console()
|
||||||
print("Декомпилируем apk...")
|
app = typer.Typer()
|
||||||
try:
|
|
||||||
result = subprocess.run(
|
|
||||||
"tools/apktool d -f -o decompiled " + os.path.join("original", apk),
|
|
||||||
shell=True,
|
|
||||||
check=True,
|
|
||||||
text=True,
|
|
||||||
stdout=subprocess.DEVNULL,
|
|
||||||
stderr=subprocess.PIPE,
|
|
||||||
)
|
|
||||||
except subprocess.CalledProcessError as e:
|
|
||||||
print("Ошибка при выполнении команды:")
|
|
||||||
print(e.stderr)
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
|
|
||||||
def compile_apk(apk: str):
|
|
||||||
print("Компилируем apk...")
|
|
||||||
try:
|
|
||||||
subprocess.run(
|
|
||||||
"tools/apktool b decompiled -o " + os.path.join("modified", apk),
|
|
||||||
shell=True,
|
|
||||||
check=True,
|
|
||||||
text=True,
|
|
||||||
stdout=subprocess.DEVNULL,
|
|
||||||
stderr=subprocess.PIPE,
|
|
||||||
)
|
|
||||||
subprocess.run(
|
|
||||||
"zipalign -v 4 " + os.path.join("modified", apk) + " " + os.path.join("modified", apk.replace(".apk", "-aligned.apk")),
|
|
||||||
shell=True,
|
|
||||||
check=True,
|
|
||||||
text=True,
|
|
||||||
stdout=subprocess.DEVNULL,
|
|
||||||
stderr=subprocess.PIPE,
|
|
||||||
)
|
|
||||||
subprocess.run(
|
|
||||||
"apksigner sign " +
|
|
||||||
"--v1-signing-enabled false " +
|
|
||||||
"--v2-signing-enabled true " +
|
|
||||||
"--v3-signing-enabled true " +
|
|
||||||
"--ks keystore.jks " +
|
|
||||||
"--ks-pass file:keystore.pass " +
|
|
||||||
"--out " + os.path.join("modified", apk.replace(".apk", "-mod.apk")) +
|
|
||||||
" " + os.path.join("modified", apk.replace(".apk", "-aligned.apk")),
|
|
||||||
shell=True,
|
|
||||||
check=True,
|
|
||||||
text=True,
|
|
||||||
stdout=subprocess.DEVNULL,
|
|
||||||
stderr=subprocess.PIPE,
|
|
||||||
)
|
|
||||||
title = "anixart mod "
|
|
||||||
with open('./decompiled/apktool.yml') as f:
|
|
||||||
package = yaml.safe_load(f)
|
|
||||||
title += ' '.join([f'{k}: {v}' for k, v in package['versionInfo'].items()])
|
|
||||||
with open("./modified/report.log", "w") as log_file:
|
|
||||||
log_file.write(title+'\n')
|
|
||||||
log_file.write("\n".join([f"{patch.name}: {'applied' if patch.applied else 'failed'}" for patch in patches]))
|
|
||||||
except subprocess.CalledProcessError as e:
|
|
||||||
print("Ошибка при выполнении команды:")
|
|
||||||
print(e.stderr)
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
|
|
||||||
|
# ======================= PATCHING =========================
|
||||||
class Patch:
|
class Patch:
|
||||||
def __init__(self, name, pkg):
|
def __init__(self, name: str, module):
|
||||||
self.name = name
|
self.name = name
|
||||||
self.package = pkg
|
self.module = module
|
||||||
self.applied = False
|
self.applied = False
|
||||||
|
self.priority = getattr(module, "priority", 0)
|
||||||
try:
|
try:
|
||||||
self.priority = pkg.priority
|
self.config = module.Config.model_validate_json((CONFIGS / f"{name}.json").read_text())
|
||||||
except AttributeError:
|
|
||||||
self.priority = 0
|
|
||||||
|
|
||||||
def apply(self, conf: dict) -> bool:
|
|
||||||
try:
|
|
||||||
self.applied = self.package.apply(conf)
|
|
||||||
return True
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Ошибка при применении патча {self.name}: {e}")
|
console.print(f"[red]Ошибка при загрузке конфигурации патча {name}: {e}")
|
||||||
print(type(e), e.args)
|
console.print(f"[yellow]Используются значения по умолчанию")
|
||||||
|
self.config = module.Config()
|
||||||
|
|
||||||
|
def apply(self, conf: Dict[str, Any]) -> bool:
|
||||||
|
try:
|
||||||
|
self.applied = bool(self.module.apply(self.config, conf))
|
||||||
|
return self.applied
|
||||||
|
except Exception as e:
|
||||||
|
console.print(f"[red]Ошибка в патче {self.name}: {e}")
|
||||||
traceback.print_exc()
|
traceback.print_exc()
|
||||||
return False
|
return False
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
parser = argparse.ArgumentParser(
|
# ========================= INIT =========================
|
||||||
description="Автоматический патчер anixart"
|
@app.command()
|
||||||
|
def init():
|
||||||
|
"""Создание директорий и скачивание инструментов"""
|
||||||
|
ensure_dirs()
|
||||||
|
conf = load_config(console)
|
||||||
|
|
||||||
|
for f in PATCHES.glob("*.py"):
|
||||||
|
if f.name.startswith("todo_") or f.name == "__init__.py":
|
||||||
|
continue
|
||||||
|
patch = Patch(f.stem, __import__(f"patches.{f.stem}", fromlist=[""]))
|
||||||
|
json_string = patch.config.model_dump_json()
|
||||||
|
(CONFIGS / f"{patch.name}.json").write_text(json_string)
|
||||||
|
|
||||||
|
if not (TOOLS / "apktool.jar").exists():
|
||||||
|
download(console, conf.tools.apktool_jar_url, TOOLS / "apktool.jar")
|
||||||
|
|
||||||
|
if not (TOOLS / "apktool").exists():
|
||||||
|
download(console, conf.tools.apktool_wrapper_url, TOOLS / "apktool")
|
||||||
|
(TOOLS / "apktool").chmod(0o755)
|
||||||
|
|
||||||
|
try:
|
||||||
|
local["java"]["-version"]()
|
||||||
|
console.print("[green]Java найдена")
|
||||||
|
except ProcessExecutionError:
|
||||||
|
console.print("[red]Java не установлена")
|
||||||
|
raise typer.Exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
# ========================= INFO =========================
|
||||||
|
@app.command()
|
||||||
|
def info(patch_name: str = ""):
|
||||||
|
"""Вывод информации о патче"""
|
||||||
|
conf = load_config(console).model_dump()
|
||||||
|
if patch_name:
|
||||||
|
patch = Patch(patch_name, __import__(f"patches.{patch_name}", fromlist=[""]))
|
||||||
|
console.print(f"[green]Информация о патче {patch.name}:")
|
||||||
|
console.print(f" [yellow]Приоритет: {patch.priority}")
|
||||||
|
console.print(f" [yellow]Описание: {patch.module.__doc__}")
|
||||||
|
|
||||||
|
console.print(f"[blue]Поля конфигурации")
|
||||||
|
for field_name, field_info in type(patch.config).model_fields.items():
|
||||||
|
field_data = {
|
||||||
|
'type': field_info.annotation.__name__,
|
||||||
|
'description': field_info.description,
|
||||||
|
'default': field_info.default,
|
||||||
|
'json_schema_extra': field_info.json_schema_extra,
|
||||||
|
}
|
||||||
|
console.print(f'{field_name} {field_data}')
|
||||||
|
console.print("\n[blue]" + "="*50 + "\n")
|
||||||
|
|
||||||
|
else:
|
||||||
|
conf = load_config(console)
|
||||||
|
console.print("[cyan]Список патчей:")
|
||||||
|
patch_list = []
|
||||||
|
for f in PATCHES.glob("*.py"):
|
||||||
|
if f.name == "__init__.py": continue
|
||||||
|
if f.name.startswith("todo_"):
|
||||||
|
try: priority = __import__(f"patches.{f.stem}.priority", fromlist=[""])
|
||||||
|
except: priority = None
|
||||||
|
patch_list.append((priority, f" [{priority}] [yellow]{f.stem}: [yellow]⚠ в разработке"))
|
||||||
|
continue
|
||||||
|
patch = Patch(f.stem, __import__(f"patches.{f.stem}", fromlist=[""]))
|
||||||
|
if patch.config.enabled: patch_list.append((patch.priority, f" [{patch.priority}] [yellow]{f.stem}: [green]✔ включен"))
|
||||||
|
else: patch_list.append((patch.priority, f" [{patch.priority}] [yellow]{f.stem}: [red]✘ выключен"))
|
||||||
|
for _, patch in sorted(patch_list, key=lambda x: (x[0] is None, x[0]), reverse=True): console.print(patch)
|
||||||
|
|
||||||
|
|
||||||
|
# ========================= UTIL =========================
|
||||||
|
def select_apk() -> Path:
|
||||||
|
apks = [f for f in ORIGINAL.glob("*.apk")]
|
||||||
|
if not apks:
|
||||||
|
console.print("[red]Нет apk-файлов в папке original")
|
||||||
|
raise typer.Exit(1)
|
||||||
|
|
||||||
|
if len(apks) == 1:
|
||||||
|
console.print(f"[green]Выбран {apks[0].name}")
|
||||||
|
return apks[0]
|
||||||
|
|
||||||
|
options = {str(i): apk for i, apk in enumerate(apks, 1)}
|
||||||
|
for k, v in options.items():
|
||||||
|
console.print(f"{k}. {v.name}")
|
||||||
|
|
||||||
|
choice = Prompt.ask("Выберите номер", choices=list(options.keys()))
|
||||||
|
return options[choice]
|
||||||
|
|
||||||
|
|
||||||
|
def decompile(apk: Path):
|
||||||
|
console.print("[yellow]Декомпиляция apk...")
|
||||||
|
run(
|
||||||
|
console,
|
||||||
|
[
|
||||||
|
"java",
|
||||||
|
"-jar", str(TOOLS / "apktool.jar"),
|
||||||
|
"d", "-f",
|
||||||
|
"-o", str(DECOMPILED),
|
||||||
|
str(apk),
|
||||||
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
parser.add_argument("-v", "--verbose",
|
|
||||||
action="store_true",
|
|
||||||
help="Выводить подробные сообщения")
|
|
||||||
|
|
||||||
parser.add_argument("-f", "--force",
|
def compile(apk: Path, patches: List[Patch]):
|
||||||
action="store_true",
|
console.print("[yellow]Сборка apk...")
|
||||||
help="Принудительно собрать APK")
|
|
||||||
|
|
||||||
args = parser.parse_args()
|
with open(DECOMPILED / "apktool.yml", encoding="utf-8") as f:
|
||||||
|
meta = yaml.safe_load(f)
|
||||||
|
version_info = meta.get("versionInfo", {})
|
||||||
|
version_code = version_info.get("versionCode", 0)
|
||||||
|
version_name = version_info.get("versionName", "unknown")
|
||||||
|
|
||||||
conf = init()
|
filename_version = version_name.lower().replace(" ", "-").replace(".", "-")
|
||||||
|
out_apk = MODIFIED / f"Anixarty-mod-v{filename_version}.apk"
|
||||||
|
aligned = out_apk.with_stem(out_apk.stem + "-aligned")
|
||||||
|
signed = out_apk.with_stem(out_apk.stem + "-mod")
|
||||||
|
|
||||||
|
run(
|
||||||
|
console,
|
||||||
|
[
|
||||||
|
"java",
|
||||||
|
"-jar", str(TOOLS / "apktool.jar"),
|
||||||
|
"b", str(DECOMPILED),
|
||||||
|
"-o", str(out_apk),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
run(
|
||||||
|
console,
|
||||||
|
["zipalign", "-v", "4", str(out_apk), str(aligned)]
|
||||||
|
)
|
||||||
|
run(
|
||||||
|
console,
|
||||||
|
[
|
||||||
|
"apksigner", "sign",
|
||||||
|
"--v1-signing-enabled", "false",
|
||||||
|
"--v2-signing-enabled", "true",
|
||||||
|
"--v3-signing-enabled", "true",
|
||||||
|
"--ks", "keystore.jks",
|
||||||
|
"--ks-pass", "file:keystore.pass",
|
||||||
|
"--out", str(signed),
|
||||||
|
str(aligned),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
console.print("[green]✔ APK успешно собран и подписан")
|
||||||
|
|
||||||
|
with open(MODIFIED / "report.log", "w", encoding="utf-8") as f:
|
||||||
|
f.write(f"Anixarty mod v {version_name} ({version_code})\n")
|
||||||
|
for p in patches:
|
||||||
|
f.write(f"{'✔' if p.applied else '✘'} {p.name}\n")
|
||||||
|
|
||||||
|
|
||||||
|
# ========================= BUILD =========================
|
||||||
|
@app.command()
|
||||||
|
def build(
|
||||||
|
force: bool = typer.Option(False, "--force", "-f", help="Принудительная сборка"),
|
||||||
|
verbose: bool = typer.Option(False, "--verbose", "-v", help="Подробный вывод"),
|
||||||
|
):
|
||||||
|
"""Декомпиляция, патчи и сборка apk"""
|
||||||
|
conf = load_config(console)
|
||||||
apk = select_apk()
|
apk = select_apk()
|
||||||
patch = decompile_apk(apk)
|
decompile(apk)
|
||||||
|
|
||||||
if args.verbose: conf["verbose"] = True
|
patch_objs: List[Patch] = []
|
||||||
|
conf.base |= {"verbose": verbose}
|
||||||
|
|
||||||
patches = []
|
for f in PATCHES.glob("*.py"):
|
||||||
for filename in os.listdir("patches/"):
|
if f.name.startswith("todo_") or f.name == "__init__.py":
|
||||||
if filename.endswith(".py") and filename != "__init__.py" and not filename.startswith("todo_"):
|
continue
|
||||||
module_name = filename[:-3]
|
name = f.stem
|
||||||
module = importlib.import_module(f"patches.{module_name}")
|
module = importlib.import_module(f"patches.{name}")
|
||||||
patches.append(Patch(module_name, module))
|
if not module.Config.model_validate_json((CONFIGS / f"{name}.json").read_text()):
|
||||||
|
console.print(f"[yellow]≫ Пропускаем {name}")
|
||||||
|
continue
|
||||||
|
patch_objs.append(Patch(name, module))
|
||||||
|
|
||||||
patches.sort(key=lambda x: x.package.priority, reverse=True)
|
patch_objs.sort(key=lambda p: p.priority, reverse=True)
|
||||||
|
|
||||||
for patch in tqdm(patches, colour="green", desc="Применение патчей"):
|
console.print("[cyan]Применение патчей")
|
||||||
tqdm.write(f"Применение патча: {patch.name}")
|
with Progress() as progress:
|
||||||
patch.apply(conf)
|
task = progress.add_task("Патчи", total=len(patch_objs))
|
||||||
|
for p in patch_objs:
|
||||||
|
ok = p.apply(conf.base)
|
||||||
|
progress.console.print(f"{'✔' if ok else '✘'} {p.name}")
|
||||||
|
progress.advance(task)
|
||||||
|
|
||||||
statuses = {}
|
successes = sum(p.applied for p in patch_objs)
|
||||||
for patch in patches:
|
if successes == len(patch_objs):
|
||||||
statuses[patch.name] = patch.applied
|
compile(apk, patch_objs)
|
||||||
marker = colorama.Fore.GREEN + "✔" if patch.applied else colorama.Fore.RED + "✘"
|
elif successes > 0 and (
|
||||||
print(f"{marker}{colorama.Style.RESET_ALL} {patch.name}")
|
force or Prompt.ask("Продолжить сборку?", choices=["y", "n"]) == "y"
|
||||||
|
):
|
||||||
if all(statuses.values()):
|
compile(apk, patch_objs)
|
||||||
print(f"{colorama.Fore.GREEN}Все патчи успешно применены{colorama.Style.RESET_ALL}")
|
|
||||||
compile_apk(apk)
|
|
||||||
elif any(statuses.values()):
|
|
||||||
print(f"{colorama.Fore.YELLOW}⚠{colorama.Style.RESET_ALL} Некоторые патчи не были успешно применены")
|
|
||||||
if args.force or input("Продолжить? (y/n): ").lower() == "y":
|
|
||||||
compile_apk(apk)
|
|
||||||
else:
|
|
||||||
print(colorama.Fore.RED + "Операция отменена" + colorama.Style.RESET_ALL)
|
|
||||||
else:
|
else:
|
||||||
print(f"{colorama.Fore.RED}Ни один патч не был успешно применен{colorama.Style.RESET_ALL}")
|
console.print("[red]Сборка отменена")
|
||||||
sys.exit(1)
|
raise typer.Exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__": app()
|
||||||
|
|||||||
@@ -1,39 +1,62 @@
|
|||||||
"""Change api server"""
|
"""Заменяет сервер api
|
||||||
priority = 0
|
|
||||||
from tqdm import tqdm
|
|
||||||
|
|
||||||
|
"change_server": {
|
||||||
|
"enabled": true,
|
||||||
|
"server": "https://anixarty.0x174.su/patch"
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
|
||||||
|
priority = 0
|
||||||
|
|
||||||
|
# imports
|
||||||
import json
|
import json
|
||||||
import requests
|
import requests
|
||||||
|
from tqdm import tqdm
|
||||||
|
from typing import Dict, Any
|
||||||
|
from pydantic import Field
|
||||||
|
|
||||||
|
from utils.config import PatchConfig
|
||||||
|
|
||||||
def apply(config: dict) -> bool:
|
#Config
|
||||||
response = requests.get(config['server'])
|
class Config(PatchConfig):
|
||||||
|
server: str = Field("https://anixarty.0x174.su/patch", description="URL сервера")
|
||||||
|
|
||||||
|
# Patch
|
||||||
|
def apply(config: Config, base: Dict[str, Any]) -> bool:
|
||||||
|
response = requests.get(config.server) # Получаем данные для патча
|
||||||
assert response.status_code == 200, f"Failed to fetch data {response.status_code} {response.text}"
|
assert response.status_code == 200, f"Failed to fetch data {response.status_code} {response.text}"
|
||||||
|
|
||||||
new_api = json.loads(response.text)
|
new_api = json.loads(response.text)
|
||||||
for item in new_api['modifications']:
|
for item in new_api['modifications']: # Применяем замены API
|
||||||
tqdm.write(f"Изменение {item['file']}")
|
tqdm.write(f"Изменение {item['file']}")
|
||||||
filepath = './decompiled/smali_classes2/com/swiftsoft/anixartd/network/api/'+item['file']
|
filepath = './decompiled/smali_classes2/com/swiftsoft/anixartd/network/api/'+item['file']
|
||||||
|
|
||||||
with open(filepath, 'r') as f:
|
with open(filepath, 'r') as f:
|
||||||
content = f.read()
|
content = f.read()
|
||||||
|
|
||||||
with open(filepath, 'w') as f:
|
with open(filepath, 'w') as f:
|
||||||
if content.count(item['src']) == 0:
|
if content.count(item['src']) == 0:
|
||||||
tqdm.write(f"⚠ Не найдено {item['src']}")
|
tqdm.write(f"⚠ Не найдено {item['src']}")
|
||||||
f.write(content.replace(item['src'], item['dst']))
|
f.write(content.replace(item['src'], item['dst']))
|
||||||
|
|
||||||
tqdm.write(f"Изменение Github ссылки")
|
tqdm.write(f"Изменение Github ссылки") # Обновление ссылки на поиск серверов в Github
|
||||||
filepath = './decompiled/smali_classes2/com/swiftsoft/anixartd/utils/anixnet/GithubPagesNetFetcher.smali'
|
filepath = './decompiled/smali_classes2/com/swiftsoft/anixartd/utils/anixnet/GithubPagesNetFetcher.smali'
|
||||||
|
|
||||||
with open(filepath, 'r') as f:
|
with open(filepath, 'r') as f:
|
||||||
content = f.read()
|
content = f.read()
|
||||||
|
|
||||||
with open(filepath, 'w') as f:
|
with open(filepath, 'w') as f:
|
||||||
f.write(content.replace('const-string v1, "https://anixhelper.github.io/pages/urls.json"', f'const-string v1, "{new_api["gh"]}"'))
|
f.write(content.replace('const-string v1, "https://anixhelper.github.io/pages/urls.json"', f'const-string v1, "{new_api["gh"]}"'))
|
||||||
|
|
||||||
content = ""
|
tqdm.write("Удаление динамического выбора сервера") # Отключение автовыбора сервера
|
||||||
tqdm.write("Удаление динамического выбора сервера")
|
|
||||||
filepath = './decompiled/smali_classes2/com/swiftsoft/anixartd/DaggerApp_HiltComponents_SingletonC$SingletonCImpl$SwitchingProvider.smali'
|
filepath = './decompiled/smali_classes2/com/swiftsoft/anixartd/DaggerApp_HiltComponents_SingletonC$SingletonCImpl$SwitchingProvider.smali'
|
||||||
|
|
||||||
|
content = ""
|
||||||
with open(filepath, 'r') as f:
|
with open(filepath, 'r') as f:
|
||||||
for line in f.readlines():
|
for line in f.readlines():
|
||||||
if "addInterceptor" in line: continue
|
if "addInterceptor" in line: continue
|
||||||
content += line
|
content += line
|
||||||
|
|
||||||
with open(filepath, 'w') as f:
|
with open(filepath, 'w') as f:
|
||||||
f.write(content)
|
f.write(content)
|
||||||
|
|
||||||
|
|||||||
@@ -1,22 +0,0 @@
|
|||||||
"""Remove unnecessary files"""
|
|
||||||
priority = 0
|
|
||||||
from tqdm import tqdm
|
|
||||||
|
|
||||||
import os
|
|
||||||
import shutil
|
|
||||||
|
|
||||||
|
|
||||||
def apply(config: dict) -> bool:
|
|
||||||
for item in os.listdir("./decompiled/unknown/"):
|
|
||||||
item_path = os.path.join("./decompiled/unknown/", item)
|
|
||||||
|
|
||||||
if os.path.isfile(item_path):
|
|
||||||
os.remove(item_path)
|
|
||||||
if config.get("verbose", False):
|
|
||||||
tqdm.write(f'Удалён файл: {item_path}')
|
|
||||||
elif os.path.isdir(item_path):
|
|
||||||
if item not in config["cleanup"]["keep_dirs"]:
|
|
||||||
shutil.rmtree(item_path)
|
|
||||||
if config.get("verbose", False):
|
|
||||||
tqdm.write(f'Удалена папка: {item_path}')
|
|
||||||
return True
|
|
||||||
+73
-39
@@ -1,23 +1,64 @@
|
|||||||
"""Change application theme"""
|
"""Изменяет цветовую тему приложения и иконку
|
||||||
|
|
||||||
|
"color_theme": {
|
||||||
|
"enabled": true,
|
||||||
|
"logo": {
|
||||||
|
"gradient": {
|
||||||
|
"angle": 0.0,
|
||||||
|
"start_color": "#ffccff00",
|
||||||
|
"end_color": "#ffcccc00"
|
||||||
|
},
|
||||||
|
"ears_color": "#ffffd0d0"
|
||||||
|
},
|
||||||
|
"colors": {
|
||||||
|
"primary": "#ccff00",
|
||||||
|
"secondary": "#ffcccc00",
|
||||||
|
"background": "#ffffff",
|
||||||
|
"text": "#000000"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
|
||||||
priority = 0
|
priority = 0
|
||||||
|
|
||||||
|
# imports
|
||||||
from lxml import etree
|
from lxml import etree
|
||||||
|
from typing import Dict, Any
|
||||||
|
from pydantic import Field, BaseModel
|
||||||
|
|
||||||
|
from utils.config import PatchConfig
|
||||||
from utils.public import (
|
from utils.public import (
|
||||||
insert_after_public,
|
insert_after_public,
|
||||||
insert_after_color,
|
insert_after_color,
|
||||||
insert_after_id,
|
|
||||||
change_color,
|
change_color,
|
||||||
)
|
)
|
||||||
|
|
||||||
def apply(config: dict) -> bool:
|
#Config
|
||||||
main_color = config["theme"]["colors"]["primary"]
|
class Gradient(BaseModel):
|
||||||
splash_color = config["theme"]["colors"]["secondary"]
|
angle: float = Field(0.0, description="Угол градиента")
|
||||||
gradient_angle = config["theme"]["gradient"]["angle"]
|
start_color: str = Field("#ffccff00", description="Начальный цвет градиента")
|
||||||
gradient_from = config["theme"]["gradient"]["from"]
|
end_color: str = Field("#ffcccc00", description="Конечный цвет градиента")
|
||||||
gradient_to = config["theme"]["gradient"]["to"]
|
|
||||||
|
|
||||||
# No connection alert coolor
|
class Logo(BaseModel):
|
||||||
|
gradient: Gradient = Field(Gradient(), description="Настройки градиента") # type: ignore [reportCallIssue]
|
||||||
|
ears_color: str = Field("#ffd0d0d0", description="Цвет ушей логотипа")
|
||||||
|
|
||||||
|
class Colors(BaseModel):
|
||||||
|
primary: str = Field("#ccff00", description="Основной цвет")
|
||||||
|
secondary: str = Field("#ffcccc00", description="Вторичный цвет")
|
||||||
|
background: str = Field("#ffffff", description="Фоновый цвет")
|
||||||
|
text: str = Field("#000000", description="Цвет текста")
|
||||||
|
|
||||||
|
class Config(PatchConfig):
|
||||||
|
logo: Logo = Field(Logo(), description="Настройки цветов логотипа") # type: ignore [reportCallIssue]
|
||||||
|
colors: Colors = Field(Colors(), description="Настройки цветов") # type: ignore [reportCallIssue]
|
||||||
|
|
||||||
|
# Patch
|
||||||
|
def apply(config: Config, base: Dict[str, Any]) -> bool:
|
||||||
|
main_color = config.colors.primary
|
||||||
|
splash_color = config.colors.secondary
|
||||||
|
|
||||||
|
# Обновление сообщения об отсутствии подключения
|
||||||
with open("./decompiled/assets/no_connection.html", "r", encoding="utf-8") as file:
|
with open("./decompiled/assets/no_connection.html", "r", encoding="utf-8") as file:
|
||||||
file_contents = file.read()
|
file_contents = file.read()
|
||||||
|
|
||||||
@@ -26,39 +67,40 @@ def apply(config: dict) -> bool:
|
|||||||
with open("./decompiled/assets/no_connection.html", "w", encoding="utf-8") as file:
|
with open("./decompiled/assets/no_connection.html", "w", encoding="utf-8") as file:
|
||||||
file.write(new_contents)
|
file.write(new_contents)
|
||||||
|
|
||||||
# For logo
|
# Суффиксы лого
|
||||||
drawable_types = ["", "-night"]
|
drawable_types = ["", "-night"]
|
||||||
|
|
||||||
for drawable_type in drawable_types:
|
for drawable_type in drawable_types:
|
||||||
# Application logo gradient colors
|
# Градиент лого приложения
|
||||||
file_path = f"./decompiled/res/drawable{drawable_type}/$logo__0.xml"
|
file_path = f"./decompiled/res/drawable{drawable_type}/$logo__0.xml"
|
||||||
|
|
||||||
parser = etree.XMLParser(remove_blank_text=True)
|
parser = etree.XMLParser(remove_blank_text=True)
|
||||||
tree = etree.parse(file_path, parser)
|
tree = etree.parse(file_path, parser)
|
||||||
root = tree.getroot()
|
root = tree.getroot()
|
||||||
|
|
||||||
# Change attributes with namespace
|
# Замена атрибутов значениями из конфигурации
|
||||||
root.set(f"{{{config['xml_ns']['android']}}}angle", gradient_angle)
|
root.set(f"{{{base['xml_ns']['android']}}}angle", str(config.logo.gradient.angle))
|
||||||
root.set(f"{{{config['xml_ns']['android']}}}startColor", gradient_from)
|
root.set(f"{{{base['xml_ns']['android']}}}startColor", config.logo.gradient.start_color)
|
||||||
root.set(f"{{{config['xml_ns']['android']}}}endColor", gradient_to)
|
root.set(f"{{{base['xml_ns']['android']}}}endColor", config.logo.gradient.end_color)
|
||||||
|
|
||||||
# Save back
|
# Сохранение
|
||||||
tree.write(file_path, pretty_print=True, xml_declaration=True, encoding="utf-8")
|
tree.write(file_path, pretty_print=True, xml_declaration=True, encoding="utf-8")
|
||||||
|
|
||||||
# Application logo anim color
|
# Замена анимации лого
|
||||||
file_path = f"./decompiled/res/drawable{drawable_type}/$logo_splash_anim__0.xml"
|
file_path = f"./decompiled/res/drawable{drawable_type}/$logo_splash_anim__0.xml"
|
||||||
|
|
||||||
parser = etree.XMLParser(remove_blank_text=True)
|
parser = etree.XMLParser(remove_blank_text=True)
|
||||||
tree = etree.parse(file_path, parser)
|
tree = etree.parse(file_path, parser)
|
||||||
root = tree.getroot()
|
root = tree.getroot()
|
||||||
|
|
||||||
# Finding "path"
|
for el in root.findall("path", namespaces=base["xml_ns"]):
|
||||||
for el in root.findall("path", namespaces=config["xml_ns"]):
|
name = el.get(f"{{{base['xml_ns']['android']}}}name")
|
||||||
name = el.get(f"{{{config['xml_ns']['android']}}}name")
|
|
||||||
if name == "path":
|
if name == "path":
|
||||||
el.set(f"{{{config['xml_ns']['android']}}}fillColor", splash_color)
|
el.set(f"{{{base['xml_ns']['android']}}}fillColor", config.colors.secondary)
|
||||||
|
elif name in ["path_1", "path_2"]:
|
||||||
|
el.set(f"{{{base['xml_ns']['android']}}}fillColor", config.logo.ears_color)
|
||||||
|
|
||||||
# Save back
|
# Сохранение
|
||||||
tree.write(file_path, pretty_print=True, xml_declaration=True, encoding="utf-8")
|
tree.write(file_path, pretty_print=True, xml_declaration=True, encoding="utf-8")
|
||||||
|
|
||||||
for filename in ["$ic_launcher_foreground__0", "$ic_banner_foreground__0"]:
|
for filename in ["$ic_launcher_foreground__0", "$ic_banner_foreground__0"]:
|
||||||
@@ -68,25 +110,28 @@ def apply(config: dict) -> bool:
|
|||||||
tree = etree.parse(file_path, parser)
|
tree = etree.parse(file_path, parser)
|
||||||
root = tree.getroot()
|
root = tree.getroot()
|
||||||
|
|
||||||
# Change attributes with namespace
|
# Замена атрибутов значениями из конфигурации
|
||||||
root.set(f"{{{config['xml_ns']['android']}}}angle", gradient_angle)
|
root.set(f"{{{base['xml_ns']['android']}}}angle", str(config.logo.gradient.angle))
|
||||||
items = root.findall("item", namespaces=config['xml_ns'])
|
items = root.findall("item", namespaces=base['xml_ns'])
|
||||||
assert len(items) == 2
|
assert len(items) == 2
|
||||||
items[0].set(f"{{{config['xml_ns']['android']}}}color", gradient_from)
|
items[0].set(f"{{{base['xml_ns']['android']}}}color", config.logo.gradient.start_color)
|
||||||
items[1].set(f"{{{config['xml_ns']['android']}}}color", gradient_to)
|
items[1].set(f"{{{base['xml_ns']['android']}}}color", config.logo.gradient.end_color)
|
||||||
|
|
||||||
# Save back
|
# Сохранение
|
||||||
tree.write(file_path, pretty_print=True, xml_declaration=True, encoding="utf-8")
|
tree.write(file_path, pretty_print=True, xml_declaration=True, encoding="utf-8")
|
||||||
|
|
||||||
|
# Добаление новых цветов для темы
|
||||||
insert_after_public("carmine", "custom_color")
|
insert_after_public("carmine", "custom_color")
|
||||||
insert_after_public("carmine_alpha_10", "custom_color_alpha_10")
|
insert_after_public("carmine_alpha_10", "custom_color_alpha_10")
|
||||||
insert_after_color("carmine", "custom_color", main_color[0]+'ff'+main_color[1:])
|
insert_after_color("carmine", "custom_color", main_color[0]+'ff'+main_color[1:])
|
||||||
insert_after_color("carmine_alpha_10", "custom_color_alpha_10", main_color[0]+'1a'+main_color[1:])
|
insert_after_color("carmine_alpha_10", "custom_color_alpha_10", main_color[0]+'1a'+main_color[1:])
|
||||||
|
|
||||||
|
# Замена цветов
|
||||||
change_color("accent_alpha_10", main_color[0]+'1a'+main_color[1:])
|
change_color("accent_alpha_10", main_color[0]+'1a'+main_color[1:])
|
||||||
change_color("accent_alpha_20", main_color[0]+'33'+main_color[1:])
|
change_color("accent_alpha_20", main_color[0]+'33'+main_color[1:])
|
||||||
change_color("accent_alpha_50", main_color[0]+'80'+main_color[1:])
|
change_color("accent_alpha_50", main_color[0]+'80'+main_color[1:])
|
||||||
change_color("accent_alpha_70", main_color[0]+'b3'+main_color[1:])
|
change_color("accent_alpha_70", main_color[0]+'b3'+main_color[1:])
|
||||||
|
|
||||||
change_color("colorAccent", main_color[0]+'ff'+main_color[1:])
|
change_color("colorAccent", main_color[0]+'ff'+main_color[1:])
|
||||||
change_color("link_color", main_color[0]+'ff'+main_color[1:])
|
change_color("link_color", main_color[0]+'ff'+main_color[1:])
|
||||||
change_color("link_color_alpha_70", main_color[0]+'b3'+main_color[1:])
|
change_color("link_color_alpha_70", main_color[0]+'b3'+main_color[1:])
|
||||||
@@ -97,15 +142,4 @@ def apply(config: dict) -> bool:
|
|||||||
change_color("bottom_nav_indicator_icon_checked", main_color[0]+'ff'+main_color[1:])
|
change_color("bottom_nav_indicator_icon_checked", main_color[0]+'ff'+main_color[1:])
|
||||||
change_color("bottom_nav_indicator_label_checked", main_color[0]+'ff'+main_color[1:])
|
change_color("bottom_nav_indicator_label_checked", main_color[0]+'ff'+main_color[1:])
|
||||||
|
|
||||||
insert_after_public("warning_error_counter_background", "ic_custom_telegram")
|
|
||||||
insert_after_public("warning_error_counter_background", "ic_custom_crown")
|
|
||||||
|
|
||||||
try:
|
|
||||||
last = "speed75"
|
|
||||||
for speed in config.get("speeds", []):
|
|
||||||
insert_after_public(last, f"speed{int(float(speed)*10)}")
|
|
||||||
insert_after_id(last, f"speed{int(float(speed)*10)}")
|
|
||||||
last = f"speed{int(float(speed)*10)}"
|
|
||||||
except Exception as e:
|
|
||||||
print(f"Error occurred while processing speeds: {e}")
|
|
||||||
return True
|
return True
|
||||||
|
|||||||
@@ -0,0 +1,99 @@
|
|||||||
|
"""Меняет местами кнопки лайка и дизлайка у коментария и иконки
|
||||||
|
|
||||||
|
"comment_vote": {
|
||||||
|
"enabled": true,
|
||||||
|
"replace": true,
|
||||||
|
"custom_icons": true,
|
||||||
|
"icons_size": "14.0dip"
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
|
||||||
|
priority = 0
|
||||||
|
|
||||||
|
# imports
|
||||||
|
import os
|
||||||
|
import shutil
|
||||||
|
from tqdm import tqdm
|
||||||
|
from lxml import etree
|
||||||
|
from pydantic import Field
|
||||||
|
from typing import Dict, Any
|
||||||
|
from utils.config import PatchConfig
|
||||||
|
|
||||||
|
#Config
|
||||||
|
class Config(PatchConfig):
|
||||||
|
replace: bool = Field(True, description="Менять местами лайк/дизлайк")
|
||||||
|
custom_icons: bool = Field(True, description="Кастомные иконки")
|
||||||
|
icon_size: str = Field("18.0dip", description="Размер иконки")
|
||||||
|
|
||||||
|
# Patch
|
||||||
|
def apply(config, base: Dict[str, Any]) -> bool:
|
||||||
|
file_path = "./decompiled/res/layout/item_comment.xml"
|
||||||
|
parser = etree.XMLParser(remove_blank_text=True)
|
||||||
|
tree = etree.parse(file_path, parser)
|
||||||
|
root = tree.getroot()
|
||||||
|
|
||||||
|
tqdm.write("Меняем размер иконок лайка и дизлайка...")
|
||||||
|
for icon in root.xpath(
|
||||||
|
".//*[@android:id='@id/votePlusInactive']//ImageView | "
|
||||||
|
".//*[@android:id='@id/votePlusActive']//ImageView | "
|
||||||
|
".//*[@android:id='@id/voteMinusInactive']//ImageView | "
|
||||||
|
".//*[@android:id='@id/voteMinusActive']//ImageView",
|
||||||
|
namespaces=base['xml_ns'],
|
||||||
|
):
|
||||||
|
icon.set(f"{{{base['xml_ns']['android']}}}layout_width", config.icon_size)
|
||||||
|
# icon.set(f"{{{base['xml_ns']['android']}}}layout_height", config.icon_size)
|
||||||
|
|
||||||
|
if config.replace:
|
||||||
|
tqdm.write("Меняем местами лайк и дизлайк комментария...")
|
||||||
|
|
||||||
|
containers = root.xpath(
|
||||||
|
".//LinearLayout[.//*[@android:id='@id/voteMinus'] and .//*[@android:id='@id/votePlus']]",
|
||||||
|
namespaces=base["xml_ns"],
|
||||||
|
)
|
||||||
|
|
||||||
|
found = False
|
||||||
|
for container in containers:
|
||||||
|
children = list(container)
|
||||||
|
vote_plus = None
|
||||||
|
vote_minus = None
|
||||||
|
|
||||||
|
for ch in children:
|
||||||
|
cid = ch.get(f'{{{base["xml_ns"]["android"]}}}id')
|
||||||
|
if cid == "@id/votePlus":
|
||||||
|
vote_plus = ch
|
||||||
|
elif cid == "@id/voteMinus":
|
||||||
|
vote_minus = ch
|
||||||
|
|
||||||
|
if vote_plus is not None and vote_minus is not None:
|
||||||
|
found = True
|
||||||
|
i_plus = children.index(vote_plus)
|
||||||
|
i_minus = children.index(vote_minus)
|
||||||
|
children[i_plus], children[i_minus] = children[i_minus], children[i_plus]
|
||||||
|
container[:] = children
|
||||||
|
tqdm.write("Кнопки лайк и дизлайк поменялись местами.")
|
||||||
|
break
|
||||||
|
|
||||||
|
if not found:
|
||||||
|
tqdm.write("Не удалось найти оба узла votePlus/voteMinus даже в общих LinearLayout.")
|
||||||
|
|
||||||
|
if config.custom_icons:
|
||||||
|
tqdm.write("Заменяем иконки лайка и дизлайка на кастомные...")
|
||||||
|
for suffix in ["up", "up_40", "down", "down_40"]:
|
||||||
|
shutil.copy(
|
||||||
|
f"./resources/ic_chevron_{suffix}.xml",
|
||||||
|
f"./decompiled/res/drawable/ic_chevron_{suffix}.xml",
|
||||||
|
)
|
||||||
|
|
||||||
|
for inactive in root.xpath(
|
||||||
|
".//*[@android:id='@id/votePlusInactive'] | .//*[@android:id='@id/voteMinusInactive']",
|
||||||
|
namespaces=base["xml_ns"],
|
||||||
|
):
|
||||||
|
for img in inactive.xpath(".//ImageView[@android:src]", namespaces=base["xml_ns"]):
|
||||||
|
src = img.get(f'{{{base["xml_ns"]["android"]}}}src', "")
|
||||||
|
if src.startswith("@drawable/") and not src.endswith("_40"):
|
||||||
|
img.set(f'{{{base["xml_ns"]["android"]}}}src', src + "_40")
|
||||||
|
|
||||||
|
# Сохраняем
|
||||||
|
tree.write(file_path, encoding="utf-8", xml_declaration=True, pretty_print=True)
|
||||||
|
|
||||||
|
return True
|
||||||
@@ -0,0 +1,308 @@
|
|||||||
|
"""Удаляет ненужное и сжимает ресурсы что-бы уменьшить размер АПК
|
||||||
|
|
||||||
|
Эффективность на проверена на версии 9.0 Beta 7
|
||||||
|
разница = оригинальный размер апк - патченный размер апк, не учитывая другие патчи
|
||||||
|
|
||||||
|
| Настройка | Размер файла | Разница | % |
|
||||||
|
| :--------------: | :-------------------: | :-----------------: | :-: |
|
||||||
|
| Ничего | 17092 bytes - 17.1 MB | - | - |
|
||||||
|
| Сжатие PNG | 17072 bytes - 17.1 MB | 20 bytes - 0.0 MB | 0.11% |
|
||||||
|
| Удалить unknown | 17020 bytes - 17.0 MB | 72 bytes - 0.1 MB | 0.42% |
|
||||||
|
| Удалить draws | 16940 bytes - 16.9 MB | 152 bytes - 0.2 MB | 0.89% |
|
||||||
|
| Удалить lines | 16444 bytes - 16.4 MB | 648 bytes - 0.7 MB | 3.79% |
|
||||||
|
| Удалить ai voice | 15812 bytes - 15.8 MB | 1280 bytes - 1.3 MB | 7.49% |
|
||||||
|
| Удалить языки | 15764 bytes - 15.7 MB | 1328 bytes - 1.3 MB | 7.76% |
|
||||||
|
| Все включены | 13592 bytes - 13.6 MB | 3500 bytes - 4.8 MB | 20.5% |
|
||||||
|
|
||||||
|
"compress": {
|
||||||
|
"enabled": true,
|
||||||
|
"remove_language_files": true, // удаляет все языки кроме русского и английского
|
||||||
|
"remove_AI_voiceover": true, // заменяет ИИ озвучку маскота аниксы пустыми mp3 файлами
|
||||||
|
"remove_debug_lines": false, // удаляет строки `.line n` из smali файлов использованные для дебага
|
||||||
|
"remove_drawable_files": false, // удаляет неиспользованные drawable-* из директории decompiled/res
|
||||||
|
"remove_unknown_files": true, // удаляет файлы из директории decompiled/unknown
|
||||||
|
"remove_unknown_files_keep_dirs": ["META-INF", "kotlin"], // оставляет указанные директории в decompiled/unknown
|
||||||
|
"compress_png_files": true // сжимает PNG в директории decompiled/res
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
|
||||||
|
priority = -1
|
||||||
|
|
||||||
|
# imports
|
||||||
|
import os
|
||||||
|
import shutil
|
||||||
|
import subprocess
|
||||||
|
from tqdm import tqdm
|
||||||
|
from pydantic import Field
|
||||||
|
from typing import Dict, List, Any
|
||||||
|
|
||||||
|
from utils.config import PatchConfig
|
||||||
|
from utils.smali_parser import get_smali_lines, save_smali_lines
|
||||||
|
|
||||||
|
#Config
|
||||||
|
class Config(PatchConfig):
|
||||||
|
remove_language_files: bool = Field(True, description="Удаляет все языки кроме русского и английского")
|
||||||
|
remove_AI_voiceover: bool = Field(True, description="Заменяет ИИ озвучку маскота аниксы пустыми mp3 файлами")
|
||||||
|
remove_debug_lines: bool = Field(False, description="Удаляет строки `.line n` из smali файлов использованные для дебага")
|
||||||
|
remove_drawable_files: bool = Field(False, description="Удаляет неиспользованные drawable-* из директории decompiled/res")
|
||||||
|
remove_unknown_files: bool = Field(True, description="Удаляет файлы из директории decompiled/unknown")
|
||||||
|
remove_unknown_files_keep_dirs: List[str] = Field(["META-INF", "kotlin"], description="Оставляет указанные директории в decompiled/unknown")
|
||||||
|
compress_png_files: bool = Field(True, description="Сжимает PNG в директории decompiled/res")
|
||||||
|
|
||||||
|
# Patch
|
||||||
|
def remove_unknown_files(config: Config, base: Dict[str, Any]):
|
||||||
|
path = "./decompiled/unknown"
|
||||||
|
items = os.listdir(path)
|
||||||
|
for item in items:
|
||||||
|
item_path = f"{path}/{item}"
|
||||||
|
if os.path.isfile(item_path):
|
||||||
|
os.remove(item_path)
|
||||||
|
if base.get("verbose", False):
|
||||||
|
tqdm.write(f"Удалён файл: {item_path}")
|
||||||
|
elif os.path.isdir(item_path):
|
||||||
|
if item not in config.remove_unknown_files_keep_dirs:
|
||||||
|
shutil.rmtree(item_path)
|
||||||
|
if base.get("verbose", False):
|
||||||
|
tqdm.write(f"Удалёна директория: {item_path}")
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def remove_debug_lines(config: Dict[str, Any]):
|
||||||
|
for root, _, files in os.walk("./decompiled"):
|
||||||
|
for filename in files:
|
||||||
|
file_path = os.path.join(root, filename)
|
||||||
|
if os.path.isfile(file_path) and filename.endswith(".smali"):
|
||||||
|
file_content = get_smali_lines(file_path)
|
||||||
|
new_content = []
|
||||||
|
for line in file_content:
|
||||||
|
if line.find(".line") >= 0:
|
||||||
|
continue
|
||||||
|
new_content.append(line)
|
||||||
|
save_smali_lines(file_path, new_content)
|
||||||
|
if config.get("verbose", False):
|
||||||
|
tqdm.write(f"Удалены дебаг линии из: {file_path}")
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def compress_png(config: Dict[str, Any], png_path: str):
|
||||||
|
try:
|
||||||
|
assert subprocess.run(
|
||||||
|
[
|
||||||
|
"pngquant",
|
||||||
|
"--force",
|
||||||
|
"--ext", ".png",
|
||||||
|
"--quality=65-90",
|
||||||
|
png_path,
|
||||||
|
],
|
||||||
|
capture_output=True,
|
||||||
|
).returncode in [0, 99]
|
||||||
|
if config.get("verbose", False):
|
||||||
|
tqdm.write(f"Сжат файл PNG: {png_path}")
|
||||||
|
return True
|
||||||
|
except subprocess.CalledProcessError as e:
|
||||||
|
tqdm.write(f"Ошибка при сжатии {png_path}: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def compress_png_files(config: Dict[str, Any]):
|
||||||
|
compressed = []
|
||||||
|
for root, _, files in os.walk("./decompiled"):
|
||||||
|
for file in files:
|
||||||
|
if file.lower().endswith(".png"):
|
||||||
|
compress_png(config, f"{root}/{file}")
|
||||||
|
compressed.append(f"{root}/{file}")
|
||||||
|
return len(compressed) > 0 and any(compressed)
|
||||||
|
|
||||||
|
|
||||||
|
def remove_AI_voiceover(config: Dict[str, Any]):
|
||||||
|
blank = "./resources/blank.mp3"
|
||||||
|
path = "./decompiled/res/raw"
|
||||||
|
files = [
|
||||||
|
"reputation_1.mp3",
|
||||||
|
"reputation_2.mp3",
|
||||||
|
"reputation_3.mp3",
|
||||||
|
"sound_beta_1.mp3",
|
||||||
|
"sound_create_blog_1.mp3",
|
||||||
|
"sound_create_blog_2.mp3",
|
||||||
|
"sound_create_blog_3.mp3",
|
||||||
|
"sound_create_blog_4.mp3",
|
||||||
|
"sound_create_blog_5.mp3",
|
||||||
|
"sound_create_blog_6.mp3",
|
||||||
|
"sound_create_blog_reputation_1.mp3",
|
||||||
|
"sound_create_blog_reputation_2.mp3",
|
||||||
|
"sound_create_blog_reputation_3.mp3",
|
||||||
|
"sound_create_blog_reputation_4.mp3",
|
||||||
|
"sound_create_blog_reputation_5.mp3",
|
||||||
|
"sound_create_blog_reputation_6.mp3",
|
||||||
|
]
|
||||||
|
|
||||||
|
for file in files:
|
||||||
|
if os.path.exists(f"{path}/{file}"):
|
||||||
|
os.remove(f"{path}/{file}")
|
||||||
|
shutil.copyfile(blank, f"{path}/{file}")
|
||||||
|
if config.get("verbose", False):
|
||||||
|
tqdm.write(f"Файл mp3 был заменён на пустой: {path}/{file}")
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def remove_language_files(config: Dict[str, Any]):
|
||||||
|
path = "./decompiled/res"
|
||||||
|
folders = [
|
||||||
|
"values-af",
|
||||||
|
"values-am",
|
||||||
|
"values-ar",
|
||||||
|
"values-as",
|
||||||
|
"values-az",
|
||||||
|
"values-b+es+419",
|
||||||
|
"values-b+sr+Latn",
|
||||||
|
"values-be",
|
||||||
|
"values-bg",
|
||||||
|
"values-bn",
|
||||||
|
"values-bs",
|
||||||
|
"values-ca",
|
||||||
|
"values-cs",
|
||||||
|
"values-da",
|
||||||
|
"values-de",
|
||||||
|
"values-el",
|
||||||
|
"values-en-rAU",
|
||||||
|
"values-en-rCA",
|
||||||
|
"values-en-rGB",
|
||||||
|
"values-en-rIN",
|
||||||
|
"values-en-rXC",
|
||||||
|
"values-es",
|
||||||
|
"values-es-rGT",
|
||||||
|
"values-es-rUS",
|
||||||
|
"values-et",
|
||||||
|
"values-eu",
|
||||||
|
"values-fa",
|
||||||
|
"values-fi",
|
||||||
|
"values-fr",
|
||||||
|
"values-fr-rCA",
|
||||||
|
"values-gl",
|
||||||
|
"values-gu",
|
||||||
|
"values-hi",
|
||||||
|
"values-hr",
|
||||||
|
"values-hu",
|
||||||
|
"values-hy",
|
||||||
|
"values-in",
|
||||||
|
"values-is",
|
||||||
|
"values-it",
|
||||||
|
"values-iw",
|
||||||
|
"values-ja",
|
||||||
|
"values-ka",
|
||||||
|
"values-kk",
|
||||||
|
"values-km",
|
||||||
|
"values-kn",
|
||||||
|
"values-ko",
|
||||||
|
"values-ky",
|
||||||
|
"values-lo",
|
||||||
|
"values-lt",
|
||||||
|
"values-lv",
|
||||||
|
"values-mk",
|
||||||
|
"values-ml",
|
||||||
|
"values-mn",
|
||||||
|
"values-mr",
|
||||||
|
"values-ms",
|
||||||
|
"values-my",
|
||||||
|
"values-nb",
|
||||||
|
"values-ne",
|
||||||
|
"values-nl",
|
||||||
|
"values-or",
|
||||||
|
"values-pa",
|
||||||
|
"values-pl",
|
||||||
|
"values-pt",
|
||||||
|
"values-pt-rBR",
|
||||||
|
"values-pt-rPT",
|
||||||
|
"values-ro",
|
||||||
|
"values-si",
|
||||||
|
"values-sk",
|
||||||
|
"values-sl",
|
||||||
|
"values-sq",
|
||||||
|
"values-sr",
|
||||||
|
"values-sv",
|
||||||
|
"values-sw",
|
||||||
|
"values-ta",
|
||||||
|
"values-te",
|
||||||
|
"values-th",
|
||||||
|
"values-tl",
|
||||||
|
"values-tr",
|
||||||
|
"values-uk",
|
||||||
|
"values-ur",
|
||||||
|
"values-uz",
|
||||||
|
"values-vi",
|
||||||
|
"values-zh",
|
||||||
|
"values-zh-rCN",
|
||||||
|
"values-zh-rHK",
|
||||||
|
"values-zh-rTW",
|
||||||
|
"values-zu",
|
||||||
|
"values-watch",
|
||||||
|
]
|
||||||
|
|
||||||
|
for folder in folders:
|
||||||
|
if os.path.exists(f"{path}/{folder}"):
|
||||||
|
shutil.rmtree(f"{path}/{folder}")
|
||||||
|
if config.get("verbose", False):
|
||||||
|
tqdm.write(f"Удалена директория: {path}/{folder}")
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def remove_drawable_files(config: Dict[str, Any]):
|
||||||
|
path = "./decompiled/res"
|
||||||
|
folders = [
|
||||||
|
"drawable-en-hdpi",
|
||||||
|
"drawable-en-ldpi",
|
||||||
|
"drawable-en-mdpi",
|
||||||
|
"drawable-en-xhdpi",
|
||||||
|
"drawable-en-xxhdpi",
|
||||||
|
"drawable-en-xxxhdpi",
|
||||||
|
"drawable-ldrtl-hdpi",
|
||||||
|
"drawable-ldrtl-mdpi",
|
||||||
|
"drawable-ldrtl-xhdpi",
|
||||||
|
"drawable-ldrtl-xxhdpi",
|
||||||
|
"drawable-ldrtl-xxxhdpi",
|
||||||
|
"drawable-tr-anydpi",
|
||||||
|
"drawable-tr-hdpi",
|
||||||
|
"drawable-tr-ldpi",
|
||||||
|
"drawable-tr-mdpi",
|
||||||
|
"drawable-tr-xhdpi",
|
||||||
|
"drawable-tr-xxhdpi",
|
||||||
|
"drawable-tr-xxxhdpi",
|
||||||
|
"drawable-watch",
|
||||||
|
"layout-watch",
|
||||||
|
]
|
||||||
|
|
||||||
|
for folder in folders:
|
||||||
|
if os.path.exists(f"{path}/{folder}"):
|
||||||
|
shutil.rmtree(f"{path}/{folder}")
|
||||||
|
if config.get("verbose", False):
|
||||||
|
tqdm.write(f"Удалена директория: {path}/{folder}")
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def apply(config: Config, base: Dict[str, Any]) -> bool:
|
||||||
|
if config.remove_unknown_files:
|
||||||
|
tqdm.write(f"Удаление неизвестных файлов...")
|
||||||
|
remove_unknown_files(config, base)
|
||||||
|
|
||||||
|
if config.remove_drawable_files:
|
||||||
|
tqdm.write(f"Удаление директорий drawable-xx...")
|
||||||
|
remove_drawable_files(base)
|
||||||
|
|
||||||
|
if config.compress_png_files:
|
||||||
|
tqdm.write(f"Сжатие PNG файлов...")
|
||||||
|
compress_png_files(base)
|
||||||
|
|
||||||
|
if config.remove_language_files:
|
||||||
|
tqdm.write(f"Удаление языков...")
|
||||||
|
remove_language_files(base)
|
||||||
|
|
||||||
|
if config.remove_AI_voiceover:
|
||||||
|
tqdm.write(f"Удаление ИИ озвучки...")
|
||||||
|
remove_AI_voiceover(base)
|
||||||
|
|
||||||
|
if config.remove_debug_lines:
|
||||||
|
tqdm.write(f"Удаление дебаг линий...")
|
||||||
|
remove_debug_lines(base)
|
||||||
|
|
||||||
|
return True
|
||||||
@@ -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)
|
|
||||||
+24
-9
@@ -1,6 +1,18 @@
|
|||||||
"""Disable ad banners"""
|
"""Удаляет баннеры рекламы
|
||||||
|
|
||||||
|
"disable_ad": {
|
||||||
|
"enabled": true
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
|
||||||
priority = 0
|
priority = 0
|
||||||
|
|
||||||
|
# imports
|
||||||
|
import textwrap
|
||||||
|
from tqdm import tqdm
|
||||||
|
from typing import Dict, Any
|
||||||
|
|
||||||
|
from utils.config import PatchConfig
|
||||||
from utils.smali_parser import (
|
from utils.smali_parser import (
|
||||||
find_smali_method_end,
|
find_smali_method_end,
|
||||||
find_smali_method_start,
|
find_smali_method_start,
|
||||||
@@ -9,15 +21,18 @@ from utils.smali_parser import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
replace = """ .locals 0
|
#Config
|
||||||
|
class Config(PatchConfig): ...
|
||||||
const/4 p0, 0x1
|
|
||||||
|
|
||||||
return p0
|
|
||||||
"""
|
|
||||||
|
|
||||||
|
|
||||||
def apply(config) -> bool:
|
# Patch
|
||||||
|
def apply(config: Config, base: Dict[str, Any]) -> bool:
|
||||||
|
replacement = [f'\t{line}\n' for line in textwrap.dedent("""\
|
||||||
|
.locals 0
|
||||||
|
const/4 p0, 0x1
|
||||||
|
return p0
|
||||||
|
""").splitlines()]
|
||||||
|
|
||||||
path = "./decompiled/smali_classes2/com/swiftsoft/anixartd/Prefs.smali"
|
path = "./decompiled/smali_classes2/com/swiftsoft/anixartd/Prefs.smali"
|
||||||
lines = get_smali_lines(path)
|
lines = get_smali_lines(path)
|
||||||
for index, line in enumerate(lines):
|
for index, line in enumerate(lines):
|
||||||
@@ -25,7 +40,7 @@ def apply(config) -> bool:
|
|||||||
method_start = find_smali_method_start(lines, index)
|
method_start = find_smali_method_start(lines, index)
|
||||||
method_end = find_smali_method_end(lines, index)
|
method_end = find_smali_method_end(lines, index)
|
||||||
new_content = replace_smali_method_body(
|
new_content = replace_smali_method_body(
|
||||||
lines, method_start, method_end, replace
|
lines, method_start, method_end, replacement
|
||||||
)
|
)
|
||||||
|
|
||||||
with open(path, "w", encoding="utf-8") as file:
|
with open(path, "w", encoding="utf-8") as file:
|
||||||
|
|||||||
@@ -1,12 +1,26 @@
|
|||||||
"""Remove beta banner"""
|
"""Удаляет баннеры бета-версии
|
||||||
|
|
||||||
|
"disable_beta_banner": {
|
||||||
|
"enabled": true
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
|
||||||
priority = 0
|
priority = 0
|
||||||
from tqdm import tqdm
|
|
||||||
|
|
||||||
|
# imports
|
||||||
import os
|
import os
|
||||||
|
from tqdm import tqdm
|
||||||
from lxml import etree
|
from lxml import etree
|
||||||
|
from typing import Dict, Any
|
||||||
|
|
||||||
|
from utils.config import PatchConfig
|
||||||
|
from utils.smali_parser import get_smali_lines, save_smali_lines
|
||||||
|
|
||||||
def apply(config) -> bool:
|
#Config
|
||||||
|
class Config(PatchConfig): ...
|
||||||
|
|
||||||
|
# Patch
|
||||||
|
def apply(config: Config, base: Dict[str, Any]) -> bool:
|
||||||
attributes = [
|
attributes = [
|
||||||
"paddingTop",
|
"paddingTop",
|
||||||
"paddingBottom",
|
"paddingBottom",
|
||||||
@@ -27,8 +41,9 @@ def apply(config) -> bool:
|
|||||||
root = tree.getroot()
|
root = tree.getroot()
|
||||||
|
|
||||||
for attr in attributes:
|
for attr in attributes:
|
||||||
# tqdm.write(f"set {attr} = 0.0dip")
|
if base.get("verbose", False):
|
||||||
root.set(f"{{{config["xml_ns"]['android']}}}{attr}", "0.0dip")
|
tqdm.write(f"set {attr} = 0.0dip")
|
||||||
|
root.set(f"{{{base['xml_ns']['android']}}}{attr}", "0.0dip")
|
||||||
|
|
||||||
tree.write(
|
tree.write(
|
||||||
beta_banner_xml, pretty_print=True, xml_declaration=True, encoding="utf-8"
|
beta_banner_xml, pretty_print=True, xml_declaration=True, encoding="utf-8"
|
||||||
|
|||||||
@@ -1,44 +0,0 @@
|
|||||||
"""Insert new files"""
|
|
||||||
priority = 0
|
|
||||||
|
|
||||||
import shutil
|
|
||||||
import os
|
|
||||||
|
|
||||||
|
|
||||||
def apply(config: dict) -> bool:
|
|
||||||
# Mod first launch window
|
|
||||||
shutil.copytree(
|
|
||||||
"./patches/resources/smali_classes4/", "./decompiled/smali_classes4/"
|
|
||||||
)
|
|
||||||
|
|
||||||
# Mod assets
|
|
||||||
shutil.copy("./patches/resources/avatar.png", "./decompiled/assets/avatar.png")
|
|
||||||
shutil.copy(
|
|
||||||
"./patches/resources/OpenSans-Regular.ttf",
|
|
||||||
"./decompiled/assets/OpenSans-Regular.ttf",
|
|
||||||
)
|
|
||||||
shutil.copy(
|
|
||||||
"./patches/resources/ic_custom_crown.xml",
|
|
||||||
"./decompiled/res/drawable/ic_custom_crown.xml",
|
|
||||||
)
|
|
||||||
shutil.copy(
|
|
||||||
"./patches/resources/ic_custom_telegram.xml",
|
|
||||||
"./decompiled/res/drawable/ic_custom_telegram.xml",
|
|
||||||
)
|
|
||||||
shutil.copy(
|
|
||||||
"./patches/resources/ytsans_medium.ttf",
|
|
||||||
"./decompiled/res/font/ytsans_medium.ttf",
|
|
||||||
)
|
|
||||||
os.remove("./decompiled/res/font/ytsans_medium.otf")
|
|
||||||
|
|
||||||
# IDK
|
|
||||||
shutil.move(
|
|
||||||
"./decompiled/res/raw/bundled_cert.crt",
|
|
||||||
"./decompiled/res/raw/bundled_cert.cer",
|
|
||||||
)
|
|
||||||
shutil.move(
|
|
||||||
"./decompiled/res/raw/sdkinternalca.crt",
|
|
||||||
"./decompiled/res/raw/sdkinternalca.cer",
|
|
||||||
)
|
|
||||||
|
|
||||||
return True
|
|
||||||
+33
-15
@@ -1,44 +1,63 @@
|
|||||||
"""Change package name of apk"""
|
"""Изменяет имя пакета в apk, удаляет вход по google и vk
|
||||||
|
|
||||||
|
"package_name": {
|
||||||
|
"enabled": true,
|
||||||
|
"new_package_name": "com.wowlikon.anixart"
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
|
||||||
priority = -1
|
priority = -1
|
||||||
|
|
||||||
|
# imports
|
||||||
import os
|
import os
|
||||||
|
from tqdm import tqdm
|
||||||
from lxml import etree
|
from lxml import etree
|
||||||
|
from typing import Dict, Any
|
||||||
|
from pydantic import Field
|
||||||
|
|
||||||
|
from utils.config import PatchConfig
|
||||||
|
|
||||||
|
#Config
|
||||||
|
class Config(PatchConfig):
|
||||||
|
package_name: str = Field("com.wowlikon.anixart", description="Название пакета")
|
||||||
|
|
||||||
|
# Patch
|
||||||
def rename_dir(src, dst):
|
def rename_dir(src, dst):
|
||||||
os.makedirs(os.path.dirname(dst), exist_ok=True)
|
os.makedirs(os.path.dirname(dst), exist_ok=True)
|
||||||
os.rename(src, dst)
|
os.rename(src, dst)
|
||||||
|
|
||||||
|
|
||||||
def apply(config: dict) -> bool:
|
def apply(config: Config, base: Dict[str, Any]) -> bool:
|
||||||
assert config["new_package_name"] is not None, "new_package_name is not configured"
|
|
||||||
|
|
||||||
for root, dirs, files in os.walk("./decompiled"):
|
for root, dirs, files in os.walk("./decompiled"):
|
||||||
for filename in files:
|
for filename in files:
|
||||||
file_path = os.path.join(root, filename)
|
file_path = os.path.join(root, filename)
|
||||||
|
|
||||||
if os.path.isfile(file_path):
|
if os.path.isfile(file_path):
|
||||||
try:
|
try: # Изменяем имя пакета в файлах
|
||||||
with open(file_path, "r", encoding="utf-8") as file:
|
with open(file_path, "r", encoding="utf-8") as file:
|
||||||
file_contents = file.read()
|
file_contents = file.read()
|
||||||
|
|
||||||
new_contents = file_contents.replace(
|
new_contents = file_contents.replace(
|
||||||
"com.swiftsoft.anixartd", config["new_package_name"]
|
"com.swiftsoft.anixartd", config.package_name
|
||||||
)
|
)
|
||||||
new_contents = new_contents.replace(
|
new_contents = new_contents.replace(
|
||||||
"com/swiftsoft/anixartd",
|
"com/swiftsoft/anixartd",
|
||||||
config["new_package_name"].replace(".", "/"),
|
config.package_name.replace(".", "/"),
|
||||||
|
).replace(
|
||||||
|
"com/swiftsoft",
|
||||||
|
"/".join(config.package_name.split(".")[:2]),
|
||||||
)
|
)
|
||||||
with open(file_path, "w", encoding="utf-8") as file:
|
with open(file_path, "w", encoding="utf-8") as file:
|
||||||
file.write(new_contents)
|
file.write(new_contents)
|
||||||
except:
|
except:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
# Изменяем названия папок
|
||||||
if os.path.exists("./decompiled/smali/com/swiftsoft/anixartd"):
|
if os.path.exists("./decompiled/smali/com/swiftsoft/anixartd"):
|
||||||
rename_dir(
|
rename_dir(
|
||||||
"./decompiled/smali/com/swiftsoft/anixartd",
|
"./decompiled/smali/com/swiftsoft/anixartd",
|
||||||
os.path.join(
|
os.path.join(
|
||||||
"./decompiled", "smali", config["new_package_name"].replace(".", "/")
|
"./decompiled", "smali", config.package_name.replace(".", "/")
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
if os.path.exists("./decompiled/smali_classes2/com/swiftsoft/anixartd"):
|
if os.path.exists("./decompiled/smali_classes2/com/swiftsoft/anixartd"):
|
||||||
@@ -47,7 +66,7 @@ def apply(config: dict) -> bool:
|
|||||||
os.path.join(
|
os.path.join(
|
||||||
"./decompiled",
|
"./decompiled",
|
||||||
"smali_classes2",
|
"smali_classes2",
|
||||||
config["new_package_name"].replace(".", "/"),
|
config.package_name.replace(".", "/"),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
if os.path.exists("./decompiled/smali_classes4/com/swiftsoft"):
|
if os.path.exists("./decompiled/smali_classes4/com/swiftsoft"):
|
||||||
@@ -56,7 +75,7 @@ def apply(config: dict) -> bool:
|
|||||||
os.path.join(
|
os.path.join(
|
||||||
"./decompiled",
|
"./decompiled",
|
||||||
"smali_classes4",
|
"smali_classes4",
|
||||||
"/".join(config["new_package_name"].split(".")[:-1]),
|
"/".join(config.package_name.split(".")[:2]),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -69,6 +88,7 @@ def apply(config: dict) -> bool:
|
|||||||
# ),
|
# ),
|
||||||
# )
|
# )
|
||||||
|
|
||||||
|
# Замена названия пакета для smali_classes4
|
||||||
for root, dirs, files in os.walk("./decompiled/smali_classes4/"):
|
for root, dirs, files in os.walk("./decompiled/smali_classes4/"):
|
||||||
for filename in files:
|
for filename in files:
|
||||||
file_path = os.path.join(root, filename)
|
file_path = os.path.join(root, filename)
|
||||||
@@ -80,24 +100,22 @@ def apply(config: dict) -> bool:
|
|||||||
|
|
||||||
new_contents = file_contents.replace(
|
new_contents = file_contents.replace(
|
||||||
"com/swiftsoft",
|
"com/swiftsoft",
|
||||||
"/".join(config["new_package_name"].split(".")[:-1]),
|
"/".join(config.package_name.split(".")[:-1]),
|
||||||
)
|
)
|
||||||
with open(file_path, "w", encoding="utf-8") as file:
|
with open(file_path, "w", encoding="utf-8") as file:
|
||||||
file.write(new_contents)
|
file.write(new_contents)
|
||||||
except:
|
except:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
# Скрытие входа по Google и VK (НЕ РАБОТАЮТ В МОДАХ)
|
||||||
file_path = "./decompiled/res/layout/fragment_sign_in.xml"
|
file_path = "./decompiled/res/layout/fragment_sign_in.xml"
|
||||||
parser = etree.XMLParser(remove_blank_text=True)
|
parser = etree.XMLParser(remove_blank_text=True)
|
||||||
tree = etree.parse(file_path, parser)
|
tree = etree.parse(file_path, parser)
|
||||||
root = tree.getroot()
|
root = tree.getroot()
|
||||||
|
|
||||||
last_linear = root.xpath("//LinearLayout/LinearLayout[4]")[0]
|
last_linear = root.xpath("//LinearLayout/LinearLayout[4]")[0]
|
||||||
last_linear.set(f"{{{config['xml_ns']['android']}}}visibility", "gone")
|
last_linear.set(f"{{{base['xml_ns']['android']}}}visibility", "gone")
|
||||||
|
|
||||||
tree.write(file_path, pretty_print=True, xml_declaration=True, encoding="utf-8")
|
tree.write(file_path, pretty_print=True, xml_declaration=True, encoding="utf-8")
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
# smali_classes2/com/wowlikon/anixart/utils/DeviceInfoUtil.smali: const-string v3, "\u0411\u0430\u0433-\u0440\u0435\u043f\u043e\u0440\u0442 9.0 BETA 5 (25062213)"
|
|
||||||
|
|||||||
@@ -0,0 +1,62 @@
|
|||||||
|
"""
|
||||||
|
Меняет порядок вкладок в панели навигации
|
||||||
|
|
||||||
|
"replace_navbar": {
|
||||||
|
"enabled": true,
|
||||||
|
"items": ["home", "discover", "feed", "bookmarks", "profile"]
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
|
||||||
|
priority = 0
|
||||||
|
|
||||||
|
# imports
|
||||||
|
from lxml import etree
|
||||||
|
from tqdm import tqdm
|
||||||
|
from typing import Dict, List, Any
|
||||||
|
from pydantic import Field
|
||||||
|
|
||||||
|
from utils.config import PatchConfig
|
||||||
|
|
||||||
|
#Config
|
||||||
|
class Config(PatchConfig):
|
||||||
|
items: List[str] = Field(["home", "discover", "feed", "bookmarks", "profile"], description="Список элементов в панели навигации")
|
||||||
|
|
||||||
|
# Patch
|
||||||
|
def apply(config: Config, base: Dict[str, Any]) -> bool:
|
||||||
|
file_path = "./decompiled/res/menu/bottom.xml"
|
||||||
|
|
||||||
|
parser = etree.XMLParser(remove_blank_text=True)
|
||||||
|
tree = etree.parse(file_path, parser)
|
||||||
|
root = tree.getroot()
|
||||||
|
|
||||||
|
# Получение элементов панели навигации
|
||||||
|
items = root.findall("item", namespaces=base['xml_ns'])
|
||||||
|
|
||||||
|
def get_id_suffix(item):
|
||||||
|
full_id = item.get(f"{{{base['xml_ns']['android']}}}id")
|
||||||
|
return full_id.split("tab_")[-1] if full_id else None
|
||||||
|
|
||||||
|
items_by_id = {get_id_suffix(item): item for item in items}
|
||||||
|
existing_order = [get_id_suffix(item) for item in items]
|
||||||
|
|
||||||
|
# Размещение в новом порядке
|
||||||
|
ordered_items = []
|
||||||
|
for key in config.items:
|
||||||
|
if key in items_by_id:
|
||||||
|
ordered_items.append(items_by_id[key])
|
||||||
|
|
||||||
|
# Если есть не указанные в конфиге они помещаются в конец списка
|
||||||
|
extra = [i for i in items if get_id_suffix(i) not in config.items]
|
||||||
|
if extra:
|
||||||
|
tqdm.write("⚠Найдены лишние элементы: " + str([get_id_suffix(i) for i in extra]))
|
||||||
|
ordered_items.extend(extra)
|
||||||
|
|
||||||
|
for i in root.findall("item", namespaces=base['xml_ns']):
|
||||||
|
root.remove(i)
|
||||||
|
|
||||||
|
for item in ordered_items:
|
||||||
|
root.append(item)
|
||||||
|
|
||||||
|
tree.write(file_path, pretty_print=True, xml_declaration=True, encoding="utf-8")
|
||||||
|
|
||||||
|
return True
|
||||||
Binary file not shown.
@@ -0,0 +1,42 @@
|
|||||||
|
"""Делает текст в описании аниме копируемым
|
||||||
|
|
||||||
|
"selectable_text": {
|
||||||
|
"enabled": true
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
|
||||||
|
priority = 0
|
||||||
|
|
||||||
|
# imports
|
||||||
|
from tqdm import tqdm
|
||||||
|
from lxml import etree
|
||||||
|
from typing import Dict, Any
|
||||||
|
from pydantic import Field
|
||||||
|
|
||||||
|
from utils.config import PatchConfig
|
||||||
|
|
||||||
|
#Config
|
||||||
|
class Config(PatchConfig): ...
|
||||||
|
|
||||||
|
# Patch
|
||||||
|
def apply(config: Config, base: Dict[str, Any]) -> bool:
|
||||||
|
|
||||||
|
file_path = "./decompiled/res/layout/release_info.xml"
|
||||||
|
|
||||||
|
parser = etree.XMLParser(remove_blank_text=True)
|
||||||
|
tree = etree.parse(file_path, parser)
|
||||||
|
root = tree.getroot()
|
||||||
|
|
||||||
|
# Список тегов, к которым нужно добавить атрибут
|
||||||
|
tags = ["TextView", "at.blogc.android.views.ExpandableTextView"]
|
||||||
|
|
||||||
|
for tag in tags:
|
||||||
|
for element in root.findall(f".//{tag}", namespaces=base["xml_ns"]):
|
||||||
|
# Проверяем, нет ли уже атрибута
|
||||||
|
if f"{{{base['xml_ns']['android']}}}textIsSelectable" not in element.attrib:
|
||||||
|
element.set(f"{{{base['xml_ns']['android']}}}textIsSelectable", "true")
|
||||||
|
|
||||||
|
# Сохраняем
|
||||||
|
tree.write(file_path, encoding="utf-8", xml_declaration=True, pretty_print=True)
|
||||||
|
|
||||||
|
return True
|
||||||
+105
-8
@@ -1,9 +1,78 @@
|
|||||||
"""Add new settings"""
|
"""Добавляет в настройки ссылки и добвляет текст к версии приложения
|
||||||
|
|
||||||
|
"settings_urls": {
|
||||||
|
"enabled": true,
|
||||||
|
"menu": {
|
||||||
|
"Раздел": [
|
||||||
|
{
|
||||||
|
"title": "Заголовок",
|
||||||
|
"description": "Описание",
|
||||||
|
"url": "ссылка",
|
||||||
|
"icon": "@drawable/ic_custom_telegram",
|
||||||
|
"icon_space_reserved": "false"
|
||||||
|
},
|
||||||
|
...
|
||||||
|
],
|
||||||
|
...
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"version": " by wowlikon"
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
|
||||||
priority = 0
|
priority = 0
|
||||||
|
|
||||||
|
# imports
|
||||||
|
import shutil
|
||||||
from lxml import etree
|
from lxml import etree
|
||||||
|
from tqdm import tqdm
|
||||||
|
from typing import Dict, List, Any
|
||||||
|
from pydantic import Field
|
||||||
|
|
||||||
|
from utils.config import PatchConfig
|
||||||
|
from utils.public import insert_after_public
|
||||||
|
|
||||||
|
#Config
|
||||||
|
DEFAULT_MENU = {
|
||||||
|
"Мы в социальных сетях": [
|
||||||
|
{
|
||||||
|
"title": "wowlikon",
|
||||||
|
"description": "Разработчик",
|
||||||
|
"url": "https://t.me/wowlikon",
|
||||||
|
"icon": "@drawable/ic_custom_telegram",
|
||||||
|
"icon_space_reserved": "false"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Kentai Radiquum",
|
||||||
|
"description": "Разработчик",
|
||||||
|
"url": "https://t.me/radiquum",
|
||||||
|
"icon": "@drawable/ic_custom_telegram",
|
||||||
|
"icon_space_reserved": "false"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Мы в Telegram",
|
||||||
|
"description": "Подпишитесь на канал, чтобы быть в курсе последних новостей.",
|
||||||
|
"url": "https://t.me/http_teapod",
|
||||||
|
"icon": "@drawable/ic_custom_telegram",
|
||||||
|
"icon_space_reserved": "false"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"Прочее": [
|
||||||
|
{
|
||||||
|
"title": "Помочь проекту",
|
||||||
|
"description": "Вы можете помочь нам с идеями, написанием кода или тестированием.",
|
||||||
|
"url": "https://git.wowlikon.tech/anixart-mod",
|
||||||
|
"icon": "@drawable/ic_custom_crown",
|
||||||
|
"icon_space_reserved": "false"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
class Config(PatchConfig):
|
||||||
|
version: str = Field(" by wowlikon", description="Суффикс версии")
|
||||||
|
menu: Dict[str, List[Dict[str, str]]] = Field(DEFAULT_MENU, description="Меню")
|
||||||
|
|
||||||
|
# Patch
|
||||||
def make_category(ns, name, items):
|
def make_category(ns, name, items):
|
||||||
cat = etree.Element("PreferenceCategory", nsmap=ns)
|
cat = etree.Element("PreferenceCategory", nsmap=ns)
|
||||||
cat.set(f"{{{ns['android']}}}title", name)
|
cat.set(f"{{{ns['android']}}}title", name)
|
||||||
@@ -23,19 +92,47 @@ def make_category(ns, name, items):
|
|||||||
|
|
||||||
return cat
|
return cat
|
||||||
|
|
||||||
def apply(config: dict) -> bool:
|
def apply(config: Config, base: Dict[str, Any]) -> bool:
|
||||||
|
# Добавление кастомных иконок
|
||||||
|
shutil.copy(
|
||||||
|
"./resources/ic_custom_crown.xml",
|
||||||
|
"./decompiled/res/drawable/ic_custom_crown.xml",
|
||||||
|
)
|
||||||
|
insert_after_public("warning_error_counter_background", "ic_custom_crown")
|
||||||
|
|
||||||
|
shutil.copy(
|
||||||
|
"./resources/ic_custom_telegram.xml",
|
||||||
|
"./decompiled/res/drawable/ic_custom_telegram.xml",
|
||||||
|
)
|
||||||
|
insert_after_public("warning_error_counter_background", "ic_custom_telegram")
|
||||||
|
|
||||||
file_path = "./decompiled/res/xml/preference_main.xml"
|
file_path = "./decompiled/res/xml/preference_main.xml"
|
||||||
parser = etree.XMLParser(remove_blank_text=True)
|
parser = etree.XMLParser(remove_blank_text=True)
|
||||||
tree = etree.parse(file_path, parser)
|
tree = etree.parse(file_path, parser)
|
||||||
root = tree.getroot()
|
root = tree.getroot()
|
||||||
|
|
||||||
# Insert new PreferenceCategory before the last element
|
# Вставка новых пунктов перед последним
|
||||||
last = root[-1] # last element
|
pos = root.index(root[-1])
|
||||||
pos = root.index(last)
|
for section, items in config.menu.items():
|
||||||
for section, items in config["settings_urls"].items():
|
root.insert(pos, make_category(base["xml_ns"], section, items))
|
||||||
root.insert(pos, make_category(config["xml_ns"], section, items))
|
|
||||||
pos += 1
|
pos += 1
|
||||||
|
|
||||||
# Save back
|
# Сохранение
|
||||||
tree.write(file_path, pretty_print=True, xml_declaration=True, encoding="utf-8")
|
tree.write(file_path, pretty_print=True, xml_declaration=True, encoding="utf-8")
|
||||||
|
|
||||||
|
# Добавление суффикса версии
|
||||||
|
filepaths = [
|
||||||
|
"./decompiled/smali_classes2/com/swiftsoft/anixartd/ui/activity/UpdateActivity.smali",
|
||||||
|
"./decompiled/smali_classes2/com/swiftsoft/anixartd/ui/fragment/main/preference/MainPreferenceFragment.smali",
|
||||||
|
]
|
||||||
|
for filepath in filepaths:
|
||||||
|
content = ""
|
||||||
|
with open(filepath, "r", encoding="utf-8") as file:
|
||||||
|
for line in file.readlines():
|
||||||
|
if '"\u0412\u0435\u0440\u0441\u0438\u044f'.encode("unicode_escape").decode() in line:
|
||||||
|
content += line[:line.rindex('"')] + config.version + line[line.rindex('"'):]
|
||||||
|
else:
|
||||||
|
content += line
|
||||||
|
with open(filepath, "w", encoding="utf-8") as file:
|
||||||
|
file.write(content)
|
||||||
return True
|
return True
|
||||||
|
|||||||
@@ -0,0 +1,53 @@
|
|||||||
|
"""Изменяет формат "поделиться"
|
||||||
|
|
||||||
|
"selectable_text": {
|
||||||
|
"enabled": true,
|
||||||
|
"format": {
|
||||||
|
"share_channel_text": "Канал: «%1$s»\n%2$schannel/%3$d",
|
||||||
|
"share_collection_text": "Коллекция: «%1$s»\n%2$scollection/%3$d",
|
||||||
|
"share_profile_text": "Профиль пользователя «%1$s»\n%2$sprofile/%3$d",
|
||||||
|
"share_release_text": "Релиз: «%1$s»\n%2$srelease/%3$d"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
|
||||||
|
priority = 0
|
||||||
|
|
||||||
|
# imports
|
||||||
|
from tqdm import tqdm
|
||||||
|
from lxml import etree
|
||||||
|
from typing import Dict, Any
|
||||||
|
from pydantic import Field
|
||||||
|
|
||||||
|
from utils.config import PatchConfig
|
||||||
|
|
||||||
|
#Config
|
||||||
|
DEFAULT_FORMATS = {
|
||||||
|
"share_channel_text": "Канал: «%1$s»\n%2$schannel/%3$d",
|
||||||
|
"share_collection_text": "Коллекция: «%1$s»\n%2$scollection/%3$d",
|
||||||
|
"share_profile_text": "Профиль пользователя «%1$s»\n%2$sprofile/%3$d",
|
||||||
|
"share_release_text": "Релиз: «%1$s»\n%2$srelease/%3$d"
|
||||||
|
}
|
||||||
|
|
||||||
|
class Config(PatchConfig):
|
||||||
|
format: Dict[str, str] = Field(DEFAULT_FORMATS, description="Строки для замены в `strings.xml`")
|
||||||
|
|
||||||
|
# Patch
|
||||||
|
def apply(config: Config, base: Dict[str, Any]) -> bool:
|
||||||
|
|
||||||
|
file_path = "./decompiled/res/values/strings.xml"
|
||||||
|
|
||||||
|
parser = etree.XMLParser(remove_blank_text=True)
|
||||||
|
tree = etree.parse(file_path, parser)
|
||||||
|
root = tree.getroot()
|
||||||
|
|
||||||
|
# Обновляем значения
|
||||||
|
for string in root.findall("string"):
|
||||||
|
name = string.get("name")
|
||||||
|
if name in config.format:
|
||||||
|
string.text = config.format[name]
|
||||||
|
|
||||||
|
# Сохраняем обратно
|
||||||
|
tree.write(file_path, encoding="utf-8", xml_declaration=True, pretty_print=True)
|
||||||
|
|
||||||
|
return True
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
"""Change application icon"""
|
|
||||||
priority = 0
|
|
||||||
|
|
||||||
|
|
||||||
def apply(config: dict) -> bool:
|
|
||||||
return False
|
|
||||||
@@ -1,14 +1,38 @@
|
|||||||
"""Change application icon"""
|
"""Добавляет пользовательские скорости воспроизведения видео
|
||||||
|
|
||||||
|
"custom_speed": {
|
||||||
|
"enabled": true,
|
||||||
|
"speeds": [9.0]
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
|
||||||
priority = 0
|
priority = 0
|
||||||
|
|
||||||
import struct
|
# imports
|
||||||
|
from tqdm import tqdm
|
||||||
|
from typing import Dict, List, Any
|
||||||
|
from pydantic import Field
|
||||||
|
|
||||||
|
from utils.config import PatchConfig
|
||||||
|
from utils.smali_parser import float_to_hex
|
||||||
|
from utils.public import (
|
||||||
|
insert_after_public,
|
||||||
|
insert_after_id,
|
||||||
|
)
|
||||||
|
|
||||||
|
#Config
|
||||||
|
class Config(PatchConfig):
|
||||||
|
speeds: List[float] = Field([9.0], description="Список пользовательских скоростей воспроизведения")
|
||||||
|
|
||||||
|
|
||||||
def float_to_hex(f):
|
# Patch
|
||||||
b = struct.pack(">f", f)
|
def apply(config: Config, base: Dict[str, Any]) -> bool:
|
||||||
return b.hex()
|
|
||||||
|
|
||||||
|
|
||||||
def apply(config: dict) -> bool:
|
|
||||||
assert float_to_hex(1.5) == "0x3fc00000"
|
assert float_to_hex(1.5) == "0x3fc00000"
|
||||||
|
|
||||||
|
last = "speed75"
|
||||||
|
for speed in config.speeds:
|
||||||
|
insert_after_public(last, f"speed{int(float(speed)*10)}")
|
||||||
|
insert_after_id(last, f"speed{int(float(speed)*10)}")
|
||||||
|
last = f"speed{int(float(speed)*10)}"
|
||||||
|
|
||||||
return False
|
return False
|
||||||
|
|||||||
@@ -0,0 +1,48 @@
|
|||||||
|
"""Шаблон патча
|
||||||
|
|
||||||
|
Здесь вы можете добавить описание патча, его назначение и другие детали.
|
||||||
|
|
||||||
|
Каждый патч должен независеть от других патчей и проверять себя при применении. Он не должен вернуть True, если есть проблемы.
|
||||||
|
На данный момент каждый патч должен иметь функцию `apply`, которая принимает на вход конфигурацию и возвращает True или False.
|
||||||
|
И модель `Config`, которая наследуется от `PatchConfig` (поле `enabled` добавлять не нужно).
|
||||||
|
Это позволяет упростить конфигурирование и проверять типы данных, и делать значения по умолчанию.
|
||||||
|
При успешном применении патча, функция apply должна вернуть True, иначе False.
|
||||||
|
Ошибка будет интерпретирована как False. С выводом ошибки в консоль.
|
||||||
|
Ещё патч должен иметь переменную `priority`, которая указывает приоритет патча, чем выше, тем раньше он будет применен.
|
||||||
|
|
||||||
|
Коротко о конфигурации. Она состоит из двух частей `config` (на основе модели `Config` и из файла `configs/название_патча.json`).
|
||||||
|
И постоянной не типизированной переменной `base` из `config.json` и флага `verbose`.
|
||||||
|
```
|
||||||
|
python ./main.py build --verbose
|
||||||
|
```
|
||||||
|
|
||||||
|
В конце docstring может быть дополнительное описание конфигурации патча (основное описание получается из модели `Config`).
|
||||||
|
Это может быть как короткий фрагмент из названия патча и одной опции "enabled", которая обрабатывается в коде патчера.
|
||||||
|
|
||||||
|
"todo_template": {
|
||||||
|
"enabled": true, // Пример описания тк этот текст просто пример
|
||||||
|
"example": true // Пример кастомного параметра
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
|
||||||
|
priority = 0 # Приоритет патча, чем выше, тем раньше он будет применен
|
||||||
|
|
||||||
|
# imports
|
||||||
|
from tqdm import tqdm
|
||||||
|
from typing import Dict, List, Any
|
||||||
|
from pydantic import Field
|
||||||
|
|
||||||
|
from utils.config import PatchConfig
|
||||||
|
|
||||||
|
#Config
|
||||||
|
class Config(PatchConfig):
|
||||||
|
example: bool = Field(True, description="Пример кастомного параметра")
|
||||||
|
|
||||||
|
|
||||||
|
# Patch
|
||||||
|
def apply(config: Config, base: Dict[str, Any]) -> bool: # Анотации типов для удобства, читаемости и поддержки IDE
|
||||||
|
tqdm.write("Вывод информации через tqdm, чтобы не мешать прогресс-бару")
|
||||||
|
tqdm.write("Пример включен" if config.example else "Пример отключен")
|
||||||
|
if base["verbose"]:
|
||||||
|
tqdm.write("Для вывода подробной и отладочной информации используйте флаг --verbose")
|
||||||
|
return True
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
"""Добавляет всплывающее окно при первом входе
|
||||||
|
|
||||||
|
"welcome": {
|
||||||
|
"enabled": true,
|
||||||
|
"title": "Anixarty",
|
||||||
|
"description": "Описание",
|
||||||
|
"link_text": "МЫ В TELEGRAM",
|
||||||
|
"link_url": "https://t.me/http_teapod",
|
||||||
|
"skip_text": "Пропустить",
|
||||||
|
"title_bg_color": "#FFFFFF"
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
|
||||||
|
priority = 0
|
||||||
|
|
||||||
|
# imports
|
||||||
|
import os
|
||||||
|
import shutil
|
||||||
|
from pydantic import Field
|
||||||
|
from typing import Dict, Any
|
||||||
|
from utils.config import PatchConfig
|
||||||
|
from utils.smali_parser import (
|
||||||
|
find_and_replace_smali_line,
|
||||||
|
get_smali_lines,
|
||||||
|
save_smali_lines
|
||||||
|
)
|
||||||
|
|
||||||
|
#Config
|
||||||
|
class Config(PatchConfig):
|
||||||
|
title: str = Field("Anixarty", description="Заголовок")
|
||||||
|
description: str = Field("Описание", description="Описание")
|
||||||
|
link_text: str = Field("МЫ В TELEGRAM", description="Текст ссылки")
|
||||||
|
link_url: str = Field("https://t.me/http_teapod", description="Ссылка")
|
||||||
|
skip_text: str = Field("Пропустить", description="Текст кнопки пропуска")
|
||||||
|
title_bg_color: str = Field("#FFFFFF", description="Цвет фона заголовка")
|
||||||
|
|
||||||
|
|
||||||
|
# Patch
|
||||||
|
def apply(config: Config, base: Dict[str, Any]) -> bool:
|
||||||
|
# Добавление ресурсов окна первого входа
|
||||||
|
shutil.copy("./resources/avatar.png", "./decompiled/assets/avatar.png")
|
||||||
|
shutil.copy(
|
||||||
|
"./resources/OpenSans-Regular.ttf",
|
||||||
|
"./decompiled/assets/OpenSans-Regular.ttf",
|
||||||
|
)
|
||||||
|
shutil.copytree(
|
||||||
|
"./resources/smali_classes4/", "./decompiled/smali_classes4/"
|
||||||
|
)
|
||||||
|
|
||||||
|
file_path = "./decompiled/smali_classes2/com/swiftsoft/anixartd/ui/activity/MainActivity.smali"
|
||||||
|
method = "invoke-super {p0}, Lmoxy/MvpAppCompatActivity;->onResume()V"
|
||||||
|
lines = get_smali_lines(file_path)
|
||||||
|
lines = find_and_replace_smali_line(lines, method, method+"\ninvoke-static {p0}, Lcom/swiftsoft/about/$2;->oooooo(Landroid/content/Context;)Ljava/lang/Object;")
|
||||||
|
save_smali_lines(file_path, lines)
|
||||||
|
|
||||||
|
return True
|
||||||
@@ -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>
|
||||||
+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>
|
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
from pydantic import BaseModel, Field, ValidationError
|
||||||
|
from rich.console import Console
|
||||||
|
from typing import Dict, Any
|
||||||
|
from pathlib import Path
|
||||||
|
import typer
|
||||||
|
|
||||||
|
|
||||||
|
class ToolsConfig(BaseModel):
|
||||||
|
apktool_jar_url: str
|
||||||
|
apktool_wrapper_url: str
|
||||||
|
|
||||||
|
|
||||||
|
class Config(BaseModel):
|
||||||
|
tools: ToolsConfig
|
||||||
|
base: Dict[str, Any]
|
||||||
|
|
||||||
|
|
||||||
|
class PatchConfig(BaseModel):
|
||||||
|
enabled: bool = Field(True, description="Включить или отключить патч")
|
||||||
|
|
||||||
|
|
||||||
|
def load_config(console: Console) -> Config:
|
||||||
|
try:
|
||||||
|
return Config.model_validate_json(Path("config.json").read_text())
|
||||||
|
except FileNotFoundError:
|
||||||
|
console.print("[red]Файл config.json не найден")
|
||||||
|
raise typer.Exit(1)
|
||||||
|
except ValidationError as e:
|
||||||
|
console.print("[red]Ошибка валидации config.json:", e)
|
||||||
|
raise typer.Exit(1)
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
import struct
|
|
||||||
|
|
||||||
|
|
||||||
def float_to_hex(f):
|
|
||||||
b = struct.pack(">f", f)
|
|
||||||
return b.hex()
|
|
||||||
+13
-5
@@ -1,8 +1,9 @@
|
|||||||
from lxml import etree
|
from typing_extensions import Optional
|
||||||
from copy import deepcopy
|
from copy import deepcopy
|
||||||
|
from lxml import etree
|
||||||
|
|
||||||
|
|
||||||
def insert_after_public(anchor_name: str, elem_name: str):
|
def insert_after_public(anchor_name: str, elem_name: str) -> Optional[int]:
|
||||||
file_path = "./decompiled/res/values/public.xml"
|
file_path = "./decompiled/res/values/public.xml"
|
||||||
parser = etree.XMLParser(remove_blank_text=True)
|
parser = etree.XMLParser(remove_blank_text=True)
|
||||||
tree = etree.parse(file_path, parser)
|
tree = etree.parse(file_path, parser)
|
||||||
@@ -19,6 +20,8 @@ def insert_after_public(anchor_name: str, elem_name: str):
|
|||||||
anchor = (elem, attrs)
|
anchor = (elem, attrs)
|
||||||
types[attrs["type"]] = types.get(attrs["type"], []) + [int(attrs["id"], 16)]
|
types[attrs["type"]] = types.get(attrs["type"], []) + [int(attrs["id"], 16)]
|
||||||
|
|
||||||
|
assert anchor != None
|
||||||
|
|
||||||
free_ids = set()
|
free_ids = set()
|
||||||
group = types[anchor[1]["type"]]
|
group = types[anchor[1]["type"]]
|
||||||
for i in range(min(group), max(group) + 1):
|
for i in range(min(group), max(group) + 1):
|
||||||
@@ -47,7 +50,7 @@ def insert_after_public(anchor_name: str, elem_name: str):
|
|||||||
return new_id
|
return new_id
|
||||||
|
|
||||||
|
|
||||||
def insert_after_id(anchor_name: str, elem_name: str):
|
def insert_after_id(anchor_name: str, elem_name: str) -> None:
|
||||||
file_path = "./decompiled/res/values/ids.xml"
|
file_path = "./decompiled/res/values/ids.xml"
|
||||||
parser = etree.XMLParser(remove_blank_text=True)
|
parser = etree.XMLParser(remove_blank_text=True)
|
||||||
tree = etree.parse(file_path, parser)
|
tree = etree.parse(file_path, parser)
|
||||||
@@ -62,13 +65,15 @@ def insert_after_id(anchor_name: str, elem_name: str):
|
|||||||
assert anchor == None
|
assert anchor == None
|
||||||
anchor = (elem, attrs)
|
anchor = (elem, attrs)
|
||||||
|
|
||||||
|
assert anchor != None
|
||||||
|
|
||||||
new_elem = deepcopy(anchor[0])
|
new_elem = deepcopy(anchor[0])
|
||||||
new_elem.set("name", elem_name)
|
new_elem.set("name", elem_name)
|
||||||
anchor[0].addnext(new_elem)
|
anchor[0].addnext(new_elem)
|
||||||
tree.write(file_path, pretty_print=True, xml_declaration=True, encoding="utf-8")
|
tree.write(file_path, pretty_print=True, xml_declaration=True, encoding="utf-8")
|
||||||
|
|
||||||
|
|
||||||
def change_color(name: str, value: str):
|
def change_color(name: str, value: str) -> None:
|
||||||
file_path = "./decompiled/res/values/colors.xml"
|
file_path = "./decompiled/res/values/colors.xml"
|
||||||
parser = etree.XMLParser(remove_blank_text=True)
|
parser = etree.XMLParser(remove_blank_text=True)
|
||||||
tree = etree.parse(file_path, parser)
|
tree = etree.parse(file_path, parser)
|
||||||
@@ -86,7 +91,8 @@ def change_color(name: str, value: str):
|
|||||||
assert replacements >= 1
|
assert replacements >= 1
|
||||||
tree.write(file_path, pretty_print=True, xml_declaration=True, encoding="utf-8")
|
tree.write(file_path, pretty_print=True, xml_declaration=True, encoding="utf-8")
|
||||||
|
|
||||||
def insert_after_color(anchor_name: str, elem_name: str, elem_value: str):
|
|
||||||
|
def insert_after_color(anchor_name: str, elem_name: str, elem_value: str) -> None:
|
||||||
file_path = "./decompiled/res/values/colors.xml"
|
file_path = "./decompiled/res/values/colors.xml"
|
||||||
parser = etree.XMLParser(remove_blank_text=True)
|
parser = etree.XMLParser(remove_blank_text=True)
|
||||||
tree = etree.parse(file_path, parser)
|
tree = etree.parse(file_path, parser)
|
||||||
@@ -101,6 +107,8 @@ def insert_after_color(anchor_name: str, elem_name: str, elem_value: str):
|
|||||||
assert anchor == None
|
assert anchor == None
|
||||||
anchor = (elem, attrs)
|
anchor = (elem, attrs)
|
||||||
|
|
||||||
|
assert anchor != None
|
||||||
|
|
||||||
new_elem = deepcopy(anchor[0])
|
new_elem = deepcopy(anchor[0])
|
||||||
new_elem.set("name", elem_name)
|
new_elem.set("name", elem_name)
|
||||||
anchor[0].addnext(new_elem)
|
anchor[0].addnext(new_elem)
|
||||||
|
|||||||
+38
-19
@@ -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
|
||||||
@@ -29,30 +43,35 @@ def replace_smali_method_body(lines: list[str], start: int, end: int, new_lines:
|
|||||||
while index != (start + 1):
|
while index != (start + 1):
|
||||||
new_content.append(lines[index])
|
new_content.append(lines[index])
|
||||||
index += 1
|
index += 1
|
||||||
|
|
||||||
for line in new_lines:
|
for line in new_lines:
|
||||||
new_content.append(line)
|
new_content.append(line)
|
||||||
|
|
||||||
index += skip
|
index += skip
|
||||||
while index < len(lines):
|
while index < len(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__":
|
def find_and_replace_smali_line(
|
||||||
# lines = get_smali_lines("./decompiled/smali_classes2/com/radiquum/anixart/Prefs.smali")
|
lines: list[str], search: str, replace: str
|
||||||
|
) -> list[str]:
|
||||||
# for index, line in enumerate(lines):
|
for index, line in enumerate(lines):
|
||||||
# if line.find("IS_SPONSOR") >= 0:
|
if line.find(search) >= 0:
|
||||||
# method_start = find_smali_method_start(lines, index)
|
lines[index] = lines[index].replace(search, replace)
|
||||||
# method_end = find_smali_method_end(lines, index)
|
return lines
|
||||||
# new_content = replace_smali_method_body(lines, method_start, method_end, c)
|
|
||||||
|
|
||||||
# with open("./help/Prefs_orig.smali", "w", encoding="utf-8") as file:
|
def float_to_hex(f):
|
||||||
# file.writelines(lines)
|
b = struct.pack(">f", f)
|
||||||
# with open("./help/Prefs_modified.smali", "w", encoding="utf-8") as file:
|
return b.hex()
|
||||||
# file.writelines(new_content)
|
|
||||||
|
|
||||||
|
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,47 @@
|
|||||||
|
from plumbum import local, ProcessExecutionError
|
||||||
|
from rich.progress import Progress
|
||||||
|
from rich.console import Console
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import List
|
||||||
|
import httpx
|
||||||
|
import typer
|
||||||
|
|
||||||
|
|
||||||
|
TOOLS = Path("tools")
|
||||||
|
ORIGINAL = Path("original")
|
||||||
|
MODIFIED = Path("modified")
|
||||||
|
DECOMPILED = Path("decompiled")
|
||||||
|
PATCHES = Path("patches")
|
||||||
|
CONFIGS = Path("configs")
|
||||||
|
|
||||||
|
|
||||||
|
def ensure_dirs():
|
||||||
|
for d in [TOOLS, ORIGINAL, MODIFIED, DECOMPILED, PATCHES, CONFIGS]:
|
||||||
|
d.mkdir(exist_ok=True)
|
||||||
|
|
||||||
|
|
||||||
|
def run(console: Console, cmd: List[str], hide_output=True):
|
||||||
|
prog = local[cmd[0]][cmd[1:]]
|
||||||
|
try:
|
||||||
|
prog() if hide_output else prog & FG # type: ignore [reportUndefinedVariable]
|
||||||
|
except ProcessExecutionError as e:
|
||||||
|
console.print(f"[red]Ошибка при выполнении команды: {' '.join(cmd)}")
|
||||||
|
console.print(e.stderr)
|
||||||
|
raise typer.Exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
def download(console: Console, url: str, dest: Path):
|
||||||
|
console.print(f"[cyan]Скачивание {url} → {dest.name}")
|
||||||
|
|
||||||
|
with httpx.Client(follow_redirects=True, timeout=60.0) as client:
|
||||||
|
with client.stream("GET", url) as response:
|
||||||
|
response.raise_for_status()
|
||||||
|
total = int(response.headers.get("Content-Length", 0))
|
||||||
|
|
||||||
|
dest.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
with open(dest, "wb") as f, Progress(console=console) as progress:
|
||||||
|
task = progress.add_task("Загрузка", total=total if total else None)
|
||||||
|
for chunk in response.iter_bytes(chunk_size=8192):
|
||||||
|
f.write(chunk)
|
||||||
|
progress.update(task, advance=len(chunk))
|
||||||
Reference in New Issue
Block a user