v1.1
This commit is contained in:
+34
-58
@@ -4,38 +4,33 @@ import sys
|
||||
import shutil
|
||||
import argparse
|
||||
|
||||
# 根据命令行参数决定打包中文版还是英文版
|
||||
# 用法: python patch_android.py --lang zh 或 --lang en
|
||||
# 根据命令行参数决定打包语言版本
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--lang", choices=["zh", "en"], default="zh", help="打包语言版本")
|
||||
args = parser.parse_args()
|
||||
|
||||
LANG = args.lang
|
||||
PKG_NAME = "juneix.embyx"
|
||||
PKG_PATH = PKG_NAME.replace(".", "/") # juneix/embyx
|
||||
PKG_PATH = PKG_NAME.replace(".", "/")
|
||||
APP_NAME = "EmbyX"
|
||||
ICON_SRC = f"{LANG}/icon.png" # zh/icon.png 或 en/icon.png
|
||||
ICON_SRC = f"{LANG}/icon.png"
|
||||
|
||||
# ── 0. 从 HTML 徽章提取版本号 ─────────────────────────────────────────────────
|
||||
# HTML 中的版本徽章格式为 ">v1.1<",两个版本的徽章内容保持一致,统一读 zh/index.html
|
||||
# 修改规格:如需从其他文件读取版本,修改下方 VERSION_SRC 路径即可
|
||||
VERSION_SRC = "zh/index.html"
|
||||
version_name = "1.0" # 默认回退值
|
||||
version_code = 100 # 对应 v1.0
|
||||
version_name = "1.0"
|
||||
version_code = 100
|
||||
|
||||
if os.path.exists(VERSION_SRC):
|
||||
with open(VERSION_SRC, "r", encoding="utf-8") as f:
|
||||
html = f.read()
|
||||
# 匹配徽章文本,例如 ">v1.1<" 或 ">v2.0<"(非贪婪,只取第一个)
|
||||
# 匹配徽章文本,例如 ">v1.1<" 或 ">v2.0<"
|
||||
m = re.search(r">v(\d+)\.(\d+)(?:\.(\d+))?<", html)
|
||||
if m:
|
||||
major = int(m.group(1))
|
||||
minor = int(m.group(2))
|
||||
patch = int(m.group(3)) if m.group(3) else 0
|
||||
version_name = f"{major}.{minor}" if patch == 0 else f"{major}.{minor}.{patch}"
|
||||
# versionCode 规则:major×10000 + minor×100 + patch
|
||||
# 示例:v1.0→10000, v1.1→10100, v1.9→10900, v2.0→20000, v1.1.2→10102
|
||||
# 这样三段版本号也能正确递增,且不会与两段版本号冲突
|
||||
# versionCode 规则:major×10000 + minor×100 + patch 保证覆盖安装递增
|
||||
version_code = major * 10000 + minor * 100 + patch
|
||||
print(f" Detected version: v{version_name} → versionCode={version_code}")
|
||||
else:
|
||||
@@ -46,7 +41,6 @@ else:
|
||||
print(f"Patching Android Project for lang={LANG}, pkg={PKG_NAME}, version={version_name}...")
|
||||
|
||||
# ── 1. 图标文件 ──────────────────────────────────────────────────────────────
|
||||
# 将对应语言版本的 icon.png 复制到 Android drawable 资源目录
|
||||
os.makedirs("android/app/src/main/res/drawable", exist_ok=True)
|
||||
if os.path.exists(ICON_SRC):
|
||||
shutil.copy(ICON_SRC, "android/app/src/main/res/drawable/icon.png")
|
||||
@@ -59,20 +53,19 @@ manifest_path = "android/app/src/main/AndroidManifest.xml"
|
||||
with open(manifest_path, "r", encoding="utf-8") as f:
|
||||
manifest = f.read()
|
||||
|
||||
# 替换默认图标引用为我们的 drawable/icon
|
||||
# 替换默认图标引用
|
||||
manifest = manifest.replace("@mipmap/ic_launcher_round", "@drawable/icon")
|
||||
manifest = manifest.replace("@mipmap/ic_launcher", "@drawable/icon")
|
||||
manifest = manifest.replace("@drawable/icon_round", "@drawable/icon")
|
||||
|
||||
# 添加 WAKE_LOCK 权限(屏保/常亮需要)
|
||||
# 添加 WAKE_LOCK 权限(屏保与视频常亮需要)
|
||||
if "android.permission.WAKE_LOCK" not in manifest:
|
||||
manifest = manifest.replace(
|
||||
"</manifest>",
|
||||
' <uses-permission android:name="android.permission.WAKE_LOCK" />\n</manifest>'
|
||||
)
|
||||
|
||||
# 注册 EmbyXDreamService(Android 系统屏保服务)
|
||||
# 修改规格:如需换 label 或 icon,修改下方 android:label / android:permission 即可
|
||||
# 注册系统屏保服务 EmbyXDreamService
|
||||
service_block = """
|
||||
<service
|
||||
android:name=".EmbyXDreamService"
|
||||
@@ -97,8 +90,6 @@ with open(manifest_path, "w", encoding="utf-8") as f:
|
||||
print(" Patched AndroidManifest.xml")
|
||||
|
||||
# ── 3. MainActivity.java 补丁 ────────────────────────────────────────────────
|
||||
# 添加全屏沉浸式模式 + FLAG_KEEP_SCREEN_ON(视频播放常亮)
|
||||
# 修改规格:如需改变沉浸式行为,修改下方 SYSTEM_UI_FLAG_* 标志位组合
|
||||
main_activity_path = f"android/app/src/main/java/{PKG_PATH}/MainActivity.java"
|
||||
with open(main_activity_path, "r", encoding="utf-8") as f:
|
||||
main_activity = f.read()
|
||||
@@ -119,7 +110,7 @@ immersive_code = """
|
||||
}
|
||||
"""
|
||||
|
||||
# 添加 FLAG_KEEP_SCREEN_ON(视频播放常亮,防止熄屏打断视频)
|
||||
# 添加 FLAG_KEEP_SCREEN_ON(防止视频播放时熄屏)
|
||||
if "FLAG_KEEP_SCREEN_ON" not in main_activity:
|
||||
main_activity = main_activity.replace(
|
||||
"super.onCreate(savedInstanceState);",
|
||||
@@ -135,10 +126,7 @@ with open(main_activity_path, "w", encoding="utf-8") as f:
|
||||
print(" Patched MainActivity.java (fullscreen + keep screen on)")
|
||||
|
||||
# ── 4. EmbyXDreamService.java ───────────────────────────────────────────────
|
||||
# 屏保实现:使用 Android DreamService,内嵌 WebView 加载 EmbyX 本地页面
|
||||
# Capacitor 打包后的 webDir 内容位于 file:///android_asset/public/index.html
|
||||
# EmbyX 会读取 localStorage 中已保存的 Emby 服务器配置,自动连接并播放
|
||||
# 修改规格:如需改变屏保交互性,修改 setInteractive(true/false)
|
||||
# 屏保服务使用 Android DreamService,内嵌 WebView 加载本地 index.html,读取 localStorage 中 Emby 配置自动播放
|
||||
dream_service_code = f"""package {PKG_NAME};
|
||||
|
||||
import android.service.dreams.DreamService;
|
||||
@@ -154,20 +142,18 @@ public class EmbyXDreamService extends DreamService {{
|
||||
public void onAttachedToWindow() {{
|
||||
super.onAttachedToWindow();
|
||||
|
||||
// 允许用户与屏保交互(点击/滑动视频)
|
||||
setInteractive(true);
|
||||
setFullscreen(true);
|
||||
|
||||
webView = new WebView(this);
|
||||
WebSettings settings = webView.getSettings();
|
||||
settings.setJavaScriptEnabled(true);
|
||||
settings.setDomStorageEnabled(true); // localStorage 需要,用于读取 Emby token
|
||||
settings.setDomStorageEnabled(true);
|
||||
settings.setDatabaseEnabled(true);
|
||||
settings.setMediaPlaybackRequiresUserGesture(false); // 屏保自动播放视频无需手势
|
||||
settings.setMixedContentMode(WebSettings.MIXED_CONTENT_ALWAYS_ALLOW); // 允许 HTTP 视频流
|
||||
settings.setMediaPlaybackRequiresUserGesture(false);
|
||||
settings.setMixedContentMode(WebSettings.MIXED_CONTENT_ALWAYS_ALLOW);
|
||||
|
||||
webView.setWebViewClient(new WebViewClient());
|
||||
// 加载 Capacitor 打包的本地 index.html(EmbyX 会自动读取已保存的 Emby 配置)
|
||||
webView.loadUrl("file:///android_asset/public/index.html");
|
||||
|
||||
setContentView(webView);
|
||||
@@ -176,7 +162,6 @@ public class EmbyXDreamService extends DreamService {{
|
||||
@Override
|
||||
public void onDreamingStarted() {{
|
||||
super.onDreamingStarted();
|
||||
// 屏保激活时保持屏幕常亮(播放视频需要)
|
||||
getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
|
||||
}}
|
||||
|
||||
@@ -197,7 +182,6 @@ with open(dream_service_path, "w", encoding="utf-8") as f:
|
||||
print(f" Created EmbyXDreamService.java at {dream_service_path}")
|
||||
|
||||
# ── 5. dream_info.xml ────────────────────────────────────────────────────────
|
||||
# Android 需要此 XML 文件来注册屏保,previewImage 显示在系统屏保选择列表中
|
||||
os.makedirs("android/app/src/main/res/xml", exist_ok=True)
|
||||
xml_code = f"""<?xml version="1.0" encoding="utf-8"?>
|
||||
<dream android:settingsActivity="{PKG_NAME}.MainActivity"
|
||||
@@ -209,18 +193,13 @@ with open("android/app/src/main/res/xml/dream_info.xml", "w", encoding="utf-8")
|
||||
print(" Created dream_info.xml")
|
||||
|
||||
# ── 6. 注入 APK 版本号到 build.gradle ───────────────────────────────────────
|
||||
# Android 覆盖安装依赖 versionCode(整数)必须递增,versionName 是人类可读标签
|
||||
# Capacitor 生成的默认值是 versionCode=1, versionName="1.0"
|
||||
# 这里用从 HTML 读取的版本替换,保证每次发布 APK 能覆盖旧版本
|
||||
# 修改规格:versionCode 算法在脚本顶部第 0 步,修改 major/minor/patch 的乘数即可
|
||||
gradle_path = "android/app/build.gradle"
|
||||
if os.path.exists(gradle_path):
|
||||
with open(gradle_path, "r", encoding="utf-8") as f:
|
||||
gradle = f.read()
|
||||
|
||||
# 替换 versionCode(适配 Capacitor 8 / Gradle 8 的 "=" 赋值语法)
|
||||
# 替换 versionCode / versionName
|
||||
gradle = re.sub(r"versionCode(?:\s*=\s*|\s+)\d+", f"versionCode = {version_code}", gradle)
|
||||
# 替换 versionName
|
||||
gradle = re.sub(r'versionName(?:\s*=\s*|\s+)"[^"]+"', f'versionName = "{version_name}"', gradle)
|
||||
|
||||
with open(gradle_path, "w", encoding="utf-8") as f:
|
||||
@@ -229,12 +208,10 @@ if os.path.exists(gradle_path):
|
||||
else:
|
||||
print(f" WARNING: {gradle_path} not found, skipping version injection")
|
||||
|
||||
# ── 7. 启动页 (Splash Screen) 官方规范适配 ───────────────────────────────────
|
||||
# 遵循 Android 12+ SplashScreen API 标准 (Plan A)
|
||||
# ── 7. 启动页 (Splash Screen) 规范适配 ───────────────────────────────────
|
||||
res_dir = "android/app/src/main/res"
|
||||
os.makedirs(f"{res_dir}/values", exist_ok=True)
|
||||
|
||||
# 写入颜色资源
|
||||
colors_path = f"{res_dir}/values/colors.xml"
|
||||
colors_xml = """<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
@@ -247,7 +224,6 @@ with open(colors_path, "w", encoding="utf-8") as f:
|
||||
print(" Created/Updated colors.xml")
|
||||
|
||||
# ── 8. 修改主题 (Themes/Styles) 适配 Google SplashScreen API ─────────────────
|
||||
# 增强鲁棒性:同时查找 themes.xml 和 styles.xml,处理多种可能的生成路径
|
||||
target_files = [
|
||||
"values/themes.xml",
|
||||
"values-night/themes.xml",
|
||||
@@ -262,25 +238,28 @@ for rel_path in target_files:
|
||||
with open(full_path, "r", encoding="utf-8") as f:
|
||||
content = f.read()
|
||||
|
||||
# 使用更灵活的正则匹配 style 标签及其 parent
|
||||
# 兼容单引号/双引号/空格
|
||||
style_pattern = r'(<style\s+name="AppTheme\.NoActionBarLaunch"\s+parent=")(.*?)(">)'
|
||||
if re.search(style_pattern, content):
|
||||
content = re.sub(style_pattern, r'\1Theme.SplashScreen\3', content)
|
||||
# 定位启动主题节点,替换 parent 并注入规范属性
|
||||
style_block_pattern = r'(<style\s+name="AppTheme\.NoActionBarLaunch"[^>]*>)(.*?)(</style>)'
|
||||
m = re.search(style_block_pattern, content, flags=re.DOTALL)
|
||||
if m:
|
||||
start_tag = m.group(1)
|
||||
inner_items = m.group(2)
|
||||
|
||||
# 注入官方规范属性
|
||||
# 移除可能存在的旧 background 属性
|
||||
content = re.sub(r'<item\s+name="android:background">.*?</item>', '', content)
|
||||
content = re.sub(r'<item\s+name="windowSplashScreenBackground">.*?</item>', '', content)
|
||||
content = re.sub(r'<item\s+name="windowSplashScreenAnimatedIcon">.*?</item>', '', content)
|
||||
content = re.sub(r'<item\s+name="postSplashScreenTheme">.*?</item>', '', content)
|
||||
start_tag = re.sub(r'parent="[^"]*"', 'parent="Theme.SplashScreen"', start_tag)
|
||||
|
||||
# 清理历史兼容属性,避免冲突
|
||||
inner_items = re.sub(r'<item\s+name="android:background">.*?</item>', '', inner_items)
|
||||
inner_items = re.sub(r'<item\s+name="windowSplashScreenBackground">.*?</item>', '', inner_items)
|
||||
inner_items = re.sub(r'<item\s+name="windowSplashScreenAnimatedIcon">.*?</item>', '', inner_items)
|
||||
inner_items = re.sub(r'<item\s+name="postSplashScreenTheme">.*?</item>', '', inner_items)
|
||||
|
||||
splash_items = """
|
||||
<item name="windowSplashScreenBackground">@color/black</item>
|
||||
<item name="windowSplashScreenAnimatedIcon">@drawable/icon</item>
|
||||
<item name="postSplashScreenTheme">@style/AppTheme.NoActionBar</item>
|
||||
"""
|
||||
content = content.replace('</style>', f'{splash_items} </style>', 1)
|
||||
new_block = f"{start_tag}{inner_items}{splash_items} </style>"
|
||||
content = content[:m.start()] + new_block + content[m.end():]
|
||||
|
||||
with open(full_path, "w", encoding="utf-8") as f:
|
||||
f.write(content)
|
||||
@@ -290,20 +269,17 @@ for rel_path in target_files:
|
||||
if not splash_style_found:
|
||||
print(" WARNING: AppTheme.NoActionBarLaunch style not found in any res files!")
|
||||
|
||||
# ── 9. MainActivity 注入官方启动入口 ──────────────────────────────────────────
|
||||
# 确保在 super.onCreate 之前注入,且不重复注入
|
||||
# ── 9. MainActivity 注入启动页入口 ──────────────────────────────────────────
|
||||
with open(main_activity_path, "r", encoding="utf-8") as f:
|
||||
ma = f.read()
|
||||
|
||||
# 确保导入语句存在
|
||||
if "import androidx.core.splashscreen.SplashScreen;" not in ma:
|
||||
ma = ma.replace("import android.os.Bundle;", "import android.os.Bundle;\nimport androidx.core.splashscreen.SplashScreen;")
|
||||
|
||||
if "SplashScreen.installSplashScreen(this)" not in ma:
|
||||
# 精准查找 super.onCreate 并在其上方插入
|
||||
ma = ma.replace(
|
||||
"super.onCreate(savedInstanceState);",
|
||||
"SplashScreen.installSplashScreen(this);\n super.onCreate(savedInstanceState);\n // 设置透明背景防止闪烁\n this.bridge.getWebView().setBackgroundColor(android.graphics.Color.BLACK);"
|
||||
"SplashScreen.installSplashScreen(this);\n super.onCreate(savedInstanceState);\n // 设置透明底色防止切换回白屏闪烁\n this.bridge.getWebView().setBackgroundColor(android.graphics.Color.BLACK);"
|
||||
)
|
||||
|
||||
with open(main_activity_path, "w", encoding="utf-8") as f:
|
||||
|
||||
Reference in New Issue
Block a user