Улучшение cli и удобства создания патчей
Сборка мода / build (push) Successful in 2m16s

This commit is contained in:
2025-12-28 17:47:56 +03:00
parent ec047cd3a5
commit 70337ee3ec
35 changed files with 2200 additions and 1111 deletions
View File
+55 -40
View File
@@ -6,58 +6,73 @@
}
"""
priority = 0
# imports
__author__ = "wowlikon <wowlikon@gmail.com>"
__version__ = "1.0.0"
import json
from typing import Any, Dict
import requests
from tqdm import tqdm
from typing import Dict, Any
from pydantic import Field
from tqdm import tqdm
from utils.config import PatchConfig
from utils.config import PatchTemplate
#Config
class Config(PatchConfig):
class Patch(PatchTemplate):
priority: int = Field(frozen=True, exclude=True, default=0)
server: str = Field("https://anixarty.0x174.su/patch", description="URL сервера")
# Patch
def apply(config: Config, base: Dict[str, Any]) -> bool:
response = requests.get(config.server) # Получаем данные для патча
assert response.status_code == 200, f"Failed to fetch data {response.status_code} {response.text}"
def apply(self, base: Dict[str, Any]) -> bool:
response = requests.get(self.server) # Получаем данные для патча
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']: # Применяем замены API
tqdm.write(f"Изменение {item['file']}")
filepath = './decompiled/smali_classes2/com/swiftsoft/anixartd/network/api/'+item['file']
new_api = json.loads(response.text)
for item in new_api["modifications"]: # Применяем замены API
tqdm.write(f"Изменение {item['file']}")
filepath = (
"./decompiled/smali_classes2/com/swiftsoft/anixartd/network/api/"
+ item["file"]
)
with open(filepath, 'r') as f:
with open(filepath, "r") as f:
content = f.read()
with open(filepath, "w") as f:
if content.count(item["src"]) == 0:
tqdm.write(f"Не найдено {item['src']}")
f.write(content.replace(item["src"], item["dst"]))
tqdm.write(
f"Изменение Github ссылки"
) # Обновление ссылки на поиск серверов в Github
filepath = "./decompiled/smali_classes2/com/swiftsoft/anixartd/utils/anixnet/GithubPagesNetFetcher.smali"
with open(filepath, "r") as f:
content = f.read()
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']))
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"]}"',
)
)
tqdm.write(f"Изменение Github ссылки") # Обновление ссылки на поиск серверов в Github
filepath = './decompiled/smali_classes2/com/swiftsoft/anixartd/utils/anixnet/GithubPagesNetFetcher.smali'
tqdm.write(
"Удаление динамического выбора сервера"
) # Отключение автовыбора сервера
filepath = "./decompiled/smali_classes2/com/swiftsoft/anixartd/DaggerApp_HiltComponents_SingletonC$SingletonCImpl$SwitchingProvider.smali"
with open(filepath, 'r') as f:
content = f.read()
content = ""
with open(filepath, "r") as f:
for line in f.readlines():
if "addInterceptor" in line:
continue
content += line
with open(filepath, 'w') as f:
f.write(content.replace('const-string v1, "https://anixhelper.github.io/pages/urls.json"', f'const-string v1, "{new_api["gh"]}"'))
with open(filepath, "w") as f:
f.write(content)
tqdm.write("Удаление динамического выбора сервера") # Отключение автовыбора сервера
filepath = './decompiled/smali_classes2/com/swiftsoft/anixartd/DaggerApp_HiltComponents_SingletonC$SingletonCImpl$SwitchingProvider.smali'
content = ""
with open(filepath, 'r') as f:
for line in f.readlines():
if "addInterceptor" in line: continue
content += line
with open(filepath, 'w') as f:
f.write(content)
return True
return True
+146 -84
View File
@@ -19,127 +19,189 @@
}
"""
priority = 0
__author__ = "wowlikon <wowlikon@gmail.com>"
__version__ = "1.0.0"
from typing import Any, Dict
# imports
from lxml import etree
from typing import Dict, Any
from pydantic import Field, BaseModel
from pydantic import BaseModel, Field, model_validator
from utils.config import PatchTemplate
from utils.public import change_color, insert_after_color, insert_after_public
from utils.config import PatchConfig
from utils.public import (
insert_after_public,
insert_after_color,
change_color,
)
#Config
class Gradient(BaseModel):
priority: int = Field(frozen=True, exclude=True, default=0)
angle: float = Field(0.0, description="Угол градиента")
start_color: str = Field("#ffccff00", description="Начальный цвет градиента")
end_color: str = Field("#ffcccc00", description="Конечный цвет градиента")
class Logo(BaseModel):
gradient: Gradient = Field(Gradient(), description="Настройки градиента") # type: ignore [reportCallIssue]
gradient: Gradient = Field(
default_factory=Gradient, description="Настройки градиента"
)
ears_color: str = Field("#ffd0d0d0", description="Цвет ушей логотипа")
class Colors(BaseModel):
primary: str = Field("#ccff00", description="Основной цвет")
secondary: str = Field("#ffcccc00", description="Вторичный цвет")
background: str = Field("#ffffff", description="Фоновый цвет")
text: str = Field("#000000", description="Цвет текста")
class Config(PatchConfig):
logo: Logo = Field(Logo(), description="Настройки цветов логотипа") # type: ignore [reportCallIssue]
colors: Colors = Field(Colors(), description="Настройки цветов") # type: ignore [reportCallIssue]
# Patch
def apply(config: Config, base: Dict[str, Any]) -> bool:
main_color = config.colors.primary
splash_color = config.colors.secondary
class Patch(PatchTemplate):
priority: int = Field(frozen=True, exclude=True, default=0)
logo: Logo = Field(default_factory=Logo, description="Настройки цветов логотипа")
colors: Colors = Field(default_factory=Colors, description="Настройки цветов")
# Обновление сообщения об отсутствии подключения
with open("./decompiled/assets/no_connection.html", "r", encoding="utf-8") as file:
file_contents = file.read()
@model_validator(mode="before")
@classmethod
def validate_nested(cls, data):
if isinstance(data, dict):
if "logo" in data and isinstance(data["logo"], dict):
data["logo"] = Logo(**data["logo"])
if "colors" in data and isinstance(data["colors"], dict):
data["colors"] = Colors(**data["colors"])
return data
new_contents = file_contents.replace("#f04e4e", main_color)
def hex_to_lottie(hex_color: str) -> tuple[float, float, float]:
hex_color = hex_color.lstrip("#")
hex_color = hex_color[2:] if len(hex_color) == 8 else hex_color
return (
int(hex_color[:2], 16) / 255.0,
int(hex_color[2:4], 16) / 255.0,
int(hex_color[4:6], 16) / 255.0,
)
with open("./decompiled/assets/no_connection.html", "w", encoding="utf-8") as file:
file.write(new_contents)
def apply(self, base: Dict[str, Any]) -> bool:
main_color = self.colors.primary
splash_color = self.colors.secondary
# Суффиксы лого
drawable_types = ["", "-night"]
# Обновление сообщения об отсутствии подключения
with open(
"./decompiled/assets/no_connection.html", "r", encoding="utf-8"
) as file:
file_contents = file.read()
for drawable_type in drawable_types:
# Градиент лого приложения
file_path = f"./decompiled/res/drawable{drawable_type}/$logo__0.xml"
new_contents = file_contents.replace("#f04e4e", main_color)
parser = etree.XMLParser(remove_blank_text=True)
tree = etree.parse(file_path, parser)
root = tree.getroot()
with open(
"./decompiled/assets/no_connection.html", "w", encoding="utf-8"
) as file:
file.write(new_contents)
# Замена атрибутов значениями из конфигурации
root.set(f"{{{base['xml_ns']['android']}}}angle", str(config.logo.gradient.angle))
root.set(f"{{{base['xml_ns']['android']}}}startColor", config.logo.gradient.start_color)
root.set(f"{{{base['xml_ns']['android']}}}endColor", config.logo.gradient.end_color)
# Суффиксы лого
drawable_types = ["", "-night"]
# Сохранение
tree.write(file_path, pretty_print=True, xml_declaration=True, encoding="utf-8")
for drawable_type in drawable_types:
# Градиент лого приложения
file_path = f"./decompiled/res/drawable{drawable_type}/$logo__0.xml"
# Замена анимации лого
file_path = f"./decompiled/res/drawable{drawable_type}/$logo_splash_anim__0.xml"
parser = etree.XMLParser(remove_blank_text=True)
tree = etree.parse(file_path, parser)
root = tree.getroot()
parser = etree.XMLParser(remove_blank_text=True)
tree = etree.parse(file_path, parser)
root = tree.getroot()
# Замена атрибутов значениями из конфигурации
root.set(
f"{{{base['xml_ns']['android']}}}angle", str(self.logo.gradient.angle)
)
root.set(
f"{{{base['xml_ns']['android']}}}startColor",
self.logo.gradient.start_color,
)
root.set(
f"{{{base['xml_ns']['android']}}}endColor", self.logo.gradient.end_color
)
for el in root.findall("path", namespaces=base["xml_ns"]):
name = el.get(f"{{{base['xml_ns']['android']}}}name")
if name == "path":
el.set(f"{{{base['xml_ns']['android']}}}fillColor", config.colors.secondary)
elif name in ["path_1", "path_2"]:
el.set(f"{{{base['xml_ns']['android']}}}fillColor", config.logo.ears_color)
# Сохранение
tree.write(
file_path, pretty_print=True, xml_declaration=True, encoding="utf-8"
)
# Сохранение
tree.write(file_path, pretty_print=True, xml_declaration=True, encoding="utf-8")
# Замена анимации лого
file_path = (
f"./decompiled/res/drawable{drawable_type}/$logo_splash_anim__0.xml"
)
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()
parser = etree.XMLParser(remove_blank_text=True)
tree = etree.parse(file_path, parser)
root = tree.getroot()
for el in root.findall("path", namespaces=base["xml_ns"]):
name = el.get(f"{{{base['xml_ns']['android']}}}name")
if name == "path":
el.set(
f"{{{base['xml_ns']['android']}}}fillColor",
self.colors.secondary,
)
elif name in ["path_1", "path_2"]:
el.set(
f"{{{base['xml_ns']['android']}}}fillColor",
self.logo.ears_color,
)
# Замена атрибутов значениями из конфигурации
root.set(f"{{{base['xml_ns']['android']}}}angle", str(config.logo.gradient.angle))
items = root.findall("item", namespaces=base['xml_ns'])
assert len(items) == 2
items[0].set(f"{{{base['xml_ns']['android']}}}color", config.logo.gradient.start_color)
items[1].set(f"{{{base['xml_ns']['android']}}}color", config.logo.gradient.end_color)
# Сохранение
tree.write(
file_path, pretty_print=True, xml_declaration=True, encoding="utf-8"
)
# Сохранение
tree.write(file_path, pretty_print=True, xml_declaration=True, encoding="utf-8")
for filename in ["$ic_launcher_foreground__0", "$ic_banner_foreground__0"]:
file_path = f"./decompiled/res/drawable-v24/{filename}.xml"
# Добаление новых цветов для темы
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:])
parser = etree.XMLParser(remove_blank_text=True)
tree = etree.parse(file_path, parser)
root = tree.getroot()
# Замена цветов
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:])
# Замена атрибутов значениями из конфигурации
root.set(
f"{{{base['xml_ns']['android']}}}angle", str(self.logo.gradient.angle)
)
items = root.findall("item", namespaces=base["xml_ns"])
assert len(items) == 2
items[0].set(
f"{{{base['xml_ns']['android']}}}color", self.logo.gradient.start_color
)
items[1].set(
f"{{{base['xml_ns']['android']}}}color", self.logo.gradient.end_color
)
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:])
# Сохранение
tree.write(
file_path, pretty_print=True, xml_declaration=True, encoding="utf-8"
)
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:])
# Добаление новых цветов для темы
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:],
)
return True
# Замена цветов
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
+83 -75
View File
@@ -8,92 +8,100 @@
}
"""
priority = 0
# imports
__author__ = "wowlikon <wowlikon@gmail.com>"
__version__ = "1.0.0"
import os
import shutil
from tqdm import tqdm
from typing import Any, Dict
from lxml import etree
from pydantic import Field
from typing import Dict, Any
from utils.config import PatchConfig
from tqdm import tqdm
#Config
class Config(PatchConfig):
from utils.config import PatchTemplate
class Patch(PatchTemplate):
priority: int = Field(frozen=True, exclude=True, default=0)
replace: bool = Field(True, description="Менять местами лайк/дизлайк")
custom_icons: bool = Field(True, description="Кастомные иконки")
icon_size: str = Field("18.0dip", description="Размер иконки")
# Patch
def apply(config, base: Dict[str, Any]) -> bool:
file_path = "./decompiled/res/layout/item_comment.xml"
parser = etree.XMLParser(remove_blank_text=True)
tree = etree.parse(file_path, parser)
root = tree.getroot()
def apply(self, base: Dict[str, Any]) -> bool:
file_path = "./decompiled/res/layout/item_comment.xml"
parser = etree.XMLParser(remove_blank_text=True)
tree = etree.parse(file_path, parser)
root = tree.getroot()
tqdm.write("Меняем размер иконок лайка и дизлайка...")
for icon in root.xpath(
".//*[@android:id='@id/votePlusInactive']//ImageView | "
".//*[@android:id='@id/votePlusActive']//ImageView | "
".//*[@android:id='@id/voteMinusInactive']//ImageView | "
".//*[@android:id='@id/voteMinusActive']//ImageView",
namespaces=base['xml_ns'],
):
icon.set(f"{{{base['xml_ns']['android']}}}layout_width", config.icon_size)
# icon.set(f"{{{base['xml_ns']['android']}}}layout_height", config.icon_size)
if config.replace:
tqdm.write("Меняем местами лайк и дизлайк комментария...")
containers = root.xpath(
".//LinearLayout[.//*[@android:id='@id/voteMinus'] and .//*[@android:id='@id/votePlus']]",
namespaces=base["xml_ns"],
)
found = False
for container in containers:
children = list(container)
vote_plus = None
vote_minus = None
for ch in children:
cid = ch.get(f'{{{base["xml_ns"]["android"]}}}id')
if cid == "@id/votePlus":
vote_plus = ch
elif cid == "@id/voteMinus":
vote_minus = ch
if vote_plus is not None and vote_minus is not None:
found = True
i_plus = children.index(vote_plus)
i_minus = children.index(vote_minus)
children[i_plus], children[i_minus] = children[i_minus], children[i_plus]
container[:] = children
tqdm.write("Кнопки лайк и дизлайк поменялись местами.")
break
if not found:
tqdm.write("Не удалось найти оба узла votePlus/voteMinus даже в общих LinearLayout.")
if config.custom_icons:
tqdm.write("Заменяем иконки лайка и дизлайка на кастомные...")
for suffix in ["up", "up_40", "down", "down_40"]:
shutil.copy(
f"./resources/ic_chevron_{suffix}.xml",
f"./decompiled/res/drawable/ic_chevron_{suffix}.xml",
)
for inactive in root.xpath(
".//*[@android:id='@id/votePlusInactive'] | .//*[@android:id='@id/voteMinusInactive']",
tqdm.write("Меняем размер иконок лайка и дизлайка...")
for icon in root.xpath(
".//*[@android:id='@id/votePlusInactive']//ImageView | "
".//*[@android:id='@id/votePlusActive']//ImageView | "
".//*[@android:id='@id/voteMinusInactive']//ImageView | "
".//*[@android:id='@id/voteMinusActive']//ImageView",
namespaces=base["xml_ns"],
):
for img in inactive.xpath(".//ImageView[@android:src]", namespaces=base["xml_ns"]):
src = img.get(f'{{{base["xml_ns"]["android"]}}}src', "")
if src.startswith("@drawable/") and not src.endswith("_40"):
img.set(f'{{{base["xml_ns"]["android"]}}}src', src + "_40")
icon.set(f"{{{base['xml_ns']['android']}}}layout_width", self.icon_size)
# icon.set(f"{{{base['xml_ns']['android']}}}layout_height", self.icon_size)
# Сохраняем
tree.write(file_path, encoding="utf-8", xml_declaration=True, pretty_print=True)
if self.replace:
tqdm.write("Меняем местами лайк и дизлайк комментария...")
return True
containers = root.xpath(
".//LinearLayout[.//*[@android:id='@id/voteMinus'] and .//*[@android:id='@id/votePlus']]",
namespaces=base["xml_ns"],
)
found = False
for container in containers:
children = list(container)
vote_plus = None
vote_minus = None
for ch in children:
cid = ch.get(f'{{{base["xml_ns"]["android"]}}}id')
if cid == "@id/votePlus":
vote_plus = ch
elif cid == "@id/voteMinus":
vote_minus = ch
if vote_plus is not None and vote_minus is not None:
found = True
i_plus = children.index(vote_plus)
i_minus = children.index(vote_minus)
children[i_plus], children[i_minus] = (
children[i_minus],
children[i_plus],
)
container[:] = children
tqdm.write("Кнопки лайк и дизлайк поменялись местами.")
break
if not found:
tqdm.write(
"Не удалось найти оба узла votePlus/voteMinus даже в общих LinearLayout."
)
if self.custom_icons:
tqdm.write("Заменяем иконки лайка и дизлайка на кастомные...")
for suffix in ["up", "up_40", "down", "down_40"]:
shutil.copy(
f"./resources/ic_chevron_{suffix}.xml",
f"./decompiled/res/drawable/ic_chevron_{suffix}.xml",
)
for inactive in root.xpath(
".//*[@android:id='@id/votePlusInactive'] | .//*[@android:id='@id/voteMinusInactive']",
namespaces=base["xml_ns"],
):
for img in inactive.xpath(
".//ImageView[@android:src]", namespaces=base["xml_ns"]
):
src = img.get(f'{{{base["xml_ns"]["android"]}}}src', "")
if src.startswith("@drawable/") and not src.endswith("_40"):
img.set(f'{{{base["xml_ns"]["android"]}}}src', src + "_40")
# Сохраняем
tree.write(file_path, encoding="utf-8", xml_declaration=True, pretty_print=True)
return True
+285 -260
View File
@@ -26,283 +26,308 @@
}
"""
priority = -1
# imports
__author__ = "Kentai Radiquum <radiquum@gmail.com>"
__version__ = "1.0.0"
import os
import shutil
import subprocess
from tqdm import tqdm
from pydantic import Field
from typing import Dict, List, Any
from typing import Any, Dict, List
from utils.config import PatchConfig
from pydantic import Field
from tqdm import tqdm
from utils.config import PatchTemplate
from utils.smali_parser import get_smali_lines, save_smali_lines
#Config
class Config(PatchConfig):
remove_language_files: bool = Field(True, description="Удаляет все языки кроме русского и английского")
remove_AI_voiceover: bool = Field(True, description="Заменяет ИИ озвучку маскота аниксы пустыми mp3 файлами")
remove_debug_lines: bool = Field(False, description="Удаляет строки `.line n` из smali файлов использованные для дебага")
remove_drawable_files: bool = Field(False, description="Удаляет неиспользованные drawable-* из директории decompiled/res")
remove_unknown_files: bool = Field(True, description="Удаляет файлы из директории decompiled/unknown")
remove_unknown_files_keep_dirs: List[str] = Field(["META-INF", "kotlin"], description="Оставляет указанные директории в decompiled/unknown")
compress_png_files: bool = Field(True, description="Сжимает PNG в директории decompiled/res")
# Patch
def remove_unknown_files(config: Config, base: Dict[str, Any]):
path = "./decompiled/unknown"
items = os.listdir(path)
for item in items:
item_path = f"{path}/{item}"
if os.path.isfile(item_path):
os.remove(item_path)
if base.get("verbose", False):
tqdm.write(f"Удалён файл: {item_path}")
elif os.path.isdir(item_path):
if item not in config.remove_unknown_files_keep_dirs:
shutil.rmtree(item_path)
class Patch(PatchTemplate):
priority: int = Field(frozen=True, exclude=True, default=-1)
remove_language_files: bool = Field(
True, description="Удаляет все языки кроме русского и английского"
)
remove_AI_voiceover: bool = Field(
True, description="Заменяет ИИ озвучку маскота аниксы пустыми mp3 файлами"
)
remove_debug_lines: bool = Field(
False,
description="Удаляет строки `.line n` из smali файлов использованные для дебага",
)
remove_drawable_files: bool = Field(
False,
description="Удаляет неиспользованные drawable-* из директории decompiled/res",
)
remove_unknown_files: bool = Field(
True, description="Удаляет файлы из директории decompiled/unknown"
)
remove_unknown_files_keep_dirs: List[str] = Field(
["META-INF", "kotlin"],
description="Оставляет указанные директории в decompiled/unknown",
)
compress_png_files: bool = Field(
True, description="Сжимает PNG в директории decompiled/res"
)
def do_remove_unknown_files(self, base: Dict[str, Any]):
path = "./decompiled/unknown"
items = os.listdir(path)
for item in items:
item_path = f"{path}/{item}"
if os.path.isfile(item_path):
os.remove(item_path)
if base.get("verbose", False):
tqdm.write(f"Удалёна директория: {item_path}")
return True
def remove_debug_lines(config: Dict[str, Any]):
for root, _, files in os.walk("./decompiled"):
for filename in files:
file_path = os.path.join(root, filename)
if os.path.isfile(file_path) and filename.endswith(".smali"):
file_content = get_smali_lines(file_path)
new_content = []
for line in file_content:
if line.find(".line") >= 0:
continue
new_content.append(line)
save_smali_lines(file_path, new_content)
if config.get("verbose", False):
tqdm.write(f"Удалены дебаг линии из: {file_path}")
return True
def compress_png(config: Dict[str, Any], png_path: str):
try:
assert subprocess.run(
[
"pngquant",
"--force",
"--ext", ".png",
"--quality=65-90",
png_path,
],
capture_output=True,
).returncode in [0, 99]
if config.get("verbose", False):
tqdm.write(f"Сжат файл PNG: {png_path}")
tqdm.write(f"Удалён файл: {item_path}")
elif os.path.isdir(item_path):
if item not in self.remove_unknown_files_keep_dirs:
shutil.rmtree(item_path)
if base.get("verbose", False):
tqdm.write(f"Удалёна директория: {item_path}")
return True
except subprocess.CalledProcessError as e:
tqdm.write(f"Ошибка при сжатии {png_path}: {e}")
return False
def do_remove_debug_lines(self, config: Dict[str, Any]):
for root, _, files in os.walk("./decompiled"):
for filename in files:
file_path = os.path.join(root, filename)
if os.path.isfile(file_path) and filename.endswith(".smali"):
file_content = get_smali_lines(file_path)
new_content = []
for line in file_content:
if line.find(".line") >= 0:
continue
new_content.append(line)
save_smali_lines(file_path, new_content)
if config.get("verbose", False):
tqdm.write(f"Удалены дебаг линии из: {file_path}")
return True
def compress_png(self, config: Dict[str, Any], png_path: str):
try:
assert subprocess.run(
[
"pngquant",
"--force",
"--ext",
".png",
"--quality=65-90",
png_path,
],
capture_output=True,
).returncode in [0, 99]
if config.get("verbose", False):
tqdm.write(f"Сжат файл PNG: {png_path}")
return True
except subprocess.CalledProcessError as e:
tqdm.write(f"Ошибка при сжатии {png_path}: {e}")
return False
def do_compress_png_files(self, config: Dict[str, Any]):
compressed = []
for root, _, files in os.walk("./decompiled"):
for file in files:
if file.lower().endswith(".png"):
self.compress_png(config, f"{root}/{file}")
compressed.append(f"{root}/{file}")
return len(compressed) > 0 and any(compressed)
def do_remove_AI_voiceover(self, config: Dict[str, Any]):
blank = "./resources/blank.mp3"
path = "./decompiled/res/raw"
files = [
"reputation_1.mp3",
"reputation_2.mp3",
"reputation_3.mp3",
"sound_beta_1.mp3",
"sound_create_blog_1.mp3",
"sound_create_blog_2.mp3",
"sound_create_blog_3.mp3",
"sound_create_blog_4.mp3",
"sound_create_blog_5.mp3",
"sound_create_blog_6.mp3",
"sound_create_blog_reputation_1.mp3",
"sound_create_blog_reputation_2.mp3",
"sound_create_blog_reputation_3.mp3",
"sound_create_blog_reputation_4.mp3",
"sound_create_blog_reputation_5.mp3",
"sound_create_blog_reputation_6.mp3",
]
def compress_png_files(config: Dict[str, Any]):
compressed = []
for root, _, files in os.walk("./decompiled"):
for file in files:
if file.lower().endswith(".png"):
compress_png(config, f"{root}/{file}")
compressed.append(f"{root}/{file}")
return len(compressed) > 0 and any(compressed)
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_AI_voiceover(config: Dict[str, Any]):
blank = "./resources/blank.mp3"
path = "./decompiled/res/raw"
files = [
"reputation_1.mp3",
"reputation_2.mp3",
"reputation_3.mp3",
"sound_beta_1.mp3",
"sound_create_blog_1.mp3",
"sound_create_blog_2.mp3",
"sound_create_blog_3.mp3",
"sound_create_blog_4.mp3",
"sound_create_blog_5.mp3",
"sound_create_blog_6.mp3",
"sound_create_blog_reputation_1.mp3",
"sound_create_blog_reputation_2.mp3",
"sound_create_blog_reputation_3.mp3",
"sound_create_blog_reputation_4.mp3",
"sound_create_blog_reputation_5.mp3",
"sound_create_blog_reputation_6.mp3",
]
def do_remove_language_files(self, config: Dict[str, Any]):
path = "./decompiled/res"
folders = [
"values-af",
"values-am",
"values-ar",
"values-as",
"values-az",
"values-b+es+419",
"values-b+sr+Latn",
"values-be",
"values-bg",
"values-bn",
"values-bs",
"values-ca",
"values-cs",
"values-da",
"values-de",
"values-el",
"values-en-rAU",
"values-en-rCA",
"values-en-rGB",
"values-en-rIN",
"values-en-rXC",
"values-es",
"values-es-rGT",
"values-es-rUS",
"values-et",
"values-eu",
"values-fa",
"values-fi",
"values-fr",
"values-fr-rCA",
"values-gl",
"values-gu",
"values-hi",
"values-hr",
"values-hu",
"values-hy",
"values-in",
"values-is",
"values-it",
"values-iw",
"values-ja",
"values-ka",
"values-kk",
"values-km",
"values-kn",
"values-ko",
"values-ky",
"values-lo",
"values-lt",
"values-lv",
"values-mk",
"values-ml",
"values-mn",
"values-mr",
"values-ms",
"values-my",
"values-nb",
"values-ne",
"values-nl",
"values-or",
"values-pa",
"values-pl",
"values-pt",
"values-pt-rBR",
"values-pt-rPT",
"values-ro",
"values-si",
"values-sk",
"values-sl",
"values-sq",
"values-sr",
"values-sv",
"values-sw",
"values-ta",
"values-te",
"values-th",
"values-tl",
"values-tr",
"values-uk",
"values-ur",
"values-uz",
"values-vi",
"values-zh",
"values-zh-rCN",
"values-zh-rHK",
"values-zh-rTW",
"values-zu",
"values-watch",
]
for 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}")
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
return True
def do_remove_drawable_files(self, config: Dict[str, Any]):
path = "./decompiled/res"
folders = [
"drawable-en-hdpi",
"drawable-en-ldpi",
"drawable-en-mdpi",
"drawable-en-xhdpi",
"drawable-en-xxhdpi",
"drawable-en-xxxhdpi",
"drawable-ldrtl-hdpi",
"drawable-ldrtl-mdpi",
"drawable-ldrtl-xhdpi",
"drawable-ldrtl-xxhdpi",
"drawable-ldrtl-xxxhdpi",
"drawable-tr-anydpi",
"drawable-tr-hdpi",
"drawable-tr-ldpi",
"drawable-tr-mdpi",
"drawable-tr-xhdpi",
"drawable-tr-xxhdpi",
"drawable-tr-xxxhdpi",
"drawable-watch",
"layout-watch",
]
for folder in folders:
if os.path.exists(f"{path}/{folder}"):
shutil.rmtree(f"{path}/{folder}")
if config.get("verbose", False):
tqdm.write(f"Удалена директория: {path}/{folder}")
return True
def remove_language_files(config: Dict[str, Any]):
path = "./decompiled/res"
folders = [
"values-af",
"values-am",
"values-ar",
"values-as",
"values-az",
"values-b+es+419",
"values-b+sr+Latn",
"values-be",
"values-bg",
"values-bn",
"values-bs",
"values-ca",
"values-cs",
"values-da",
"values-de",
"values-el",
"values-en-rAU",
"values-en-rCA",
"values-en-rGB",
"values-en-rIN",
"values-en-rXC",
"values-es",
"values-es-rGT",
"values-es-rUS",
"values-et",
"values-eu",
"values-fa",
"values-fi",
"values-fr",
"values-fr-rCA",
"values-gl",
"values-gu",
"values-hi",
"values-hr",
"values-hu",
"values-hy",
"values-in",
"values-is",
"values-it",
"values-iw",
"values-ja",
"values-ka",
"values-kk",
"values-km",
"values-kn",
"values-ko",
"values-ky",
"values-lo",
"values-lt",
"values-lv",
"values-mk",
"values-ml",
"values-mn",
"values-mr",
"values-ms",
"values-my",
"values-nb",
"values-ne",
"values-nl",
"values-or",
"values-pa",
"values-pl",
"values-pt",
"values-pt-rBR",
"values-pt-rPT",
"values-ro",
"values-si",
"values-sk",
"values-sl",
"values-sq",
"values-sr",
"values-sv",
"values-sw",
"values-ta",
"values-te",
"values-th",
"values-tl",
"values-tr",
"values-uk",
"values-ur",
"values-uz",
"values-vi",
"values-zh",
"values-zh-rCN",
"values-zh-rHK",
"values-zh-rTW",
"values-zu",
"values-watch",
]
def apply(self, base: Dict[str, Any]) -> bool:
actions = [
(
self.remove_unknown_files,
"Удаление неизвестных файлов...",
self.do_remove_unknown_files,
),
(
self.remove_drawable_files,
"Удаление директорий drawable-xx...",
self.do_remove_drawable_files,
),
(
self.compress_png_files,
"Сжатие PNG файлов...",
self.do_compress_png_files,
),
(
self.remove_language_files,
"Удаление языков...",
self.do_remove_language_files,
),
(
self.remove_AI_voiceover,
"Удаление ИИ озвучки...",
self.do_remove_AI_voiceover,
),
(
self.remove_debug_lines,
"Удаление дебаг линий...",
self.do_remove_debug_lines,
),
]
for 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
for enabled, message, action in actions:
if enabled:
tqdm.write(message)
action(base)
def remove_drawable_files(config: Dict[str, Any]):
path = "./decompiled/res"
folders = [
"drawable-en-hdpi",
"drawable-en-ldpi",
"drawable-en-mdpi",
"drawable-en-xhdpi",
"drawable-en-xxhdpi",
"drawable-en-xxxhdpi",
"drawable-ldrtl-hdpi",
"drawable-ldrtl-mdpi",
"drawable-ldrtl-xhdpi",
"drawable-ldrtl-xxhdpi",
"drawable-ldrtl-xxxhdpi",
"drawable-tr-anydpi",
"drawable-tr-hdpi",
"drawable-tr-ldpi",
"drawable-tr-mdpi",
"drawable-tr-xhdpi",
"drawable-tr-xxhdpi",
"drawable-tr-xxxhdpi",
"drawable-watch",
"layout-watch",
]
for folder in folders:
if os.path.exists(f"{path}/{folder}"):
shutil.rmtree(f"{path}/{folder}")
if config.get("verbose", False):
tqdm.write(f"Удалена директория: {path}/{folder}")
return True
def apply(config: Config, base: Dict[str, Any]) -> bool:
if config.remove_unknown_files:
tqdm.write(f"Удаление неизвестных файлов...")
remove_unknown_files(config, base)
if config.remove_drawable_files:
tqdm.write(f"Удаление директорий drawable-xx...")
remove_drawable_files(base)
if config.compress_png_files:
tqdm.write(f"Сжатие PNG файлов...")
compress_png_files(base)
if config.remove_language_files:
tqdm.write(f"Удаление языков...")
remove_language_files(base)
if config.remove_AI_voiceover:
tqdm.write(f"Удаление ИИ озвучки...")
remove_AI_voiceover(base)
if config.remove_debug_lines:
tqdm.write(f"Удаление дебаг линий...")
remove_debug_lines(base)
return True
return True
+33 -34
View File
@@ -5,44 +5,43 @@
}
"""
priority = 0
# imports
__author__ = "Kentai Radiquum <radiquum@gmail.com>"
__version__ = "1.0.0"
import textwrap
from tqdm import tqdm
from typing import Dict, Any
from typing import Any, Dict
from utils.config import PatchConfig
from utils.smali_parser import (
find_smali_method_end,
find_smali_method_start,
get_smali_lines,
replace_smali_method_body,
)
from pydantic import Field
from utils.config import PatchTemplate
from utils.smali_parser import (find_smali_method_end, find_smali_method_start,
get_smali_lines, replace_smali_method_body)
#Config
class Config(PatchConfig): ...
class Patch(PatchTemplate):
priority: int = Field(frozen=True, exclude=True, default=0)
def apply(self, base: Dict[str, Any]) -> bool:
path = "./decompiled/smali_classes2/com/swiftsoft/anixartd/Prefs.smali"
replacement = [
f"\t{line}\n"
for line in textwrap.dedent(
"""\
.locals 0
const/4 p0, 0x1
return p0
"""
).splitlines()
]
# Patch
def apply(config: Config, base: Dict[str, Any]) -> bool:
replacement = [f'\t{line}\n' for line in textwrap.dedent("""\
.locals 0
const/4 p0, 0x1
return p0
""").splitlines()]
lines = get_smali_lines(path)
for index, line in enumerate(lines):
if line.find("IS_SPONSOR") >= 0:
method_start = find_smali_method_start(lines, index)
method_end = find_smali_method_end(lines, index)
new_content = replace_smali_method_body(
lines, method_start, method_end, replacement
)
path = "./decompiled/smali_classes2/com/swiftsoft/anixartd/Prefs.smali"
lines = get_smali_lines(path)
for index, line in enumerate(lines):
if line.find("IS_SPONSOR") >= 0:
method_start = find_smali_method_start(lines, index)
method_end = find_smali_method_end(lines, index)
new_content = replace_smali_method_body(
lines, method_start, method_end, replacement
)
with open(path, "w", encoding="utf-8") as file:
file.writelines(new_content)
return True
with open(path, "w", encoding="utf-8") as file:
file.writelines(new_content)
return True
+40 -36
View File
@@ -5,48 +5,52 @@
}
"""
priority = 0
# imports
__author__ = "Kentai Radiquum <radiquum@gmail.com>"
__version__ = "1.0.0"
import os
from tqdm import tqdm
from lxml import etree
from typing import Dict, Any
from typing import Any, Dict
from utils.config import PatchConfig
from lxml import etree
from pydantic import Field
from tqdm import tqdm
from utils.config import PatchTemplate
from utils.smali_parser import get_smali_lines, save_smali_lines
#Config
class Config(PatchConfig): ...
# Patch
def apply(config: Config, base: Dict[str, Any]) -> bool:
attributes = [
"paddingTop",
"paddingBottom",
"paddingStart",
"paddingEnd",
"layout_width",
"layout_height",
"layout_marginTop",
"layout_marginBottom",
"layout_marginStart",
"layout_marginEnd",
]
class Patch(PatchTemplate):
priority: int = Field(frozen=True, exclude=True, default=0)
beta_banner_xml = "./decompiled/res/layout/item_beta.xml"
if os.path.exists(beta_banner_xml):
parser = etree.XMLParser(remove_blank_text=True)
tree = etree.parse(beta_banner_xml, parser)
root = tree.getroot()
def apply(self, base: Dict[str, Any]) -> bool:
beta_banner_xml = "./decompiled/res/layout/item_beta.xml"
attributes = [
"paddingTop",
"paddingBottom",
"paddingStart",
"paddingEnd",
"layout_width",
"layout_height",
"layout_marginTop",
"layout_marginBottom",
"layout_marginStart",
"layout_marginEnd",
]
for attr in attributes:
if base.get("verbose", False):
tqdm.write(f"set {attr} = 0.0dip")
root.set(f"{{{base['xml_ns']['android']}}}{attr}", "0.0dip")
if os.path.exists(beta_banner_xml):
parser = etree.XMLParser(remove_blank_text=True)
tree = etree.parse(beta_banner_xml, parser)
root = tree.getroot()
tree.write(
beta_banner_xml, pretty_print=True, xml_declaration=True, encoding="utf-8"
)
for attr in attributes:
if base.get("verbose", False):
tqdm.write(f"set {attr} = 0.0dip")
root.set(f"{{{base['xml_ns']['android']}}}{attr}", "0.0dip")
return True
tree.write(
beta_banner_xml,
pretty_print=True,
xml_declaration=True,
encoding="utf-8",
)
return True
+92 -94
View File
@@ -6,116 +6,114 @@
}
"""
priority = -1
# imports
__author__ = "wowlikon <wowlikon@gmail.com>"
__version__ = "1.0.0"
import os
from tqdm import tqdm
from typing import Any, Dict
from lxml import etree
from typing import Dict, Any
from pydantic import Field
from utils.config import PatchConfig
from utils.config import PatchTemplate
#Config
class Config(PatchConfig):
class Patch(PatchTemplate):
priority: int = Field(frozen=True, exclude=True, default=-1)
package_name: str = Field("com.wowlikon.anixart", description="Название пакета")
# Patch
def rename_dir(src, dst):
os.makedirs(os.path.dirname(dst), exist_ok=True)
os.rename(src, dst)
def rename_dir(self, src, dst):
os.makedirs(os.path.dirname(dst), exist_ok=True)
os.rename(src, dst)
def apply(self, base: Dict[str, Any]) -> bool:
for root, dirs, files in os.walk("./decompiled"):
for filename in files:
file_path = os.path.join(root, filename)
def apply(config: Config, base: Dict[str, Any]) -> bool:
for root, dirs, files in os.walk("./decompiled"):
for filename in files:
file_path = os.path.join(root, filename)
if os.path.isfile(file_path):
try: # Изменяем имя пакета в файлах
with open(file_path, "r", encoding="utf-8") as file:
file_contents = file.read()
if os.path.isfile(file_path):
try: # Изменяем имя пакета в файлах
with open(file_path, "r", encoding="utf-8") as file:
file_contents = file.read()
new_contents = file_contents.replace(
"com.swiftsoft.anixartd", self.package_name
)
new_contents = new_contents.replace(
"com/swiftsoft/anixartd",
self.package_name.replace(".", "/"),
).replace(
"com/swiftsoft",
"/".join(self.package_name.split(".")[:2]),
)
with open(file_path, "w", encoding="utf-8") as file:
file.write(new_contents)
except:
pass
new_contents = file_contents.replace(
"com.swiftsoft.anixartd", config.package_name
)
new_contents = new_contents.replace(
"com/swiftsoft/anixartd",
config.package_name.replace(".", "/"),
).replace(
"com/swiftsoft",
"/".join(config.package_name.split(".")[:2]),
)
with open(file_path, "w", encoding="utf-8") as file:
file.write(new_contents)
except:
pass
# Изменяем названия папок
if os.path.exists("./decompiled/smali/com/swiftsoft/anixartd"):
self.rename_dir(
"./decompiled/smali/com/swiftsoft/anixartd",
os.path.join(
"./decompiled", "smali", self.package_name.replace(".", "/")
),
)
if os.path.exists("./decompiled/smali_classes2/com/swiftsoft/anixartd"):
self.rename_dir(
"./decompiled/smali_classes2/com/swiftsoft/anixartd",
os.path.join(
"./decompiled",
"smali_classes2",
self.package_name.replace(".", "/"),
),
)
if os.path.exists("./decompiled/smali_classes4/com/swiftsoft"):
self.rename_dir(
"./decompiled/smali_classes4/com/swiftsoft",
os.path.join(
"./decompiled",
"smali_classes4",
"/".join(self.package_name.split(".")[:2]),
),
)
# Изменяем названия папок
if os.path.exists("./decompiled/smali/com/swiftsoft/anixartd"):
rename_dir(
"./decompiled/smali/com/swiftsoft/anixartd",
os.path.join(
"./decompiled", "smali", config.package_name.replace(".", "/")
),
)
if os.path.exists("./decompiled/smali_classes2/com/swiftsoft/anixartd"):
rename_dir(
"./decompiled/smali_classes2/com/swiftsoft/anixartd",
os.path.join(
"./decompiled",
"smali_classes2",
config.package_name.replace(".", "/"),
),
)
if os.path.exists("./decompiled/smali_classes4/com/swiftsoft"):
rename_dir(
"./decompiled/smali_classes4/com/swiftsoft",
os.path.join(
"./decompiled",
"smali_classes4",
"/".join(config.package_name.split(".")[:2]),
),
)
# rename_dir(
# "./decompiled/smali_classes3/com/swiftsoft/anixartd",
# os.path.join(
# "./decompiled",
# "smali_classes3",
# config["new_package_name"].replace(".", "/"),
# ),
# )
# rename_dir(
# "./decompiled/smali_classes3/com/swiftsoft/anixartd",
# os.path.join(
# "./decompiled",
# "smali_classes3",
# config["new_package_name"].replace(".", "/"),
# ),
# )
# Замена названия пакета для smali_classes4
for root, dirs, files in os.walk("./decompiled/smali_classes4/"):
for filename in files:
file_path = os.path.join(root, filename)
# Замена названия пакета для smali_classes4
for root, dirs, files in os.walk("./decompiled/smali_classes4/"):
for filename in files:
file_path = os.path.join(root, filename)
if os.path.isfile(file_path):
try:
with open(file_path, "r", encoding="utf-8") as file:
file_contents = file.read()
if os.path.isfile(file_path):
try:
with open(file_path, "r", encoding="utf-8") as file:
file_contents = file.read()
new_contents = file_contents.replace(
"com/swiftsoft",
"/".join(self.package_name.split(".")[:-1]),
)
with open(file_path, "w", encoding="utf-8") as file:
file.write(new_contents)
except:
pass
new_contents = file_contents.replace(
"com/swiftsoft",
"/".join(config.package_name.split(".")[:-1]),
)
with open(file_path, "w", encoding="utf-8") as file:
file.write(new_contents)
except:
pass
# Скрытие входа по Google и VK (НЕ РАБОТАЮТ В МОДАХ)
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()
# Скрытие входа по Google и VK (НЕ РАБОТАЮТ В МОДАХ)
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"{{{base['xml_ns']['android']}}}visibility", "gone")
last_linear = root.xpath("//LinearLayout/LinearLayout[4]")[0]
last_linear.set(f"{{{base['xml_ns']['android']}}}visibility", "gone")
tree.write(file_path, pretty_print=True, xml_declaration=True, encoding="utf-8")
tree.write(file_path, pretty_print=True, xml_declaration=True, encoding="utf-8")
return True
return True
+42 -37
View File
@@ -7,56 +7,61 @@
}
"""
priority = 0
__author__ = "wowlikon <wowlikon@gmail.com>"
__version__ = "1.0.0"
from typing import Any, Dict, List
# imports
from lxml import etree
from tqdm import tqdm
from typing import Dict, List, Any
from pydantic import Field
from tqdm import tqdm
from utils.config import PatchConfig
from utils.config import PatchTemplate
#Config
class Config(PatchConfig):
items: List[str] = Field(["home", "discover", "feed", "bookmarks", "profile"], description="Список элементов в панели навигации")
# Patch
def apply(config: Config, base: Dict[str, Any]) -> bool:
file_path = "./decompiled/res/menu/bottom.xml"
class Patch(PatchTemplate):
priority: int = Field(frozen=True, exclude=True, default=0)
items: List[str] = Field(
["home", "discover", "feed", "bookmarks", "profile"],
description="Список элементов в панели навигации",
)
parser = etree.XMLParser(remove_blank_text=True)
tree = etree.parse(file_path, parser)
root = tree.getroot()
def apply(self, base: Dict[str, Any]) -> bool:
file_path = "./decompiled/res/menu/bottom.xml"
# Получение элементов панели навигации
items = root.findall("item", namespaces=base['xml_ns'])
parser = etree.XMLParser(remove_blank_text=True)
tree = etree.parse(file_path, parser)
root = tree.getroot()
def get_id_suffix(item):
full_id = item.get(f"{{{base['xml_ns']['android']}}}id")
return full_id.split("tab_")[-1] if full_id else None
# Получение элементов панели навигации
items = root.findall("item", namespaces=base["xml_ns"])
items_by_id = {get_id_suffix(item): item for item in items}
existing_order = [get_id_suffix(item) for item in items]
def get_id_suffix(item):
full_id = item.get(f"{{{base['xml_ns']['android']}}}id")
return full_id.split("tab_")[-1] if full_id else None
# Размещение в новом порядке
ordered_items = []
for key in config.items:
if key in items_by_id:
ordered_items.append(items_by_id[key])
items_by_id = {get_id_suffix(item): item for item in items}
existing_order = [get_id_suffix(item) for item in items]
# Если есть не указанные в конфиге они помещаются в конец списка
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)
# Размещение в новом порядке
ordered_items = []
for key in self.items:
if key in items_by_id:
ordered_items.append(items_by_id[key])
for i in root.findall("item", namespaces=base['xml_ns']):
root.remove(i)
# Если есть не указанные в конфиге они помещаются в конец списка
extra = [i for i in items if get_id_suffix(i) not in self.items]
if extra:
tqdm.write(
"⚠Найдены лишние элементы: " + str([get_id_suffix(i) for i in extra])
)
ordered_items.extend(extra)
for item in ordered_items:
root.append(item)
for i in root.findall("item", namespaces=base["xml_ns"]):
root.remove(i)
tree.write(file_path, pretty_print=True, xml_declaration=True, encoding="utf-8")
for item in ordered_items:
root.append(item)
return True
tree.write(file_path, pretty_print=True, xml_declaration=True, encoding="utf-8")
return True
+26 -23
View File
@@ -5,38 +5,41 @@
}
"""
priority = 0
__author__ = "wowlikon <wowlikon@gmail.com>"
__version__ = "1.0.0"
from typing import Any, Dict
# imports
from tqdm import tqdm
from lxml import etree
from typing import Dict, Any
from pydantic import Field
from utils.config import PatchConfig
from utils.config import PatchTemplate
#Config
class Config(PatchConfig): ...
# Patch
def apply(config: Config, base: Dict[str, Any]) -> bool:
class Patch(PatchTemplate):
priority: int = Field(frozen=True, exclude=True, default=0)
file_path = "./decompiled/res/layout/release_info.xml"
def apply(self, base: Dict[str, Any]) -> bool:
file_path = "./decompiled/res/layout/release_info.xml"
parser = etree.XMLParser(remove_blank_text=True)
tree = etree.parse(file_path, parser)
root = tree.getroot()
parser = etree.XMLParser(remove_blank_text=True)
tree = etree.parse(file_path, parser)
root = tree.getroot()
# Список тегов, к которым нужно добавить атрибут
tags = ["TextView", "at.blogc.android.views.ExpandableTextView"]
# Список тегов, к которым нужно добавить атрибут
tags = ["TextView", "at.blogc.android.views.ExpandableTextView"]
for tag in tags:
for element in root.findall(f".//{tag}", namespaces=base["xml_ns"]):
# Проверяем, нет ли уже атрибута
if f"{{{base['xml_ns']['android']}}}textIsSelectable" not in element.attrib:
element.set(f"{{{base['xml_ns']['android']}}}textIsSelectable", "true")
for tag in tags:
for element in root.findall(f".//{tag}", namespaces=base["xml_ns"]):
# Проверяем, нет ли уже атрибута
if (
f"{{{base['xml_ns']['android']}}}textIsSelectable"
not in element.attrib
):
element.set(
f"{{{base['xml_ns']['android']}}}textIsSelectable", "true"
)
# Сохраняем
tree.write(file_path, encoding="utf-8", xml_declaration=True, pretty_print=True)
# Сохраняем
tree.write(file_path, encoding="utf-8", xml_declaration=True, pretty_print=True)
return True
return True
+104 -95
View File
@@ -20,119 +20,128 @@
}
"""
priority = 0
# imports
__author__ = "wowlikon <wowlikon@gmail.com>"
__version__ = "1.0.0"
import shutil
from typing import Any, Dict, List
from lxml import etree
from tqdm import tqdm
from typing import Dict, List, Any
from pydantic import Field
from utils.config import PatchConfig
from utils.config import PatchTemplate
from utils.public import insert_after_public
#Config
# Config
DEFAULT_MENU = {
"Мы в социальных сетях": [
{
"title": "wowlikon",
"description": "Разработчик",
"url": "https://t.me/wowlikon",
"icon": "@drawable/ic_custom_telegram",
"icon_space_reserved": "false"
},
{
"title": "Kentai Radiquum",
"description": "Разработчик",
"url": "https://t.me/radiquum",
"icon": "@drawable/ic_custom_telegram",
"icon_space_reserved": "false"
},
{
"title": "Мы в Telegram",
"description": "Подпишитесь на канал, чтобы быть в курсе последних новостей.",
"url": "https://t.me/http_teapod",
"icon": "@drawable/ic_custom_telegram",
"icon_space_reserved": "false"
}
],
"Прочее": [
{
"title": "Помочь проекту",
"description": "Вы можете помочь нам с идеями, написанием кода или тестированием.",
"url": "https://git.wowlikon.tech/anixart-mod",
"icon": "@drawable/ic_custom_crown",
"icon_space_reserved": "false"
}
]
"Мы в социальных сетях": [
{
"title": "wowlikon",
"description": "Разработчик",
"url": "https://t.me/wowlikon",
"icon": "@drawable/ic_custom_telegram",
"icon_space_reserved": "false",
},
{
"title": "Kentai Radiquum",
"description": "Разработчик",
"url": "https://t.me/radiquum",
"icon": "@drawable/ic_custom_telegram",
"icon_space_reserved": "false",
},
{
"title": "Мы в Telegram",
"description": "Подпишитесь на канал, чтобы быть в курсе последних новостей.",
"url": "https://t.me/http_teapod",
"icon": "@drawable/ic_custom_telegram",
"icon_space_reserved": "false",
},
],
"Прочее": [
{
"title": "Помочь проекту",
"description": "Вы можете помочь нам с идеями, написанием кода или тестированием.",
"url": "https://git.wowlikon.tech/anixart-mod",
"icon": "@drawable/ic_custom_crown",
"icon_space_reserved": "false",
}
],
}
class Config(PatchConfig):
class Patch(PatchTemplate):
priority: int = Field(frozen=True, exclude=True, default=0)
version: str = Field(" by wowlikon", description="Суффикс версии")
menu: Dict[str, List[Dict[str, str]]] = Field(DEFAULT_MENU, description="Меню")
# Patch
def make_category(ns, name, items):
cat = etree.Element("PreferenceCategory", nsmap=ns)
cat.set(f"{{{ns['android']}}}title", name)
cat.set(f"{{{ns['app']}}}iconSpaceReserved", "false")
def make_category(self, ns, name, items):
cat = etree.Element("PreferenceCategory", nsmap=ns)
cat.set(f"{{{ns['android']}}}title", name)
cat.set(f"{{{ns['app']}}}iconSpaceReserved", "false")
for item in items:
pref = etree.SubElement(cat, "Preference", nsmap=ns)
pref.set(f"{{{ns['android']}}}title", item["title"])
pref.set(f"{{{ns['android']}}}summary", item["description"])
pref.set(f"{{{ns['app']}}}icon", item["icon"])
pref.set(f"{{{ns['app']}}}iconSpaceReserved", item["icon_space_reserved"])
for item in items:
pref = etree.SubElement(cat, "Preference", nsmap=ns)
pref.set(f"{{{ns['android']}}}title", item["title"])
pref.set(f"{{{ns['android']}}}summary", item["description"])
pref.set(f"{{{ns['app']}}}icon", item["icon"])
pref.set(f"{{{ns['app']}}}iconSpaceReserved", item["icon_space_reserved"])
intent = etree.SubElement(pref, "intent", nsmap=ns)
intent.set(f"{{{ns['android']}}}action", "android.intent.action.VIEW")
intent.set(f"{{{ns['android']}}}data", item["url"])
intent.set(f"{{{ns['app']}}}iconSpaceReserved", item["icon_space_reserved"])
intent = etree.SubElement(pref, "intent", nsmap=ns)
intent.set(f"{{{ns['android']}}}action", "android.intent.action.VIEW")
intent.set(f"{{{ns['android']}}}data", item["url"])
intent.set(f"{{{ns['app']}}}iconSpaceReserved", item["icon_space_reserved"])
return cat
return cat
def apply(config: Config, base: Dict[str, Any]) -> bool:
# Добавление кастомных иконок
shutil.copy(
"./resources/ic_custom_crown.xml",
"./decompiled/res/drawable/ic_custom_crown.xml",
)
insert_after_public("warning_error_counter_background", "ic_custom_crown")
def apply(self, base: Dict[str, Any]) -> bool:
# Добавление кастомных иконок
shutil.copy(
"./resources/ic_custom_crown.xml",
"./decompiled/res/drawable/ic_custom_crown.xml",
)
insert_after_public("warning_error_counter_background", "ic_custom_crown")
shutil.copy(
"./resources/ic_custom_telegram.xml",
"./decompiled/res/drawable/ic_custom_telegram.xml",
)
insert_after_public("warning_error_counter_background", "ic_custom_telegram")
shutil.copy(
"./resources/ic_custom_telegram.xml",
"./decompiled/res/drawable/ic_custom_telegram.xml",
)
insert_after_public("warning_error_counter_background", "ic_custom_telegram")
file_path = "./decompiled/res/xml/preference_main.xml"
parser = etree.XMLParser(remove_blank_text=True)
tree = etree.parse(file_path, parser)
root = tree.getroot()
file_path = "./decompiled/res/xml/preference_main.xml"
parser = etree.XMLParser(remove_blank_text=True)
tree = etree.parse(file_path, parser)
root = tree.getroot()
# Вставка новых пунктов перед последним
pos = root.index(root[-1])
for section, items in config.menu.items():
root.insert(pos, make_category(base["xml_ns"], section, items))
pos += 1
# Вставка новых пунктов перед последним
pos = root.index(root[-1])
for section, items in self.menu.items():
root.insert(pos, self.make_category(base["xml_ns"], section, items))
pos += 1
# Сохранение
tree.write(file_path, pretty_print=True, xml_declaration=True, encoding="utf-8")
# Сохранение
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
# Добавление суффикса версии
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('"')]
+ self.version
+ line[line.rindex('"') :]
)
else:
content += line
with open(filepath, "w", encoding="utf-8") as file:
file.write(content)
return True
+23 -23
View File
@@ -11,43 +11,43 @@
}
"""
priority = 0
__author__ = "wowlikon <wowlikon@gmail.com>"
__version__ = "1.0.0"
from typing import Any, Dict
# imports
from tqdm import tqdm
from lxml import etree
from typing import Dict, Any
from pydantic import Field
from utils.config import PatchConfig
from utils.config import PatchTemplate
#Config
DEFAULT_FORMATS = {
"share_channel_text": "Канал: «%1$s»\n%2$schannel/%3$d",
"share_collection_text": "Коллекция: «%1$s»\n%2$scollection/%3$d",
"share_profile_text": "Профиль пользователя «%1$s»\n%2$sprofile/%3$d",
"share_release_text": "Релиз: «%1$s»\n%2$srelease/%3$d"
"share_release_text": "Релиз: «%1$s»\n%2$srelease/%3$d",
}
class Config(PatchConfig):
format: Dict[str, str] = Field(DEFAULT_FORMATS, description="Строки для замены в `strings.xml`")
# Patch
def apply(config: Config, base: Dict[str, Any]) -> bool:
class Patch(PatchTemplate):
priority: int = Field(frozen=True, exclude=True, default=0)
format: Dict[str, str] = Field(
DEFAULT_FORMATS, description="Строки для замены в `strings.xml`"
)
file_path = "./decompiled/res/values/strings.xml"
def apply(self, base: Dict[str, Any]) -> bool:
file_path = "./decompiled/res/values/strings.xml"
parser = etree.XMLParser(remove_blank_text=True)
tree = etree.parse(file_path, parser)
root = tree.getroot()
parser = etree.XMLParser(remove_blank_text=True)
tree = etree.parse(file_path, parser)
root = tree.getroot()
# Обновляем значения
for string in root.findall("string"):
name = string.get("name")
if name in config.format:
string.text = config.format[name]
# Обновляем значения
for string in root.findall("string"):
name = string.get("name")
if name in self.format:
string.text = self.format[name]
# Сохраняем обратно
tree.write(file_path, encoding="utf-8", xml_declaration=True, pretty_print=True)
# Сохраняем обратно
tree.write(file_path, encoding="utf-8", xml_declaration=True, pretty_print=True)
return True
return True
+19 -22
View File
@@ -6,33 +6,30 @@
}
"""
priority = 0
__author__ = "wowlikon <wowlikon@gmail.com>"
__version__ = "1.0.0"
from typing import Any, Dict, List
# imports
from tqdm import tqdm
from typing import Dict, List, Any
from pydantic import Field
from utils.config import PatchConfig
from utils.config import PatchTemplate
from utils.public import insert_after_id, insert_after_public
from utils.smali_parser import float_to_hex
from utils.public import (
insert_after_public,
insert_after_id,
)
#Config
class Config(PatchConfig):
speeds: List[float] = Field([9.0], description="Список пользовательских скоростей воспроизведения")
# Patch
def apply(config: Config, base: Dict[str, Any]) -> bool:
assert float_to_hex(1.5) == "0x3fc00000"
class Patch(PatchTemplate):
priority: int = Field(frozen=True, exclude=True, default=0)
speeds: List[float] = Field(
[9.0], description="Список пользовательских скоростей воспроизведения"
)
last = "speed75"
for speed in config.speeds:
insert_after_public(last, f"speed{int(float(speed)*10)}")
insert_after_id(last, f"speed{int(float(speed)*10)}")
last = f"speed{int(float(speed)*10)}"
def apply(self, base: Dict[str, Any]) -> bool:
assert float_to_hex(1.5) == "0x3fc00000"
return False
last = "speed75"
for speed in self.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
+21 -16
View File
@@ -4,7 +4,7 @@
Каждый патч должен независеть от других патчей и проверять себя при применении. Он не должен вернуть True, если есть проблемы.
На данный момент каждый патч должен иметь функцию `apply`, которая принимает на вход конфигурацию и возвращает True или False.
И модель `Config`, которая наследуется от `PatchConfig` (поле `enabled` добавлять не нужно).
И модель `Config`, которая наследуется от `PatchTemplate` (поле `enabled` добавлять не нужно).
Это позволяет упростить конфигурирование и проверять типы данных, и делать значения по умолчанию.
При успешном применении патча, функция apply должна вернуть True, иначе False.
Ошибка будет интерпретирована как False. С выводом ошибки в консоль.
@@ -25,24 +25,29 @@ python ./main.py build --verbose
}
"""
priority = 0 # Приоритет патча, чем выше, тем раньше он будет применен
__author__ = "wowlikon <wowlikon@gmail.com>"
__version__ = "1.0.0"
from typing import Any, Dict, List
# imports
from tqdm import tqdm
from typing import Dict, List, Any
from pydantic import Field
from tqdm import tqdm
from utils.config import PatchConfig
from utils.config import PatchTemplate
#Config
class Config(PatchConfig):
class Patch(PatchTemplate):
example: bool = Field(True, description="Пример кастомного параметра")
# Patch
def apply(config: Config, base: Dict[str, Any]) -> bool: # Анотации типов для удобства, читаемости и поддержки IDE
tqdm.write("Вывод информации через tqdm, чтобы не мешать прогресс-бару")
tqdm.write("Пример включен" if config.example else "Пример отключен")
if base["verbose"]:
tqdm.write("Для вывода подробной и отладочной информации используйте флаг --verbose")
return True
def apply(
self, base: Dict[str, Any]
) -> bool: # Анотации типов для удобства, читаемости и поддержки IDE
priority: int = Field(
frozen=True, exclude=True, default=0
) # Приоритет патча, чем выше, тем раньше он будет применен
tqdm.write("Вывод информации через tqdm, чтобы не мешать прогресс-бару")
tqdm.write("Пример включен" if self.example else "Пример отключен")
if base["verbose"]:
tqdm.write(
"Для вывода подробной и отладочной информации используйте флаг --verbose"
)
return True
+31 -32
View File
@@ -11,22 +11,20 @@
}
"""
priority = 0
# imports
import os
__author__ = "wowlikon <wowlikon@gmail.com>"
__version__ = "1.0.0"
import shutil
from pydantic import Field
from typing import Dict, Any
from utils.config import PatchConfig
from utils.smali_parser import (
find_and_replace_smali_line,
get_smali_lines,
save_smali_lines
)
from typing import Any, Dict
#Config
class Config(PatchConfig):
from pydantic import Field
from utils.config import PatchTemplate
from utils.smali_parser import (find_and_replace_smali_line, get_smali_lines,
save_smali_lines)
class Patch(PatchTemplate):
priority: int = Field(frozen=True, exclude=True, default=0)
title: str = Field("Anixarty", description="Заголовок")
description: str = Field("Описание", description="Описание")
link_text: str = Field("МЫ В TELEGRAM", description="Текст ссылки")
@@ -34,23 +32,24 @@ class Config(PatchConfig):
skip_text: str = Field("Пропустить", description="Текст кнопки пропуска")
title_bg_color: str = Field("#FFFFFF", description="Цвет фона заголовка")
def apply(self, base: Dict[str, Any]) -> bool:
file_path = "./decompiled/smali_classes2/com/swiftsoft/anixartd/ui/activity/MainActivity.smali"
# Добавление ресурсов окна первого входа
shutil.copy("./resources/avatar.png", "./decompiled/assets/avatar.png")
shutil.copy(
"./resources/OpenSans-Regular.ttf",
"./decompiled/assets/OpenSans-Regular.ttf",
)
shutil.copytree("./resources/smali_classes4/", "./decompiled/smali_classes4/")
# Patch
def apply(config: Config, base: Dict[str, Any]) -> bool:
# Добавление ресурсов окна первого входа
shutil.copy("./resources/avatar.png", "./decompiled/assets/avatar.png")
shutil.copy(
"./resources/OpenSans-Regular.ttf",
"./decompiled/assets/OpenSans-Regular.ttf",
)
shutil.copytree(
"./resources/smali_classes4/", "./decompiled/smali_classes4/"
)
method = "invoke-super {p0}, Lmoxy/MvpAppCompatActivity;->onResume()V"
lines = get_smali_lines(file_path)
lines = find_and_replace_smali_line(
lines,
method,
method
+ "\ninvoke-static {p0}, Lcom/swiftsoft/about/$2;->oooooo(Landroid/content/Context;)Ljava/lang/Object;",
)
save_smali_lines(file_path, lines)
file_path = "./decompiled/smali_classes2/com/swiftsoft/anixartd/ui/activity/MainActivity.smali"
method = "invoke-super {p0}, Lmoxy/MvpAppCompatActivity;->onResume()V"
lines = get_smali_lines(file_path)
lines = find_and_replace_smali_line(lines, method, method+"\ninvoke-static {p0}, Lcom/swiftsoft/about/$2;->oooooo(Landroid/content/Context;)Ljava/lang/Object;")
save_smali_lines(file_path, lines)
return True
return True