Compare commits
32 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 85aef3d997 | |||
| 41399eca2c | |||
| 137c939e1d | |||
| 18ad769d33 | |||
| 5b39aec161 | |||
| c1bb2f8845 | |||
| 630ab0d094 | |||
| 0f9f6f2932 | |||
| a09181fe5a | |||
| 77694ec4b7 | |||
| b8ab508dfb | |||
| debf561cf9 | |||
| f7e186d5db | |||
| 24a8a1d4d3 | |||
| 550427338a | |||
| ac241e1189 | |||
| c22ef507ba | |||
| 9da9e98547 | |||
| 48953a857b | |||
|
871ec11f7e
|
|||
| 9453b3b50b | |||
| 48ea732d77 | |||
| 62e23a2eb0 | |||
| 5986d8b069 | |||
| cc49aad2aa | |||
| d6f616da7a | |||
| 3b2e5bee18 | |||
| 8a74245c9c | |||
| 0f53c836ae | |||
| d0744050d2 | |||
| 8f30061d44 | |||
| e2614990df |
@@ -0,0 +1,81 @@
|
||||
name: Build mod
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
tags:
|
||||
- 'v*'
|
||||
#schedule: # раз в 36 часов
|
||||
# - cron: "0 0 */3 * *" # каждые 3 дня в 00:00
|
||||
# - cron: "0 12 */3 * *" # каждые 3 дня в 12:00
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Download APK
|
||||
run: |
|
||||
curl -L -o app.apk "https://mirror-dl.anixart-app.com/anixart-beta.apk"
|
||||
|
||||
- name: Ensure aapt is installed
|
||||
run: |
|
||||
if ! command -v aapt &> /dev/null; then
|
||||
echo "aapt не найден, устанавливаем..."
|
||||
sudo apt-get update && sudo apt-get install -y --no-install-recommends android-sdk-build-tools
|
||||
fi
|
||||
|
||||
- name: Ensure pngquant is installed
|
||||
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: Export secrets
|
||||
env:
|
||||
KEYSTORE: ${{ secrets.KEYSTORE }}
|
||||
KEYSTORE_PASS: ${{ secrets.KEYSTORE_PASS }}
|
||||
run: |
|
||||
# Export so later steps can reference them
|
||||
echo "$KEYSTORE" | base64 -d > keystore.jks
|
||||
echo "$KEYSTORE_PASS" > keystore.pass
|
||||
|
||||
- name: Prepare to build APK
|
||||
id: build
|
||||
run: |
|
||||
mkdir original
|
||||
mv app.apk original/
|
||||
pip install -r ./requirements.txt --break-system-packages
|
||||
python ./main.py init
|
||||
|
||||
- name: Build APK
|
||||
id: build
|
||||
run: |
|
||||
python ./main.py build -f
|
||||
|
||||
- name: Read title from report.log
|
||||
id: get_title
|
||||
run: |
|
||||
TITLE=$(head -n 1 modified/report.log)
|
||||
echo "title=${TITLE}" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Setup go
|
||||
if: steps.build.outputs.BUILD_EXIT == '0'
|
||||
uses: actions/setup-go@v4
|
||||
with:
|
||||
go-version: '>=1.20'
|
||||
|
||||
- name: Make release
|
||||
if: steps.build.outputs.BUILD_EXIT == '0'
|
||||
uses: https://gitea.com/actions/release-action@main
|
||||
with:
|
||||
title: ${{ steps.get_title.outputs.title }}
|
||||
body_path: modified/report.log
|
||||
draft: true
|
||||
api_key: '${{secrets.RELEASE_TOKEN}}'
|
||||
files: |-
|
||||
modified/**-mod.apk
|
||||
modified/report.log
|
||||
Vendored
+2
@@ -5,3 +5,5 @@ tools
|
||||
|
||||
__pycache__
|
||||
.venv
|
||||
*.jks
|
||||
*.pass
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
|
||||
---
|
||||
|
||||
|
||||
### Структура проекта:
|
||||
- `main.py` Главный файл
|
||||
- `patches` Модули патчей
|
||||
@@ -13,6 +14,37 @@
|
||||
- `patches/resources` Ресурсы, используемые патчами
|
||||
- `todo_drafts` Заметки для новых патчей(можно в любом формате)
|
||||
|
||||
### Схема
|
||||
|
||||
```mermaid
|
||||
---
|
||||
title: Процесс модифицирования приложения
|
||||
---
|
||||
|
||||
flowchart TD
|
||||
A([Оригинальный apk]) f1@==> B[поиск и выбор apk]
|
||||
|
||||
B f2@==> p[Декомпиляция]
|
||||
|
||||
subgraph p["Применение патчей по возрастанию приоритета"]
|
||||
C[Патч 1] --> D
|
||||
D[Патч 2] --...--> E[Патч n]
|
||||
end
|
||||
|
||||
p f3@==> F[Сборка apk обратно]
|
||||
F f4@==> G[Выравнивание zipalign]
|
||||
G f5@==> H[Подпись V2+V3]
|
||||
|
||||
H f6@==> I([Модифицированый apk])
|
||||
|
||||
f1@{ animate: true }
|
||||
f2@{ animate: true }
|
||||
f3@{ animate: true }
|
||||
f4@{ animate: true }
|
||||
f5@{ animate: true }
|
||||
f6@{ animate: true }
|
||||
```
|
||||
|
||||
### Установка и использование:
|
||||
|
||||
1. Клонируйте репозиторий:
|
||||
@@ -26,33 +58,24 @@
|
||||
- apksigner
|
||||
- pngquant
|
||||
|
||||
Все остальные инструменты и зависимости будут автоматически установлены при запуске `main.py`.
|
||||
Все остальные инструменты и зависимости будут автоматически установлены при запуске `main.py init`.
|
||||
|
||||
2. Создайте keystore с помощью `keytool` (требуется только один раз):
|
||||
```sh
|
||||
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. Поместите оригинальный apk файла anixart в папку `original`
|
||||
4. Запустите `main.py` и выберите файл apk
|
||||
|
||||
## ПОКА ЕЩЁ В РАЗРАБОТКЕ И ПОЭТОМУ НЕ В СКРИПТЕ
|
||||
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. Установите приложение на ваше устройство.
|
||||
3. Измените настройки мода в файле `patches/config.json`. Если вы развернули свой [сервер](https://git.wowlikon.tech/anixart-mod/server), то измените `"server": "https://new.url"`
|
||||
4. Поместите оригинальный apk файла anixart в папку `original`
|
||||
5. Запустите `main.py build` и выберите файл apk
|
||||
6. Установите приложение на ваше устройство.
|
||||
|
||||
|
||||
## Лицензия:
|
||||
Этот проект лицензирован под лицензией MIT. См. [LICENSE](./LICENSE) для получения подробной информации.
|
||||
|
||||
### Вклад в проект:
|
||||
- Seele - Все оригинальные патчи основаны на модификации приложения от Seele [[GitHub](https://github.com/seeleme) | [Telegram](https://t.me/seele_off)]
|
||||
- Kentai Radiquum - Разработка неофициального сайта и помощь с изучением API [[GitHub](https://github.com/Radiquum) | [Telegram](https://t.me/radiquum)]
|
||||
- Seele - Оригинальные патчи в начале разработки основаны на модификации от Seele [[GitHub](https://github.com/seeleme) | [Telegram](https://t.me/seele_off)]
|
||||
- Kentai Radiquum - Значительный вклад в развитие патчера, разработка [anix](https://github.com/AniX-org/AniX) и помощь с API [[GitHub](https://github.com/Radiquum) | [Telegram](https://t.me/radiquum)]
|
||||
- ReCode Liner - Помощь в модификации приложения [[Telegram](https://t.me/recodius)]
|
||||
|
||||
+101
@@ -0,0 +1,101 @@
|
||||
{
|
||||
"base": {
|
||||
"tools": {
|
||||
"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"
|
||||
},
|
||||
"xml_ns": {
|
||||
"android": "http://schemas.android.com/apk/res/android",
|
||||
"app": "http://schemas.android.com/apk/res-auto"
|
||||
}
|
||||
},
|
||||
"patches": {
|
||||
"package_name": {
|
||||
"enabled": true,
|
||||
"new_package_name": "com.wowlikon.anixart"
|
||||
},
|
||||
"compress": {
|
||||
"enabled": true,
|
||||
"remove_language_files": true,
|
||||
"remove_AI_voiceover": true,
|
||||
"remove_debug_lines": true,
|
||||
"remove_drawable_files": false,
|
||||
"remove_unknown_files": true,
|
||||
"remove_unknown_files_keep_dirs": ["META-INF", "kotlin"],
|
||||
"compress_png_files": true
|
||||
},
|
||||
"change_server": {
|
||||
"enabled": false,
|
||||
"server": "https://anixarty.wowlikon.tech/modding"
|
||||
},
|
||||
"color_theme": {
|
||||
"enabled": true,
|
||||
"colors": {
|
||||
"primary": "#ccff00",
|
||||
"secondary": "#ffffd700",
|
||||
"background": "#ffffff",
|
||||
"text": "#000000"
|
||||
},
|
||||
"gradient": {
|
||||
"angle": "135.0",
|
||||
"from": "#ffff6060",
|
||||
"to": "#ffccff00"
|
||||
}
|
||||
},
|
||||
"replace_navbar": {
|
||||
"enabled": true,
|
||||
"items": ["home", "discover", "feed", "bookmarks", "profile"]
|
||||
},
|
||||
"custom_speed": {
|
||||
"enabled": true,
|
||||
"speeds": [0.0]
|
||||
},
|
||||
"disable_ad": {
|
||||
"enabled": true
|
||||
},
|
||||
"disable_beta_banner": {
|
||||
"enabled": true
|
||||
},
|
||||
"insert_new": {
|
||||
"enabled": true
|
||||
},
|
||||
"settings_urls": {
|
||||
"enabled": true,
|
||||
"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://t.me/wowlikon",
|
||||
"icon": "@drawable/ic_custom_crown",
|
||||
"icon_space_reserved": "false"
|
||||
}
|
||||
]
|
||||
},
|
||||
"version": " by wowlikon"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,152 +1,286 @@
|
||||
import os
|
||||
import sys
|
||||
import json
|
||||
import requests
|
||||
import colorama
|
||||
from pathlib import Path
|
||||
from typing import List
|
||||
|
||||
import httpx
|
||||
import typer
|
||||
import importlib
|
||||
import subprocess
|
||||
from tqdm import tqdm
|
||||
import traceback
|
||||
import yaml
|
||||
|
||||
def init() -> dict:
|
||||
for directory in ["original", "modified", "patches", "tools", "decompiled"]:
|
||||
if not os.path.exists(directory):
|
||||
os.makedirs(directory)
|
||||
from pydantic import BaseModel, ValidationError
|
||||
from plumbum import local, ProcessExecutionError
|
||||
from rich.console import Console
|
||||
from rich.progress import Progress
|
||||
from rich.prompt import Prompt
|
||||
from rich.table import Table
|
||||
|
||||
with open("./patches/config.json", "r") as config_file:
|
||||
conf = json.load(config_file)
|
||||
# --- Paths ---
|
||||
TOOLS = Path("tools")
|
||||
ORIGINAL = Path("original")
|
||||
MODIFIED = Path("modified")
|
||||
DECOMPILED = Path("decompiled")
|
||||
PATCHES = Path("patches")
|
||||
|
||||
if not os.path.exists("./tools/apktool.jar"):
|
||||
console = Console()
|
||||
app = typer.Typer()
|
||||
|
||||
|
||||
# ======================= CONFIG =========================
|
||||
class ToolsConfig(BaseModel):
|
||||
apktool_jar_url: str
|
||||
apktool_wrapper_url: str
|
||||
|
||||
|
||||
class XmlNamespaces(BaseModel):
|
||||
android: str
|
||||
app: str
|
||||
|
||||
|
||||
class BaseSection(BaseModel):
|
||||
tools: ToolsConfig
|
||||
xml_ns: XmlNamespaces
|
||||
|
||||
|
||||
class Config(BaseModel):
|
||||
base: BaseSection
|
||||
patches: dict
|
||||
|
||||
|
||||
def load_config() -> Config:
|
||||
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):
|
||||
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)
|
||||
|
||||
|
||||
# ======================= UTILS =========================
|
||||
def ensure_dirs():
|
||||
for d in [TOOLS, ORIGINAL, MODIFIED, DECOMPILED, PATCHES]:
|
||||
d.mkdir(exist_ok=True)
|
||||
|
||||
|
||||
def run(cmd: List[str], hide_output=True):
|
||||
prog = local[cmd[0]][cmd[1:]]
|
||||
try:
|
||||
prog() if hide_output else prog & FG
|
||||
except ProcessExecutionError as e:
|
||||
console.print(f"[red]Ошибка при выполнении команды: {' '.join(cmd)}")
|
||||
console.print(e.stderr)
|
||||
raise typer.Exit(1)
|
||||
|
||||
|
||||
def download(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))
|
||||
|
||||
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)
|
||||
# ======================= INIT =========================
|
||||
@app.command()
|
||||
def init():
|
||||
"""Создание директорий и скачивание инструментов"""
|
||||
ensure_dirs()
|
||||
conf = load_config()
|
||||
|
||||
if not (TOOLS / "apktool.jar").exists():
|
||||
download(conf.base.tools.apktool_jar_url, TOOLS / "apktool.jar")
|
||||
|
||||
if not (TOOLS / "apktool").exists():
|
||||
download(conf.base.tools.apktool_wrapper_url, TOOLS / "apktool")
|
||||
(TOOLS / "apktool").chmod(0o755)
|
||||
|
||||
try:
|
||||
result = subprocess.run(
|
||||
["java", "-version"], capture_output=True, text=True, check=True
|
||||
)
|
||||
local["java"]["-version"]()
|
||||
console.print("[green]Java найдена")
|
||||
except ProcessExecutionError:
|
||||
console.print("[red]Java не установлена")
|
||||
raise typer.Exit(1)
|
||||
|
||||
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 или более поздняя версия установлена.")
|
||||
|
||||
# ======================= INFO =========================
|
||||
@app.command()
|
||||
def info(patch_name: str = ""):
|
||||
"""Вывод информации о патче"""
|
||||
conf = load_config().model_dump()
|
||||
if patch_name:
|
||||
patch = Patch(patch_name, __import__(f"patcher.patches.{patch_name}"))
|
||||
console.print(f"[green]Информация о патче {patch.name}:")
|
||||
console.print(f" [yellow]Приоритет: {patch.priority}")
|
||||
console.print(f" [yellow]Описание: {patch.module.__doc__}")
|
||||
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)
|
||||
|
||||
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("Неверный номер файла")
|
||||
console.print("[cyan]Список патчей:")
|
||||
for f in PATCHES.glob("*.py"):
|
||||
if f.name.startswith("todo_") or f.name == "__init__.py":
|
||||
continue
|
||||
name = f.stem
|
||||
if conf['patches'].get(name,{}).get('enabled',True):
|
||||
console.print(f" [yellow]{name}: [green]✔ enabled")
|
||||
else:
|
||||
apk = apks[selected_index - 1]
|
||||
print(f"Выбран файл {apk}")
|
||||
return apk
|
||||
except ValueError:
|
||||
print("Неверный формат ввода")
|
||||
except KeyboardInterrupt:
|
||||
print("Прервано пользователем")
|
||||
sys.exit(0)
|
||||
|
||||
|
||||
def decompile_apk(apk: str):
|
||||
print("Декомпилируем apk...")
|
||||
try:
|
||||
result = subprocess.run(
|
||||
"tools/apktool d -f -o decompiled " + os.path.join("original", apk),
|
||||
shell=True,
|
||||
check=True,
|
||||
text=True,
|
||||
stdout=subprocess.DEVNULL,
|
||||
stderr=subprocess.PIPE,
|
||||
)
|
||||
except subprocess.CalledProcessError as e:
|
||||
print("Ошибка при выполнении команды:")
|
||||
print(e.stderr)
|
||||
sys.exit(1)
|
||||
console.print(f" [yellow]{name}: [red]✘ disabled")
|
||||
|
||||
|
||||
# ======================= PATCHING =========================
|
||||
class Patch:
|
||||
def __init__(self, name, pkg):
|
||||
def __init__(self, name: str, module):
|
||||
self.name = name
|
||||
self.package = pkg
|
||||
self.module = module
|
||||
self.applied = False
|
||||
try:
|
||||
self.priority = pkg.priority
|
||||
except AttributeError:
|
||||
self.priority = 0
|
||||
self.priority = getattr(module, "priority", 0)
|
||||
|
||||
def apply(self, conf: dict) -> bool:
|
||||
try:
|
||||
self.applied = self.package.apply(conf)
|
||||
return True
|
||||
self.applied = bool(self.module.apply(conf))
|
||||
return self.applied
|
||||
except Exception as e:
|
||||
print(f"Ошибка при применении патча {self.name}: {e}")
|
||||
print(e.args)
|
||||
console.print(f"[red]Ошибка в патче {self.name}: {e}")
|
||||
traceback.print_exc()
|
||||
return False
|
||||
|
||||
|
||||
conf = init()
|
||||
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(
|
||||
[
|
||||
"java",
|
||||
"-jar",
|
||||
str(TOOLS / "apktool.jar"),
|
||||
"d",
|
||||
"-f",
|
||||
"-o",
|
||||
str(DECOMPILED),
|
||||
str(apk),
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
def compile(apk: Path, patches: List[Patch]):
|
||||
console.print("[yellow]Сборка apk...")
|
||||
out_apk = MODIFIED / apk.name
|
||||
aligned = out_apk.with_stem(out_apk.stem + "-aligned")
|
||||
signed = out_apk.with_stem(out_apk.stem + "-mod")
|
||||
|
||||
run(
|
||||
[
|
||||
"java",
|
||||
"-jar",
|
||||
str(TOOLS / "apktool.jar"),
|
||||
"b",
|
||||
str(DECOMPILED),
|
||||
"-o",
|
||||
str(out_apk),
|
||||
]
|
||||
)
|
||||
run(["zipalign", "-v", "4", str(out_apk), str(aligned)])
|
||||
run(
|
||||
[
|
||||
"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),
|
||||
]
|
||||
)
|
||||
|
||||
with open(DECOMPILED / "apktool.yml", encoding="utf-8") as f:
|
||||
meta = yaml.safe_load(f)
|
||||
version_str = " ".join(
|
||||
f"{k}:{v}" for k, v in meta.get("versionInfo", {}).items()
|
||||
)
|
||||
|
||||
with open(MODIFIED / "report.log", "w", encoding="utf-8") as f:
|
||||
f.write(f"anixart mod {version_str}\n")
|
||||
for p in patches:
|
||||
f.write(f"{p.name}: {'applied' if p.applied else 'failed'}\n")
|
||||
|
||||
|
||||
@app.command()
|
||||
def build(
|
||||
force: bool = typer.Option(False, "--force", "-f", help="Принудительная сборка"),
|
||||
verbose: bool = typer.Option(False, "--verbose", "-v", help="Подробный вывод"),
|
||||
):
|
||||
"""Декомпиляция, патчи и сборка apk"""
|
||||
conf = load_config().model_dump()
|
||||
apk = select_apk()
|
||||
patch = decompile_apk(apk)
|
||||
decompile(apk)
|
||||
|
||||
patches = []
|
||||
for filename in os.listdir("patches/"):
|
||||
if filename.endswith(".py") and filename != "__init__.py":
|
||||
module_name = filename[:-3]
|
||||
module = importlib.import_module(f"patches.{module_name}")
|
||||
patches.append(Patch(module_name, module))
|
||||
patch_settings = conf.get("patches", {})
|
||||
patch_objs: List[Patch] = []
|
||||
|
||||
patches.sort(key=lambda x: x.package.priority, reverse=True)
|
||||
for f in PATCHES.glob("*.py"):
|
||||
if f.name.startswith("todo_") or f.name == "__init__.py":
|
||||
continue
|
||||
name = f.stem
|
||||
settings = patch_settings.get(name, {})
|
||||
if not settings.get("enabled", True):
|
||||
console.print(f"[yellow]≫ Пропускаем {name}")
|
||||
continue
|
||||
module = importlib.import_module(f"patches.{name}")
|
||||
patch_objs.append(Patch(name, module))
|
||||
|
||||
for patch in tqdm(patches, colour="green", desc="Применение патчей"):
|
||||
tqdm.write(f"Применение патча: {patch.name}")
|
||||
patch.apply(conf)
|
||||
patch_objs.sort(key=lambda p: p.priority, reverse=True)
|
||||
|
||||
statuses = {}
|
||||
for patch in patches:
|
||||
statuses[patch.name] = patch.applied
|
||||
marker = colorama.Fore.GREEN + "✔" if patch.applied else colorama.Fore.RED + "✘"
|
||||
print(f"{marker}{colorama.Style.RESET_ALL} {patch.name}")
|
||||
console.print("[cyan]Применение патчей")
|
||||
with Progress() as progress:
|
||||
task = progress.add_task("Патчи", total=len(patch_objs))
|
||||
for p in patch_objs:
|
||||
ok = p.apply(patch_settings.get(p.name, {}) | conf.get("base", {}))
|
||||
progress.console.print(f"{'✔' if ok else '✘'} {p.name}")
|
||||
progress.advance(task)
|
||||
|
||||
if all(statuses.values()):
|
||||
print("Все патчи успешно применены")
|
||||
elif any(statuses.values()):
|
||||
print("Некоторые патчи не были успешно применены")
|
||||
successes = sum(p.applied for p in patch_objs)
|
||||
if successes == len(patch_objs):
|
||||
compile(apk, patch_objs)
|
||||
elif successes > 0 and (
|
||||
force or Prompt.ask("Продолжить сборку?", choices=["y", "n"]) == "y"
|
||||
):
|
||||
compile(apk, patch_objs)
|
||||
else:
|
||||
print("Ни один патч не был успешно применен")
|
||||
console.print("[red]Сборка отменена")
|
||||
raise typer.Exit(1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
app()
|
||||
|
||||
@@ -1,15 +1,46 @@
|
||||
"""Change api server"""
|
||||
"""
|
||||
Заменяет сервер api
|
||||
|
||||
"change_server": {
|
||||
"server": "https://anixarty.wowlikon.tech/modding"
|
||||
}
|
||||
"""
|
||||
priority = 0
|
||||
from tqdm import tqdm
|
||||
|
||||
import json
|
||||
import requests
|
||||
from tqdm import tqdm
|
||||
|
||||
|
||||
def apply(config: dict) -> bool:
|
||||
response = requests.get(config['server'])
|
||||
if response.status_code == 200:
|
||||
for item in json.loads(response.text)["modding"]:
|
||||
tqdm.write(item)
|
||||
assert response.status_code == 200, f"Failed to fetch data {response.status_code} {response.text}"
|
||||
new_api = json.loads(response.text)
|
||||
for item in new_api['modifications']:
|
||||
tqdm.write(f"Изменение {item['file']}")
|
||||
filepath = './decompiled/smali_classes2/com/swiftsoft/anixartd/network/api/'+item['file']
|
||||
with open(filepath, 'r') as f:
|
||||
content = f.read()
|
||||
with open(filepath, 'w') as f:
|
||||
if content.count(item['src']) == 0:
|
||||
tqdm.write(f"⚠ Не найдено {item['src']}")
|
||||
f.write(content.replace(item['src'], item['dst']))
|
||||
|
||||
tqdm.write(f"Изменение Github ссылки")
|
||||
filepath = './decompiled/smali_classes2/com/swiftsoft/anixartd/utils/anixnet/GithubPagesNetFetcher.smali'
|
||||
with open(filepath, 'r') as f:
|
||||
content = f.read()
|
||||
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"]}"'))
|
||||
|
||||
content = ""
|
||||
tqdm.write("Удаление динамического выбора сервера")
|
||||
filepath = './decompiled/smali_classes2/com/swiftsoft/anixartd/DaggerApp_HiltComponents_SingletonC$SingletonCImpl$SwitchingProvider.smali'
|
||||
with open(filepath, 'r') as f:
|
||||
for line in f.readlines():
|
||||
if "addInterceptor" in line: continue
|
||||
content += line
|
||||
with open(filepath, 'w') as f:
|
||||
f.write(content)
|
||||
|
||||
return True
|
||||
tqdm.write(f"Failed to fetch data {response.status_code} {response.text}")
|
||||
return False
|
||||
|
||||
@@ -1,19 +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)
|
||||
tqdm.write(f'Удалён файл: {item_path}')
|
||||
elif os.path.isdir(item_path):
|
||||
if item not in config["cleanup"]["keep_dirs"]:
|
||||
shutil.rmtree(item_path)
|
||||
tqdm.write(f'Удалена папка: {item_path}')
|
||||
return True
|
||||
+67
-7
@@ -1,14 +1,37 @@
|
||||
"""Change application theme"""
|
||||
"""
|
||||
Изменяет цветовую тему приложения и иконку
|
||||
|
||||
"color_theme": {
|
||||
"colors": {
|
||||
"primary": "#ccff00",
|
||||
"secondary": "#ffffd700",
|
||||
"background": "#ffffff",
|
||||
"text": "#000000"
|
||||
},
|
||||
"gradient": {
|
||||
"angle": "135.0",
|
||||
"from": "#ffff6060",
|
||||
"to": "#ffccff00"
|
||||
}
|
||||
}
|
||||
"""
|
||||
priority = 0
|
||||
from tqdm import tqdm
|
||||
|
||||
from lxml import etree
|
||||
|
||||
from utils.public import (
|
||||
insert_after_public,
|
||||
insert_after_color,
|
||||
change_color,
|
||||
)
|
||||
|
||||
|
||||
def apply(config: dict) -> bool:
|
||||
main_color = config["theme"]["colors"]["primary"]
|
||||
splash_color = config["theme"]["colors"]["secondary"]
|
||||
gradient_from = config["theme"]["gradient"]["from"]
|
||||
gradient_to = config["theme"]["gradient"]["to"]
|
||||
main_color = config["colors"]["primary"]
|
||||
splash_color = config["colors"]["secondary"]
|
||||
gradient_angle = config["gradient"]["angle"]
|
||||
gradient_from = config["gradient"]["from"]
|
||||
gradient_to = config["gradient"]["to"]
|
||||
|
||||
# No connection alert coolor
|
||||
with open("./decompiled/assets/no_connection.html", "r", encoding="utf-8") as file:
|
||||
@@ -31,6 +54,7 @@ def apply(config: dict) -> bool:
|
||||
root = tree.getroot()
|
||||
|
||||
# Change attributes with namespace
|
||||
root.set(f"{{{config['xml_ns']['android']}}}angle", gradient_angle)
|
||||
root.set(f"{{{config['xml_ns']['android']}}}startColor", gradient_from)
|
||||
root.set(f"{{{config['xml_ns']['android']}}}endColor", gradient_to)
|
||||
|
||||
@@ -45,7 +69,7 @@ def apply(config: dict) -> bool:
|
||||
root = tree.getroot()
|
||||
|
||||
# Finding "path"
|
||||
for el in root.findall("path", namespaces=config['xml_ns']):
|
||||
for el in root.findall("path", namespaces=config["xml_ns"]):
|
||||
name = el.get(f"{{{config['xml_ns']['android']}}}name")
|
||||
if name == "path":
|
||||
el.set(f"{{{config['xml_ns']['android']}}}fillColor", splash_color)
|
||||
@@ -53,4 +77,40 @@ def apply(config: dict) -> bool:
|
||||
# Save back
|
||||
tree.write(file_path, pretty_print=True, xml_declaration=True, encoding="utf-8")
|
||||
|
||||
for filename in ["$ic_launcher_foreground__0", "$ic_banner_foreground__0"]:
|
||||
file_path = f"./decompiled/res/drawable-v24/{filename}.xml"
|
||||
|
||||
parser = etree.XMLParser(remove_blank_text=True)
|
||||
tree = etree.parse(file_path, parser)
|
||||
root = tree.getroot()
|
||||
|
||||
# Change attributes with namespace
|
||||
root.set(f"{{{config['xml_ns']['android']}}}angle", gradient_angle)
|
||||
items = root.findall("item", namespaces=config['xml_ns'])
|
||||
assert len(items) == 2
|
||||
items[0].set(f"{{{config['xml_ns']['android']}}}color", gradient_from)
|
||||
items[1].set(f"{{{config['xml_ns']['android']}}}color", gradient_to)
|
||||
|
||||
# Save back
|
||||
tree.write(file_path, pretty_print=True, xml_declaration=True, encoding="utf-8")
|
||||
|
||||
insert_after_public("carmine", "custom_color")
|
||||
insert_after_public("carmine_alpha_10", "custom_color_alpha_10")
|
||||
insert_after_color("carmine", "custom_color", main_color[0]+'ff'+main_color[1:])
|
||||
insert_after_color("carmine_alpha_10", "custom_color_alpha_10", main_color[0]+'1a'+main_color[1:])
|
||||
|
||||
change_color("accent_alpha_10", main_color[0]+'1a'+main_color[1:])
|
||||
change_color("accent_alpha_20", main_color[0]+'33'+main_color[1:])
|
||||
change_color("accent_alpha_50", main_color[0]+'80'+main_color[1:])
|
||||
change_color("accent_alpha_70", main_color[0]+'b3'+main_color[1:])
|
||||
change_color("colorAccent", main_color[0]+'ff'+main_color[1:])
|
||||
change_color("link_color", main_color[0]+'ff'+main_color[1:])
|
||||
change_color("link_color_alpha_70", main_color[0]+'b3'+main_color[1:])
|
||||
change_color("refresh_progress", main_color[0]+'ff'+main_color[1:])
|
||||
|
||||
change_color("ic_launcher_background", "#ff000000")
|
||||
change_color("bottom_nav_indicator_active", "#ffffffff")
|
||||
change_color("bottom_nav_indicator_icon_checked", main_color[0]+'ff'+main_color[1:])
|
||||
change_color("bottom_nav_indicator_label_checked", main_color[0]+'ff'+main_color[1:])
|
||||
|
||||
return True
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
# Compress
|
||||
|
||||
Патч удаляет ненужные ресурсы что-бы уменьшить размер АПК
|
||||
|
||||
## настройки (compress в config.json)
|
||||
|
||||
- remove_unknown_files: true/false - удаляет файлы из директории decompiled/unknown
|
||||
- remove_unknown_files_keep_dirs: list[str] - оставляет указанные директории в decompiled/unknown
|
||||
- remove_debug_lines: true/false - удаляет строки `.line n` из декомпилированных smali файлов использованные для дебага
|
||||
- remove_AI_voiceover: true/false - заменяет ИИ озвучку маскота аниксы пустыми mp3 файлами
|
||||
- compress_png_files: true/false - сжимает PNG в директории decompiled/res
|
||||
- remove_drawable_files: true/false - удаляет неиспользованные drawable-* из директории decompiled/res
|
||||
- remove_language_files: true/false - удаляет все языки кроме русского и английского
|
||||
|
||||
## efficiency
|
||||
|
||||
Проверено с версией 9.0 Beta 7
|
||||
|
||||
разница = оригинальный размер апк - патченный размер апк, не учитывая другие патчи
|
||||
|
||||
| Настройка | Размер файла | Разница | % |
|
||||
| :----------- | :-------------------: | :-----------------: | :-: |
|
||||
| None | 17092 bytes - 17.1 MB | - | - |
|
||||
| Compress PNG | 17072 bytes - 17.1 MB | 20 bytes - 0.0 MB | 0.11% |
|
||||
| Remove files | 17020 bytes - 17.0 MB | 72 bytes - 0.1 MB | 0.42% |
|
||||
| Remove draws | 16940 bytes - 16.9 MB | 152 bytes - 0.2 MB | 0.89% |
|
||||
| Remove lines | 16444 bytes - 16.4 MB | 648 bytes - 0.7 MB | 3.79% |
|
||||
| Remove ai vo | 15812 bytes - 15.8 MB | 1280 bytes - 1.3 MB | 7.49% |
|
||||
| Remove langs | 15764 bytes - 15.7 MB | 1328 bytes - 1.3 MB | 7.76% |
|
||||
| Все включены | 13592 bytes - 13.6 MB | 3500 bytes - 4.8 MB | 20.5% |
|
||||
@@ -0,0 +1,270 @@
|
||||
"""Remove and compress resources"""
|
||||
|
||||
priority = -1
|
||||
|
||||
# imports
|
||||
import os
|
||||
import shutil
|
||||
import subprocess
|
||||
from tqdm import tqdm
|
||||
from utils.smali_parser import get_smali_lines, save_smali_lines
|
||||
|
||||
|
||||
# Patch
|
||||
def remove_unknown_files(config):
|
||||
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 config.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 config.get("verbose", False):
|
||||
tqdm.write(f"Удалёна директория: {item_path}")
|
||||
return True
|
||||
|
||||
|
||||
def remove_debug_lines(config):
|
||||
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, 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):
|
||||
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):
|
||||
blank = "./patches/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):
|
||||
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):
|
||||
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) -> bool:
|
||||
if config["remove_unknown_files"]:
|
||||
tqdm.write(f"Удаление неизвестных файлов...")
|
||||
remove_unknown_files(config)
|
||||
|
||||
if config["remove_drawable_files"]:
|
||||
tqdm.write(f"Удаление директорий drawable-xx...")
|
||||
remove_drawable_files(config)
|
||||
|
||||
if config["compress_png_files"]:
|
||||
tqdm.write(f"Сжатие PNG файлов...")
|
||||
compress_png_files(config)
|
||||
|
||||
if config["remove_language_files"]:
|
||||
tqdm.write(f"Удаление языков...")
|
||||
remove_language_files(config)
|
||||
|
||||
if config["remove_AI_voiceover"]:
|
||||
tqdm.write(f"Удаление ИИ озвучки...")
|
||||
remove_AI_voiceover(config)
|
||||
|
||||
if config["remove_debug_lines"]:
|
||||
tqdm.write(f"Удаление дебаг линий...")
|
||||
remove_debug_lines(config)
|
||||
|
||||
return True
|
||||
@@ -1,38 +0,0 @@
|
||||
"""Compress PNGs"""
|
||||
|
||||
priority = -1
|
||||
from tqdm import tqdm
|
||||
|
||||
import os
|
||||
import subprocess
|
||||
|
||||
|
||||
def compress_pngs(root_dir):
|
||||
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)
|
||||
tqdm.write(f"Сжимаю: {filepath}")
|
||||
try:
|
||||
subprocess.run(
|
||||
[
|
||||
"pngquant",
|
||||
"--force",
|
||||
"--ext",
|
||||
".png",
|
||||
"--quality=65-90",
|
||||
filepath,
|
||||
],
|
||||
check=True,
|
||||
capture_output=True,
|
||||
)
|
||||
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")
|
||||
return len(files) > 0 and any(files)
|
||||
@@ -1,55 +0,0 @@
|
||||
{
|
||||
"tools": {
|
||||
"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"
|
||||
},
|
||||
"new_package_name": "com.wowlikon.anixart",
|
||||
"server": "https://anixarty.wowlikon.tech/modding",
|
||||
"theme": {
|
||||
"colors": {
|
||||
"primary": "#ccff00",
|
||||
"secondary": "#ffffd700",
|
||||
"background": "#FFFFFF",
|
||||
"text": "#000000"
|
||||
},
|
||||
"gradient": {
|
||||
"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": "Мы в Telegram",
|
||||
"description": "Подпишитесь на канал, чтобы быть в курсе последних новостей.",
|
||||
"url": "https://t.me/wowlikon",
|
||||
"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": [0.25, 0.5, 0.75, 1.0, 1.25, 1.5, 1.75, 2.0, 2.5, 3.0, 9.0]
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
"""Change application icon"""
|
||||
|
||||
priority = 0
|
||||
from tqdm import tqdm
|
||||
|
||||
import struct
|
||||
|
||||
|
||||
def float_to_hex(f):
|
||||
b = struct.pack(">f", f)
|
||||
return b.hex()
|
||||
|
||||
|
||||
def apply(config: dict) -> bool:
|
||||
assert float_to_hex(1.5) == "0x3fc00000"
|
||||
return False
|
||||
@@ -1,5 +1,6 @@
|
||||
"""Disable ad banners"""
|
||||
|
||||
"""
|
||||
Удаляет баннеры рекламы
|
||||
"""
|
||||
priority = 0
|
||||
|
||||
from utils.smali_parser import (
|
||||
@@ -9,6 +10,7 @@ from utils.smali_parser import (
|
||||
replace_smali_method_body,
|
||||
)
|
||||
|
||||
|
||||
replace = """ .locals 0
|
||||
|
||||
const/4 p0, 0x1
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
"""Remove beta banner"""
|
||||
|
||||
"""
|
||||
Удаляет баннеры бета-версии
|
||||
"""
|
||||
priority = 0
|
||||
|
||||
import os
|
||||
from tqdm import tqdm
|
||||
from lxml import etree
|
||||
|
||||
from typing import TypedDict
|
||||
|
||||
|
||||
def apply(config) -> bool:
|
||||
attributes = [
|
||||
|
||||
+7
-12
@@ -1,11 +1,13 @@
|
||||
"""Insert new files"""
|
||||
|
||||
"""
|
||||
Вставляет новые файлы в проект
|
||||
"""
|
||||
priority = 0
|
||||
from tqdm import tqdm
|
||||
|
||||
import shutil
|
||||
import os
|
||||
|
||||
from utils.public import insert_after_public
|
||||
|
||||
|
||||
def apply(config: dict) -> bool:
|
||||
# Mod first launch window
|
||||
@@ -33,14 +35,7 @@ def apply(config: dict) -> bool:
|
||||
)
|
||||
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",
|
||||
)
|
||||
insert_after_public("warning_error_counter_background", "ic_custom_telegram")
|
||||
insert_after_public("warning_error_counter_background", "ic_custom_crown")
|
||||
|
||||
return True
|
||||
|
||||
+17
-4
@@ -1,9 +1,14 @@
|
||||
"""Change package name of apk"""
|
||||
"""
|
||||
Изменяет имя пакета в apk, удаляет вход по google и vk
|
||||
|
||||
"package_name": {
|
||||
"new_package_name": "com.wowlikon.anixart"
|
||||
}
|
||||
"""
|
||||
priority = -1
|
||||
from tqdm import tqdm
|
||||
|
||||
import os
|
||||
from lxml import etree
|
||||
|
||||
|
||||
def rename_dir(src, dst):
|
||||
@@ -12,8 +17,6 @@ def rename_dir(src, dst):
|
||||
|
||||
|
||||
def apply(config: dict) -> bool:
|
||||
assert config["new_package_name"] is not None, "new_package_name is not configured"
|
||||
|
||||
for root, dirs, files in os.walk("./decompiled"):
|
||||
for filename in files:
|
||||
file_path = os.path.join(root, filename)
|
||||
@@ -88,6 +91,16 @@ def apply(config: dict) -> bool:
|
||||
except:
|
||||
pass
|
||||
|
||||
file_path = "./decompiled/res/layout/fragment_sign_in.xml"
|
||||
parser = etree.XMLParser(remove_blank_text=True)
|
||||
tree = etree.parse(file_path, parser)
|
||||
root = tree.getroot()
|
||||
|
||||
last_linear = root.xpath("//LinearLayout/LinearLayout[4]")[0]
|
||||
last_linear.set(f"{{{config['xml_ns']['android']}}}visibility", "gone")
|
||||
|
||||
tree.write(file_path, pretty_print=True, xml_declaration=True, encoding="utf-8")
|
||||
|
||||
return True
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
"""
|
||||
Меняет порядок вкладок в панели навигации
|
||||
|
||||
"replace_navbar": {
|
||||
"items": ["home", "discover", "feed", "bookmarks", "profile"]
|
||||
}
|
||||
"""
|
||||
priority = 0
|
||||
|
||||
from lxml import etree
|
||||
from tqdm import tqdm
|
||||
|
||||
def apply(config: dict) -> 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=config['xml_ns'])
|
||||
|
||||
def get_id_suffix(item):
|
||||
full_id = item.get(f"{{{config['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=config['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.
@@ -1,10 +1,29 @@
|
||||
"""Add new settings"""
|
||||
"""
|
||||
Добавляет в настройки ссылки и доавляет текст к версии приложения
|
||||
|
||||
"settings_urls": {
|
||||
"menu": {
|
||||
"Раздел": [
|
||||
{
|
||||
"title": "Заголовок",
|
||||
"description": "Описание",
|
||||
"url": "ссылка",
|
||||
"icon": "@drawable/ic_custom_telegram",
|
||||
"icon_space_reserved": "false"
|
||||
},
|
||||
...
|
||||
],
|
||||
...
|
||||
]
|
||||
},
|
||||
"version": " by wowlikon"
|
||||
}
|
||||
"""
|
||||
priority = 0
|
||||
from tqdm import tqdm
|
||||
|
||||
from lxml import etree
|
||||
|
||||
# Generate PreferenceCategory
|
||||
|
||||
def make_category(ns, name, items):
|
||||
cat = etree.Element("PreferenceCategory", nsmap=ns)
|
||||
cat.set(f"{{{ns['android']}}}title", name)
|
||||
@@ -33,10 +52,25 @@ def apply(config: dict) -> bool:
|
||||
# Insert new PreferenceCategory before the last element
|
||||
last = root[-1] # last element
|
||||
pos = root.index(last)
|
||||
for section, items in config["settings_urls"].items():
|
||||
for section, items in config["menu"].items():
|
||||
root.insert(pos, make_category(config["xml_ns"], section, items))
|
||||
pos += 1
|
||||
|
||||
# Save back
|
||||
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
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
"""Change application icon"""
|
||||
priority = 0
|
||||
from tqdm import tqdm
|
||||
|
||||
import time
|
||||
|
||||
def apply(config: dict) -> bool:
|
||||
time.sleep(0.2)
|
||||
return False
|
||||
@@ -0,0 +1,25 @@
|
||||
"""
|
||||
Добавляет пользовательские скорости воспроизведения видео
|
||||
|
||||
"custom_speed": {
|
||||
"speeds": [9.0]
|
||||
}
|
||||
"""
|
||||
priority = 0
|
||||
|
||||
from utils.smali_parser import float_to_hex
|
||||
from utils.public import (
|
||||
insert_after_public,
|
||||
insert_after_id,
|
||||
)
|
||||
|
||||
def apply(config: dict) -> bool:
|
||||
assert float_to_hex(1.5) == "0x3fc00000"
|
||||
|
||||
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)}"
|
||||
|
||||
return False
|
||||
@@ -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
|
||||
+27
-10
@@ -1,10 +1,27 @@
|
||||
/res/layout/monetization_ads_internal_rewarded_close_verification.xml
|
||||
❯ diff a.txt b.txt
|
||||
4c4
|
||||
< android:background="@drawable/monetization_ads_internal_rewarded_close_verification_button_close_background"
|
||||
---
|
||||
> android:background="@drawable/draw030e"
|
||||
16c16
|
||||
< android:background="@drawable/monetization_ads_internal_rewarded_close_verification_button_dismiss_background"
|
||||
---
|
||||
> android:background="@drawable/draw030f"
|
||||
/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,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>
|
||||
+65
-7
@@ -2,15 +2,15 @@ from lxml import etree
|
||||
from copy import deepcopy
|
||||
|
||||
|
||||
def insert_after(anchor_name: str, elem_name: str):
|
||||
file_path = "../decompiled/res/values/public.xml"
|
||||
def insert_after_public(anchor_name: str, elem_name: str):
|
||||
file_path = "./decompiled/res/values/public.xml"
|
||||
parser = etree.XMLParser(remove_blank_text=True)
|
||||
tree = etree.parse(file_path, parser)
|
||||
root = tree.getroot()
|
||||
anchor = None
|
||||
types = {}
|
||||
|
||||
for idx, elem in enumerate(root):
|
||||
for elem in root:
|
||||
assert elem.tag == "public"
|
||||
assert elem.keys() == ["type", "name", "id"]
|
||||
attrs = dict(zip(elem.keys(), elem.values()))
|
||||
@@ -25,7 +25,6 @@ def insert_after(anchor_name: str, elem_name: str):
|
||||
if i not in group:
|
||||
free_ids.add(i)
|
||||
|
||||
assert len(free_ids) > 0
|
||||
new_id = None
|
||||
for i in free_ids:
|
||||
if i > int(anchor[1]["id"], 16):
|
||||
@@ -38,12 +37,71 @@ def insert_after(anchor_name: str, elem_name: str):
|
||||
if name == anchor[1]["type"]:
|
||||
continue
|
||||
if new_id in group:
|
||||
new_id = max(group)
|
||||
assert False, f"ID {new_id} already exists in group {name}"
|
||||
|
||||
new_elem = deepcopy(anchor[0])
|
||||
new_elem.set("id", new_id)
|
||||
new_elem.set("id", hex(new_id))
|
||||
new_elem.set("name", elem_name)
|
||||
anchor[0].addnext(new_elem)
|
||||
|
||||
tree.write(file_path, pretty_print=True, xml_declaration=True, encoding="utf-8")
|
||||
return new_id
|
||||
|
||||
|
||||
def insert_after_id(anchor_name: str, elem_name: str):
|
||||
file_path = "./decompiled/res/values/ids.xml"
|
||||
parser = etree.XMLParser(remove_blank_text=True)
|
||||
tree = etree.parse(file_path, parser)
|
||||
root = tree.getroot()
|
||||
anchor = None
|
||||
|
||||
for elem in root:
|
||||
assert elem.tag == "item"
|
||||
assert elem.keys() == ["type", "name"]
|
||||
attrs = dict(zip(elem.keys(), elem.values()))
|
||||
if attrs["name"] == anchor_name:
|
||||
assert anchor == None
|
||||
anchor = (elem, attrs)
|
||||
|
||||
new_elem = deepcopy(anchor[0])
|
||||
new_elem.set("name", elem_name)
|
||||
anchor[0].addnext(new_elem)
|
||||
tree.write(file_path, pretty_print=True, xml_declaration=True, encoding="utf-8")
|
||||
|
||||
|
||||
def change_color(name: str, value: str):
|
||||
file_path = "./decompiled/res/values/colors.xml"
|
||||
parser = etree.XMLParser(remove_blank_text=True)
|
||||
tree = etree.parse(file_path, parser)
|
||||
root = tree.getroot()
|
||||
replacements = 0
|
||||
|
||||
for elem in root:
|
||||
assert elem.tag == "color"
|
||||
assert elem.keys() == ["name"]
|
||||
attrs = dict(zip(elem.keys(), elem.values()))
|
||||
if attrs["name"] == name:
|
||||
elem.set("name", name)
|
||||
elem.text = value
|
||||
replacements += 1
|
||||
assert replacements >= 1
|
||||
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):
|
||||
file_path = "./decompiled/res/values/colors.xml"
|
||||
parser = etree.XMLParser(remove_blank_text=True)
|
||||
tree = etree.parse(file_path, parser)
|
||||
root = tree.getroot()
|
||||
anchor = None
|
||||
|
||||
for elem in root:
|
||||
assert elem.tag == "color"
|
||||
assert elem.keys() == ["name"]
|
||||
attrs = dict(zip(elem.keys(), elem.values()))
|
||||
if attrs["name"] == anchor_name:
|
||||
assert anchor == None
|
||||
anchor = (elem, attrs)
|
||||
|
||||
new_elem = deepcopy(anchor[0])
|
||||
new_elem.set("name", elem_name)
|
||||
anchor[0].addnext(new_elem)
|
||||
tree.write(file_path, pretty_print=True, xml_declaration=True, encoding="utf-8")
|
||||
|
||||
+25
-14
@@ -1,27 +1,41 @@
|
||||
import struct
|
||||
|
||||
|
||||
def get_smali_lines(file: str) -> list[str]:
|
||||
lines = []
|
||||
with open(file, "r", encoding="utf-8") as smali:
|
||||
lines = smali.readlines()
|
||||
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:
|
||||
while True:
|
||||
index -= 1
|
||||
if lines[index].find(".method") >= 0:
|
||||
return index
|
||||
|
||||
|
||||
def find_smali_method_end(lines: list[str], index: int) -> int:
|
||||
while True:
|
||||
index += 1
|
||||
if lines[index].find(".end method") >= 0:
|
||||
return index
|
||||
|
||||
|
||||
def debug_print_smali_method(lines: list[str], start: int, end: int) -> None:
|
||||
while start != (end + 1):
|
||||
print(start, lines[start])
|
||||
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 = []
|
||||
index = 0
|
||||
skip = end - start - 1
|
||||
@@ -38,21 +52,18 @@ def replace_smali_method_body(lines: list[str], start: int, end: int, new_lines:
|
||||
new_content.append(lines[index])
|
||||
index += 1
|
||||
|
||||
|
||||
return new_content
|
||||
|
||||
# example i guess
|
||||
# if __name__ == "__main__":
|
||||
# lines = get_smali_lines("./decompiled/smali_classes2/com/radiquum/anixart/Prefs.smali")
|
||||
|
||||
# for index, line in enumerate(lines):
|
||||
# if line.find("IS_SPONSOR") >= 0:
|
||||
# method_start = find_smali_method_start(lines, index)
|
||||
# method_end = find_smali_method_end(lines, index)
|
||||
# new_content = replace_smali_method_body(lines, method_start, method_end, c)
|
||||
def find_and_replace_smali_line(
|
||||
lines: list[str], search: str, replace: str
|
||||
) -> list[str]:
|
||||
for index, line in enumerate(lines):
|
||||
if line.find(search) >= 0:
|
||||
lines[index] = lines[index].replace(search, replace)
|
||||
return lines
|
||||
|
||||
# with open("./help/Prefs_orig.smali", "w", encoding="utf-8") as file:
|
||||
# file.writelines(lines)
|
||||
# with open("./help/Prefs_modified.smali", "w", encoding="utf-8") as file:
|
||||
# file.writelines(new_content)
|
||||
|
||||
def float_to_hex(f):
|
||||
b = struct.pack(">f", f)
|
||||
return b.hex()
|
||||
|
||||
Reference in New Issue
Block a user