32 Commits

Author SHA1 Message Date
85aef3d997 Добавлеие вывода информации о включеных патчах
Build mod / build (push) Successful in 5m27s
2025-09-18 10:54:30 +03:00
41399eca2c Запуск сборки на создание тэга
Build mod / build (push) Successful in 5m1s
2025-09-15 23:34:24 +03:00
137c939e1d Загрузка pngquant 2025-09-15 23:31:19 +03:00
18ad769d33 Дополнение requirements.txt 2025-09-15 23:26:33 +03:00
5b39aec161 Исправление загрузки файлов с прогресс-баром 2025-09-15 23:22:48 +03:00
c1bb2f8845 Исправление загрузки tools 2025-09-15 16:48:00 +03:00
630ab0d094 Merge remote-tracking branch 'origin/main' 2025-09-15 16:33:39 +03:00
0f9f6f2932 Исправление патча compress обновление списка зависимостей 2025-09-15 16:33:26 +03:00
a09181fe5a Исправление requirements 2025-09-15 11:47:17 +00:00
77694ec4b7 Merge remote-tracking branch 'origin/main' 2025-09-15 00:27:11 +03:00
b8ab508dfb Доавление описания патчей в docstring 2025-09-15 00:26:45 +03:00
debf561cf9 Обновить README.md 2025-09-14 18:51:03 +00:00
f7e186d5db Обновить README.md 2025-09-14 18:47:46 +00:00
24a8a1d4d3 Патч панели навигации и подписи версии в настройках 2025-09-14 21:46:39 +03:00
550427338a Обновление документации репозитория 2025-09-14 17:46:33 +00:00
ac241e1189 Перенос добавления ресурсов в соответствующие патчи 2025-09-14 20:37:14 +03:00
c22ef507ba Слияние с обновлённым main 2025-09-14 20:26:24 +03:00
9da9e98547 Улучшение кода main.py и конфигурации 2025-09-14 20:12:28 +03:00
48953a857b Merge pull request 'Объединение патчей cleanup.py и compress_png.py в один compress.py. Добавление доп. функций для сжатия апк' (#9) from Radiquum/patcher:main into main
Reviewed-on: #9
2025-09-14 17:05:08 +00:00
871ec11f7e Объединение патчей cleanup.py и compress_png.py в один compress.py. Добавление доп. функций для сжатия апк 2025-09-14 19:45:52 +05:00
9453b3b50b Исправление workflow 2025-09-13 20:53:59 +03:00
48ea732d77 Добавить .gitea/workflows /example.yml 2025-09-13 17:52:01 +00:00
62e23a2eb0 Обновить .gitea/workflows /build.yml 2025-09-13 17:47:49 +00:00
5986d8b069 Добавление workflow для сборки apk 2025-09-13 20:25:47 +03:00
cc49aad2aa Удаление автоматической замены сервера и полная сборка apk 2025-09-13 19:29:29 +03:00
d6f616da7a Удаление входа по VK и Google и обновление патчей api 2025-09-11 14:44:07 +03:00
3b2e5bee18 Фикс package_name 2025-09-09 12:48:10 +03:00
8a74245c9c Исправление патчей, реализация базвого функционала для сборки apk 2025-09-08 13:07:46 +03:00
0f53c836ae Merge remote-tracking branch 'origin/main' 2025-09-02 11:11:44 +03:00
d0744050d2 игнорирование недоделаных патчей, удаление лишних импортов 2025-09-02 11:11:17 +03:00
8f30061d44 Добавление mermaid диаграммы 2025-09-01 15:29:18 +00:00
e2614990df Обновление workflow 2001-01-01 00:00:00 +00:00
29 changed files with 1161 additions and 379 deletions
+81
View File
@@ -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
View File
@@ -5,3 +5,5 @@ tools
__pycache__
.venv
*.jks
*.pass
+40 -17
View File
@@ -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
View File
@@ -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"
}
}
}
+256 -122
View File
@@ -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()
apk = select_apk()
patch = decompile_apk(apk)
def select_apk() -> Path:
apks = [f for f in ORIGINAL.glob("*.apk")]
if not apks:
console.print("[red]Нет apk-файлов в папке original")
raise typer.Exit(1)
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))
if len(apks) == 1:
console.print(f"[green]Выбран {apks[0].name}")
return apks[0]
patches.sort(key=lambda x: x.package.priority, reverse=True)
options = {str(i): apk for i, apk in enumerate(apks, 1)}
for k, v in options.items():
console.print(f"{k}. {v.name}")
for patch in tqdm(patches, colour="green", desc="Применение патчей"):
tqdm.write(f"Применение патча: {patch.name}")
patch.apply(conf)
choice = Prompt.ask("Выберите номер", choices=list(options.keys()))
return options[choice]
statuses = {}
for patch in patches:
statuses[patch.name] = patch.applied
marker = colorama.Fore.GREEN + "" if patch.applied else colorama.Fore.RED + ""
print(f"{marker}{colorama.Style.RESET_ALL} {patch.name}")
if all(statuses.values()):
print("Все патчи успешно применены")
elif any(statuses.values()):
print("Некоторые патчи не были успешно применены")
else:
print("Ни один патч не был успешно применен")
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()
decompile(apk)
patch_settings = conf.get("patches", {})
patch_objs: List[Patch] = []
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))
patch_objs.sort(key=lambda p: p.priority, reverse=True)
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)
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:
console.print("[red]Сборка отменена")
raise typer.Exit(1)
if __name__ == "__main__":
app()
+38 -7
View File
@@ -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
-19
View File
@@ -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
View File
@@ -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
+30
View File
@@ -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% |
+270
View File
@@ -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
-38
View File
@@ -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)
-55
View File
@@ -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]
}
-16
View File
@@ -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
+4 -2
View File
@@ -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
+4 -4
View File
@@ -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
View File
@@ -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
View File
@@ -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
+47
View File
@@ -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.
+38 -4
View File
@@ -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
+25
View File
@@ -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
+9
View File
@@ -0,0 +1,9 @@
typer[all]>=0.16.0
rich>=14.1.0
httpx>=0.28.1
pydantic>=2.11.7
plumbum>=1.9.0
lxml>=6.0.1
PyYAML>=6.0.2
tqdm>=4.67.1
requests>=2.32.5
+27 -10
View File
@@ -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" />
-27
View File
@@ -1,27 +0,0 @@
/res/layout/release_info.xml
<TextView android:textColor="@color/light_md_blue_500" android:id="@id/note" android:background="@drawable/bg_release_note" android:paddingTop="12.0dip" android:paddingBottom="12.0dip" android:layout_width="fill_parent" android:layout_height="wrap_content" android:textIsSelectable="true" android:paddingStart="16.0dip" android:paddingEnd="16.0dip" style="?textAppearanceBodyMedium" />
</FrameLayout>
<LinearLayout android:orientation="vertical" android:layout_width="fill_parent" android:layout_height="fill_parent">
<LinearLayout android:gravity="center_vertical" android:orientation="horizontal" android:id="@id/countryLayout" android:visibility="gone" android:layout_width="fill_parent" android:layout_height="fill_parent">
<androidx.appcompat.widget.AppCompatImageView android:id="@id/iconCountry" android:layout_width="wrap_content" android:layout_height="wrap_content" app:srcCompat="@drawable/ic_flag_japan" />
<TextView android:textColor="?primaryTextColor" android:textColorLink="?primaryTextColor" android:gravity="center_vertical" android:linksClickable="true" android:id="@id/tvCountry" android:layout_width="fill_parent" android:layout_height="wrap_content" android:textIsSelectable="true" android:layout_marginStart="8.0dip" style="?textAppearanceBodyMedium" />
</LinearLayout>
<LinearLayout android:gravity="center_vertical" android:orientation="horizontal" android:id="@id/episodesLayout" android:visibility="gone" android:layout_width="fill_parent" android:layout_height="fill_parent" android:layout_marginTop="2.0dip">
<androidx.appcompat.widget.AppCompatImageView android:layout_width="wrap_content" android:layout_height="wrap_content" app:srcCompat="@drawable/ic_episodes" />
<TextView android:textColor="?primaryTextColor" android:textColorLink="?primaryTextColor" android:gravity="center_vertical" android:linksClickable="true" android:id="@id/tvEpisodes" android:layout_width="fill_parent" android:layout_height="wrap_content" android:textIsSelectable="true" android:layout_marginStart="8.0dip" style="?textAppearanceBodyMedium" />
</LinearLayout>
<LinearLayout android:gravity="center_vertical" android:orientation="horizontal" android:id="@id/scheduleLayout" android:visibility="gone" android:layout_width="fill_parent" android:layout_height="fill_parent" android:layout_marginTop="2.0dip">
<androidx.appcompat.widget.AppCompatImageView android:layout_width="wrap_content" android:layout_height="wrap_content" app:srcCompat="@drawable/ic_schedule" />
<TextView android:textColor="?primaryTextColor" android:textColorLink="?primaryTextColor" android:gravity="center_vertical" android:linksClickable="true" android:id="@id/tvSchedule" android:layout_width="fill_parent" android:layout_height="wrap_content" android:textIsSelectable="true" android:layout_marginStart="8.0dip" style="?textAppearanceBodyMedium" />
</LinearLayout>
<LinearLayout android:gravity="center_vertical" android:orientation="horizontal" android:id="@id/sourceLayout" android:visibility="gone" android:layout_width="fill_parent" android:layout_height="fill_parent" android:layout_marginTop="2.0dip">
<androidx.appcompat.widget.AppCompatImageView android:layout_width="wrap_content" android:layout_height="wrap_content" app:srcCompat="@drawable/ic_source" />
<TextView android:textColor="?primaryTextColor" android:textColorLink="?primaryTextColor" android:gravity="center_vertical" android:linksClickable="true" android:id="@id/tvSource" android:layout_width="fill_parent" android:layout_height="wrap_content" android:lineSpacingExtra="4.0sp" android:textIsSelectable="true" android:layout_marginStart="8.0dip" style="?textAppearanceBodyMedium" />
</LinearLayout>
<LinearLayout android:orientation="horizontal" android:id="@id/studioLayout" android:visibility="gone" android:layout_width="fill_parent" android:layout_height="wrap_content" android:layout_marginTop="2.0dip">
<androidx.appcompat.widget.AppCompatImageView android:layout_width="wrap_content" android:layout_height="wrap_content" app:srcCompat="@drawable/ic_studio" />
<TextView android:textColor="?primaryTextColor" android:textColorLink="?primaryTextColor" android:gravity="center_vertical" android:linksClickable="true" android:id="@id/tvStudio" android:layout_width="fill_parent" android:layout_height="wrap_content" android:lineSpacingExtra="4.0sp" android:textIsSelectable="true" android:layout_marginStart="8.0dip" style="?textAppearanceBodyMedium" />
</LinearLayout>
</LinearLayout>
<TextView android:textColor="?primaryTextColor" android:textColorLink="?primaryTextColor" android:linksClickable="true" android:id="@id/tvGenres" android:visibility="gone" android:layout_width="fill_parent" android:layout_height="wrap_content" android:layout_marginTop="16.0dip" android:layout_marginBottom="2.0dip" android:lineSpacingExtra="4.0sp" android:textIsSelectable="true" style="?textAppearanceBodyMedium" />
<at.blogc.android.views.ExpandableTextView android:textColor="?primaryTextColor" android:id="@id/description" android:layout_width="fill_parent" android:layout_height="wrap_content" android:layout_marginTop="8.0dip" android:layout_marginBottom="8.0dip" android:maxLines="5" android:lineSpacingExtra="4.0sp" android:textIsSelectable="true" app:animation_duration="225" style="?textAppearanceBodyMedium" />
-2
View File
@@ -1,2 +0,0 @@
/res/menu/bottom.xml
replace lines
-1
View File
@@ -1 +0,0 @@
<color name="ic_launcher_background">#ff000000</color>
+65 -7
View File
@@ -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
View File
@@ -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()